diff options
636 files changed, 18936 insertions, 5713 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java index 0f3b1c366fb0..033da2df9bf6 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -4944,10 +4944,14 @@ public class AlarmManagerService extends SystemService { @Override public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (action == null) { + return; + } final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); synchronized (mLock) { String pkgList[] = null; - switch (intent.getAction()) { + switch (action) { case Intent.ACTION_QUERY_PACKAGE_RESTART: pkgList = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES); for (String packageName : pkgList) { diff --git a/core/api/current.txt b/core/api/current.txt index 8eb881139b34..664dfe980b49 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -4919,6 +4919,7 @@ package android.app { method public int getPendingIntentBackgroundActivityStartMode(); method public int getPendingIntentCreatorBackgroundActivityStartMode(); method public int getSplashScreenStyle(); + method @FlaggedApi("com.android.window.flags.touch_pass_through_opt_in") public boolean isAllowPassThroughOnTouchOutside(); method @Deprecated public boolean isPendingIntentBackgroundActivityLaunchAllowed(); method public boolean isShareIdentityEnabled(); method public static android.app.ActivityOptions makeBasic(); @@ -4932,6 +4933,7 @@ package android.app { method public static android.app.ActivityOptions makeTaskLaunchBehind(); method public static android.app.ActivityOptions makeThumbnailScaleUpAnimation(android.view.View, android.graphics.Bitmap, int, int); method public void requestUsageTimeReport(android.app.PendingIntent); + method @FlaggedApi("com.android.window.flags.touch_pass_through_opt_in") public void setAllowPassThroughOnTouchOutside(boolean); method public android.app.ActivityOptions setAppVerificationBundle(android.os.Bundle); method public android.app.ActivityOptions setLaunchBounds(@Nullable android.graphics.Rect); method public android.app.ActivityOptions setLaunchDisplayId(int); @@ -6854,6 +6856,47 @@ package android.app { method public android.app.Notification.MessagingStyle.Message setData(String, android.net.Uri); } + @FlaggedApi("android.app.api_rich_ongoing") public static class Notification.ProgressStyle extends android.app.Notification.Style { + ctor public Notification.ProgressStyle(); + method @NonNull public android.app.Notification.ProgressStyle addProgressSegment(@NonNull android.app.Notification.ProgressStyle.Segment); + method @NonNull public android.app.Notification.ProgressStyle addProgressStep(@NonNull android.app.Notification.ProgressStyle.Step); + method public int getProgress(); + method @Nullable public android.graphics.drawable.Icon getProgressEndIcon(); + method public int getProgressMax(); + method @NonNull public java.util.List<android.app.Notification.ProgressStyle.Segment> getProgressSegments(); + method @Nullable public android.graphics.drawable.Icon getProgressStartIcon(); + method @NonNull public java.util.List<android.app.Notification.ProgressStyle.Step> getProgressSteps(); + method @Nullable public android.graphics.drawable.Icon getProgressTrackerIcon(); + method public boolean isProgressIndeterminate(); + method public boolean isStyledByProgress(); + method @NonNull public android.app.Notification.ProgressStyle setProgress(int); + method @NonNull public android.app.Notification.ProgressStyle setProgressEndIcon(@Nullable android.graphics.drawable.Icon); + method @NonNull public android.app.Notification.ProgressStyle setProgressIndeterminate(boolean); + method @NonNull public android.app.Notification.ProgressStyle setProgressSegments(@NonNull java.util.List<android.app.Notification.ProgressStyle.Segment>); + method @NonNull public android.app.Notification.ProgressStyle setProgressStartIcon(@Nullable android.graphics.drawable.Icon); + method @NonNull public android.app.Notification.ProgressStyle setProgressSteps(@NonNull java.util.List<android.app.Notification.ProgressStyle.Step>); + method @NonNull public android.app.Notification.ProgressStyle setProgressTrackerIcon(@Nullable android.graphics.drawable.Icon); + method @NonNull public android.app.Notification.ProgressStyle setStyledByProgress(boolean); + } + + public static final class Notification.ProgressStyle.Segment { + ctor public Notification.ProgressStyle.Segment(int); + method @ColorInt public int getColor(); + method public int getLength(); + method public int getStableId(); + method @NonNull public android.app.Notification.ProgressStyle.Segment setColor(@ColorInt int); + method @NonNull public android.app.Notification.ProgressStyle.Segment setStableId(int); + } + + public static final class Notification.ProgressStyle.Step { + ctor public Notification.ProgressStyle.Step(int); + method @ColorInt public int getColor(); + method public int getPosition(); + method public int getStableId(); + method @NonNull public android.app.Notification.ProgressStyle.Step setColor(@ColorInt int); + method @NonNull public android.app.Notification.ProgressStyle.Step setStableId(int); + } + public abstract static class Notification.Style { ctor @Deprecated public Notification.Style(); method public android.app.Notification build(); @@ -8732,13 +8775,20 @@ package android.app.admin { 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 java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); + method @Deprecated @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 java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); + 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 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 + field public static final int APP_FUNCTION_STATE_ENABLED = 1; // 0x1 } @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public abstract class AppFunctionService extends android.app.Service { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); - method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); + method @Deprecated @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); + method @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; } @@ -8773,6 +8823,7 @@ package android.app.appfunctions { 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_DENIED = 1; // 0x1 + field public static final int RESULT_DISABLED = 6; // 0x6 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_OK = 0; // 0x0 @@ -46655,6 +46706,8 @@ package android.telephony.data { field public static final int TYPE_IMS = 64; // 0x40 field public static final int TYPE_MCX = 1024; // 0x400 field public static final int TYPE_MMS = 2; // 0x2 + field @FlaggedApi("com.android.internal.telephony.flags.oem_paid_private") public static final int TYPE_OEM_PAID = 65536; // 0x10000 + field @FlaggedApi("com.android.internal.telephony.flags.oem_paid_private") public static final int TYPE_OEM_PRIVATE = 131072; // 0x20000 field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final int TYPE_RCS = 32768; // 0x8000 field public static final int TYPE_SUPL = 4; // 0x4 field public static final int TYPE_VSIM = 4096; // 0x1000 diff --git a/core/api/system-current.txt b/core/api/system-current.txt index bfddf4fb5fac..bc34f5bfe13f 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -6833,6 +6833,16 @@ package android.hardware.soundtrigger { field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.RecognitionConfig> CREATOR; } + public static final class SoundTrigger.RecognitionConfig.Builder { + ctor public SoundTrigger.RecognitionConfig.Builder(); + method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig build(); + method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setAllowMultipleTriggers(boolean); + method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setAudioCapabilities(int); + method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setCaptureRequested(boolean); + method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setData(@Nullable byte[]); + method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setKeyphrases(@NonNull java.util.Collection<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra>); + } + public static class SoundTrigger.RecognitionEvent { method @Nullable public android.media.AudioFormat getCaptureFormat(); method public int getCaptureSession(); @@ -15878,6 +15888,8 @@ package android.telephony.data { field public static final String TYPE_IMS_STRING = "ims"; field public static final String TYPE_MCX_STRING = "mcx"; field public static final String TYPE_MMS_STRING = "mms"; + field @FlaggedApi("com.android.internal.telephony.flags.oem_paid_private") public static final String TYPE_OEM_PAID_STRING = "oem_paid"; + field @FlaggedApi("com.android.internal.telephony.flags.oem_paid_private") public static final String TYPE_OEM_PRIVATE_STRING = "oem_private"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String TYPE_RCS_STRING = "rcs"; field public static final String TYPE_SUPL_STRING = "supl"; field public static final String TYPE_VSIM_STRING = "vsim"; diff --git a/core/api/test-current.txt b/core/api/test-current.txt index cc9e8367dc3d..0a10920154b8 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1614,15 +1614,15 @@ package android.hardware.camera2 { public final class CameraManager { method @NonNull public android.hardware.camera2.CameraCharacteristics getCameraCharacteristics(@NonNull String, boolean) throws android.hardware.camera2.CameraAccessException; method public String[] getCameraIdListNoLazy() throws android.hardware.camera2.CameraAccessException; - method @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static int getRotationOverrideInternal(@Nullable android.content.Context, @Nullable android.content.pm.PackageManager, @Nullable String); + method @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static int getRotationOverrideInternal(@Nullable android.content.Context, @Nullable android.content.pm.PackageManager, @Nullable String); method @RequiresPermission(android.Manifest.permission.CAMERA) public void openCamera(@NonNull String, boolean, @Nullable android.os.Handler, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException; method @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openCamera(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException; method public static boolean shouldOverrideToPortrait(@Nullable android.content.pm.PackageManager, @Nullable String); field public static final String LANDSCAPE_TO_PORTRAIT_PROP = "camera.enable_landscape_to_portrait"; field public static final long OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT = 250678880L; // 0xef10e60L - field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_NONE = 0; // 0x0 - field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT = 1; // 0x1 - field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_ROTATION_ONLY = 2; // 0x2 + field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_NONE = 0; // 0x0 + field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT = 1; // 0x1 + field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_ROTATION_ONLY = 2; // 0x2 } public abstract static class CameraManager.AvailabilityCallback { @@ -1887,7 +1887,7 @@ package android.hardware.soundtrigger { } @FlaggedApi("android.media.soundtrigger.manager_api") public static final class SoundTrigger.RecognitionConfig implements android.os.Parcelable { - ctor public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[], int); + ctor @Deprecated public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[], int); ctor public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[]); } diff --git a/core/java/Android.bp b/core/java/Android.bp index 92bca3cfbef2..99046328b1e2 100644 --- a/core/java/Android.bp +++ b/core/java/Android.bp @@ -21,14 +21,52 @@ filegroup { "**/*.aidl", ":framework-nfc-non-updatable-sources", ":messagequeue-gen", + ":ranging_stack_mock_initializer", ], // Exactly one MessageQueue.java will be added to srcs by messagequeue-gen exclude_srcs: [ "android/os/*MessageQueue/**/*.java", + "android/ranging/**/*.java", ], visibility: ["//frameworks/base"], } +//Mock to allow service registry for ranging stack. +//TODO(b/331206299): Remove this after RELEASE_RANGING_STACK is ramped up to next. +soong_config_module_type { + name: "ranging_stack_framework_mock_init", + module_type: "genrule", + config_namespace: "bootclasspath", + bool_variables: [ + "release_ranging_stack", + ], + properties: [ + "srcs", + "cmd", + "out", + ], +} + +// The actual RangingFrameworkInitializer is present in packages/modules/Uwb/ranging/framework. +// Mock RangingFrameworkInitializer does nothing and allows to successfully build +// SystemServiceRegistry after registering for system service in SystemServiceRegistry both with +// and without build flag RELEASE_RANGING_STACK enabled. +ranging_stack_framework_mock_init { + name: "ranging_stack_mock_initializer", + soong_config_variables: { + release_ranging_stack: { + cmd: "touch $(out)", + // Adding an empty file as out is mandatory. + out: ["android/ranging/empty_ranging_fw.txt"], + conditions_default: { + srcs: ["android/ranging/mock/RangingFrameworkInitializer.java"], + cmd: "mkdir -p android/ranging/; cp $(in) $(out);", + out: ["android/ranging/RangingFrameworkInitializer.java"], + }, + }, + }, +} + // Add selected MessageQueue.java implementation to srcs soong_config_module_type { name: "release_package_messagequeue_implementation_srcs", diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 91aa225039a4..0d183c7c37aa 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -26,6 +26,7 @@ import static android.content.Intent.FLAG_RECEIVER_FOREGROUND; import static android.view.Display.INVALID_DISPLAY; import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -453,6 +454,10 @@ public class ActivityOptions extends ComponentOptions { private static final String KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE = "android.activity.pendingIntentCreatorBackgroundActivityStartMode"; + /** See {@link #setAllowPassThroughOnTouchOutside(boolean)}. */ + private static final String KEY_ALLOW_PASS_THROUGH_ON_TOUCH_OUTSIDE = + "android.activity.allowPassThroughOnTouchOutside"; + /** * @see #setLaunchCookie * @hide @@ -554,6 +559,7 @@ public class ActivityOptions extends ComponentOptions { private int mPendingIntentCreatorBackgroundActivityStartMode = MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; private boolean mDisableStartingWindow; + private boolean mAllowPassThroughOnTouchOutside; /** * Create an ActivityOptions specifying a custom animation to run when @@ -1416,6 +1422,7 @@ public class ActivityOptions extends ComponentOptions { KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE, MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED); mDisableStartingWindow = opts.getBoolean(KEY_DISABLE_STARTING_WINDOW); + mAllowPassThroughOnTouchOutside = opts.getBoolean(KEY_ALLOW_PASS_THROUGH_ON_TOUCH_OUTSIDE); mAnimationAbortListener = IRemoteCallback.Stub.asInterface( opts.getBinder(KEY_ANIM_ABORT_LISTENER)); } @@ -1839,6 +1846,39 @@ public class ActivityOptions extends ComponentOptions { && mLaunchIntoPipParams.isLaunchIntoPip(); } + /** + * Returns whether the source activity allows the overlaying activities from the to-be-launched + * app to pass through touch events to it when touches fall outside the content window. + * + * @see #setAllowPassThroughOnTouchOutside(boolean) + */ + @FlaggedApi(com.android.window.flags.Flags.FLAG_TOUCH_PASS_THROUGH_OPT_IN) + public boolean isAllowPassThroughOnTouchOutside() { + return mAllowPassThroughOnTouchOutside; + } + + /** + * Sets whether the source activity allows the overlaying activities from the to-be-launched + * app to pass through touch events to it when touches fall outside the content window. + * + * <p> By default, touches that fall on a translucent non-touchable area of an overlaying + * activity window are blocked from passing through to the activity below (source activity), + * unless the overlaying activity is from the same UID as the source activity. The source + * activity may use this method to opt in and allow the overlaying activities from the + * to-be-launched app to pass through touches to itself. The source activity needs to ensure + * that it trusts the overlaying activity and its content is not vulnerable to UI redressing + * attacks. The flag is ignored if the context calling + * {@link Context#startActivity(Intent, Bundle)} is not an activity. + * + * <p> For backward compatibility, apps with target SDK 35 and below may still receive + * pass-through touches without opt-in if the cross-uid activity is launched by the source + * activity. + */ + @FlaggedApi(com.android.window.flags.Flags.FLAG_TOUCH_PASS_THROUGH_OPT_IN) + public void setAllowPassThroughOnTouchOutside(boolean allowed) { + mAllowPassThroughOnTouchOutside = allowed; + } + /** @hide */ public int getLaunchActivityType() { return mLaunchActivityType; @@ -2520,6 +2560,10 @@ public class ActivityOptions extends ComponentOptions { if (mDisableStartingWindow) { b.putBoolean(KEY_DISABLE_STARTING_WINDOW, mDisableStartingWindow); } + if (mAllowPassThroughOnTouchOutside) { + b.putBoolean(KEY_ALLOW_PASS_THROUGH_ON_TOUCH_OUTSIDE, + mAllowPassThroughOnTouchOutside); + } b.putBinder(KEY_ANIM_ABORT_LISTENER, mAnimationAbortListener != null ? mAnimationAbortListener.asBinder() : null); return b; diff --git a/core/java/android/app/DownloadManager.java b/core/java/android/app/DownloadManager.java index b781ce50c4db..f21c3e8d44d6 100644 --- a/core/java/android/app/DownloadManager.java +++ b/core/java/android/app/DownloadManager.java @@ -493,6 +493,9 @@ public class DownloadManager { * {@link Environment#getExternalStoragePublicDirectory(String)} with * {@link Environment#DIRECTORY_DOWNLOADS}). * + * All non-visible downloads that are not modified in the last 7 days will be deleted during + * idle runs. + * * @param uri a file {@link Uri} indicating the destination for the downloaded file. * @return this object */ @@ -796,7 +799,9 @@ public class DownloadManager { * public Downloads directory (as returned by * {@link Environment#getExternalStoragePublicDirectory(String)} with * {@link Environment#DIRECTORY_DOWNLOADS}) will be visible in system's Downloads UI - * and the rest will not be visible. + * and the rest will not be visible. All non-visible downloads that are not modified + * in the last 7 days will be deleted during idle runs. + * * (e.g. {@link Context#getExternalFilesDir(String)}) will not be visible. */ @Deprecated diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 392a1f113c23..c21fe0e2d8b3 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -121,7 +121,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -783,10 +782,32 @@ public class Notification implements Parcelable @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) public static final int FLAG_PROMOTED_ONGOING = 0x00040000; - private static final List<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Arrays.asList( - BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class, - DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class, - MessagingStyle.class, CallStyle.class); + private static final Set<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Set.of( + BigTextStyle.class, + BigPictureStyle.class, + InboxStyle.class, + MediaStyle.class, + DecoratedCustomViewStyle.class, + DecoratedMediaCustomViewStyle.class, + MessagingStyle.class, + CallStyle.class + ); + + private static boolean isPlatformStyle(Style style) { + if (style == null) { + return false; + } + + if (PLATFORM_STYLE_CLASSES.contains(style.getClass())) { + return true; + } + + if (Flags.apiRichOngoing()) { + return style.getClass() == ProgressStyle.class; + } + + return false; + } /** @hide */ @IntDef(flag = true, prefix = {"FLAG_"}, value = { @@ -1598,26 +1619,70 @@ public class Notification implements Parcelable public static final String EXTRA_DECLINE_COLOR = "android.declineColor"; /** - * {@link #extras} key: {@link Icon} of an image used as an overlay Icon on - * {@link Notification#mLargeIcon} for {@link EnRouteStyle} notifications. - * This extra is an {@code Icon}. + * {@link #extras} key: whether the notification should be colorized as + * supplied to {@link Builder#setColorized(boolean)}. + */ + public static final String EXTRA_COLORIZED = "android.colorized"; + + /** + * {@link #extras} key: an arraylist of {@link android.app.Notification.ProgressStyle.Segment} + * bundles provided by a + * {@link android.app.Notification.ProgressStyle} notification as supplied to + * {@link ProgressStyle#setProgressSegments} + * or {@link ProgressStyle#addProgressSegment(ProgressStyle.Segment)}. + * This extra is a parcelable array list of bundles. * @hide */ @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) - public static final String EXTRA_ENROUTE_OVERLAY_ICON = "android.enrouteOverlayIcon"; + public static final String EXTRA_PROGRESS_SEGMENTS = "android.progressSegments"; /** - * {@link #extras} key: text used as a sub-text for the largeIcon of - * {@link EnRouteStyle} notification. This extra is a {@code CharSequence}. + * {@link #extras} key: an arraylist of {@link android.app.Notification.ProgressStyle.Step} + * bundles provided by a + * {@link android.app.Notification.ProgressStyle} notification as supplied to + * {@link ProgressStyle#setProgressSteps} + * or {@link ProgressStyle#addProgressStep(ProgressStyle.Step)}. + * This extra is a parcelable array list of bundles. * @hide */ @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) - public static final String EXTRA_ENROUTE_LARGE_ICON_SUBTEXT = "android.enrouteLargeIconSubText"; + public static final String EXTRA_PROGRESS_STEPS = "android.progressSteps"; + /** - * {@link #extras} key: whether the notification should be colorized as - * supplied to {@link Builder#setColorized(boolean)}. + * {@link #extras} key: whether the progress bar should be styled by its progress as + * supplied to {@link ProgressStyle#setStyledByProgress}. + * This extra is a boolean. + * @hide */ - public static final String EXTRA_COLORIZED = "android.colorized"; + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static final String EXTRA_STYLED_BY_PROGRESS = "android.styledByProgress"; + + /** + * {@link #extras} key: this is an {@link Icon} of an image to be + * shown as progress bar progress tracker icon in {@link ProgressStyle}, supplied to + *{@link ProgressStyle#setProgressTrackerIcon(Icon)}. + * @hide + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static final String EXTRA_PROGRESS_TRACKER_ICON = "android.progressTrackerIcon"; + + /** + * {@link #extras} key: this is an {@link Icon} of an image to be + * shown at the beginning of the progress bar in {@link ProgressStyle}, supplied to + *{@link ProgressStyle#setProgressStartIcon(Icon)}. + * @hide + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static final String EXTRA_PROGRESS_START_ICON = "android.progressStartIcon"; + + /** + * {@link #extras} key: this is an {@link Icon} of an image to be + * shown at the end of the progress bar in {@link ProgressStyle}, supplied to + *{@link ProgressStyle#setProgressEndIcon(Icon)}. + * @hide + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static final String EXTRA_PROGRESS_END_ICON = "android.progressEndIcon"; /** * @hide @@ -3071,7 +3136,9 @@ public class Notification implements Parcelable } if (Flags.apiRichOngoing()) { - visitIconUri(visitor, extras.getParcelable(EXTRA_ENROUTE_OVERLAY_ICON, Icon.class)); + visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_TRACKER_ICON, Icon.class)); + visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_START_ICON, Icon.class)); + visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_END_ICON, Icon.class)); } if (mBubbleMetadata != null) { @@ -6630,7 +6697,7 @@ public class Notification implements Parcelable // Custom views which come from a platform style class are safe, and thus do not need to // be wrapped. Any subclass of those styles has the opportunity to make arbitrary // changes to the RemoteViews, and thus can't be trusted as a fully vetted view. - if (fromStyle && PLATFORM_STYLE_CLASSES.contains(mStyle.getClass())) { + if (fromStyle && isPlatformStyle(mStyle)) { return false; } return mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S; @@ -7971,6 +8038,12 @@ public class Notification implements Parcelable return innerClass; } } + + if (Flags.apiRichOngoing()) { + if (templateClass.equals(ProgressStyle.class.getName())) { + return ProgressStyle.class; + } + } return null; } @@ -11083,92 +11156,396 @@ public class Notification implements Parcelable } /** - * TODO(b/360827871): Make EnRouteStyle public. - * A style used to represent the progress of a real-world journey with a known destination. - * For example: - * <ul> - * <li>Delivery tracking</li> - * <li>Ride progress</li> - * <li>Flight tracking</li> - * </ul> + * A Notification Style used to to define a notification whose expanded state includes + * a highly customizable progress bar with segments, steps, a custom tracker icon, + * and custom icons at the start and end of the progress bar. + * + * This style is suggested for use cases where the app is showing a tracker to the + * user of a thing they are interested in: the location of a car on its way + * to pick them up, food being delivered, or their own progress in a navigation + * journey. + * + * To use this style with your Notification, feed it to + * {@link Notification.Builder#setStyle(android.app.Notification.Style)} like so: + * <pre class="prettyprint"> + * new Notification.Builder(context) + * .setSmallIcon(R.drawable.ic_notification) + * .setColor(Color.GREEN) + * .setColorized(true) + * .setContentTitle("Arrive 10:08 AM"). + * .setContentText("Dominique Ansel Bakery Soho") + * .addAction(new Notification.Action("Exit navigation",...)) + * .setStyle(new Notification.ProgressStyle() + * .setStyledByProgress(false) + * .setProgress(456) + * .setProgressTrackerIcon(Icon.createWithResource(R.drawable.ic_driving_tracker)) + * .addProgressSegment(new Segment(41).setColor(Color.BLACK)) + * .addProgressSegment(new Segment(552).setColor(Color.YELLOW)) + * .addProgressSegment(new Segment(253).setColor(Color.YELLOW)) + * .addProgressSegment(new Segment(94).setColor(Color.BLUE)) + * .addProgressStep(new Step(60).setColor(Color.RED)) + * .addProgressStep(new Step(560).setColor(Color.YELLOW)) + * ) + * </pre> + * + * + * + * NOTE: The progress bar layout will be mirrored for RTL layout. + * NOTE: The extras set by {@link Notification.Builder#setProgress} will be overridden by + * the values set on this style object when the notification is built. * - * The exact fields from {@link Notification} that are shown with this style may vary by - * the surface where this update appears, but the following fields are recommended: - * <ul> - * <li>{@link Notification.Builder#setContentTitle}</li> - * <li>{@link Notification.Builder#setContentText}</li> - * <li>{@link Notification.Builder#setSubText}</li> - * <li>{@link Notification.Builder#setLargeIcon}</li> - * <li>{@link Notification.Builder#setProgress}</li> - * <li>{@link Notification.Builder#setWhen} - This should be the future time of the next, - * final, or most important stop on this journey.</li> - * </ul> - * @hide */ @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) - public static class EnRouteStyle extends Notification.Style { + public static class ProgressStyle extends Notification.Style { + private static final String KEY_ELEMENT_STABLE_ID = "stableId"; + private static final String KEY_ELEMENT_COLOR = "colorInt"; + private static final String KEY_SEGMENT_LENGTH = "length"; + private static final String KEY_STEP_POSITION = "position"; - @Nullable - private Icon mOverlayIcon = null; + private static final int MAX_PROGRESS_SEGMENT_LIMIT = 15; + private static final int MAX_PROGRESS_STEP_LIMIT = 5; + private static final int DEFAULT_PROGRESS_MAX = 100; + private List<Segment> mProgressSegments = new ArrayList<>(); + private List<Step> mProgressSteps = new ArrayList<>(); + + private int mProgress = 0; + + private boolean mIndeterminate; + + private boolean mIsStyledByProgress = true; + + @Nullable + private Icon mTrackerIcon; @Nullable - private CharSequence mLargeIconSubText = null; + private Icon mStartIcon; + @Nullable + private Icon mEndIcon; + + /** + * @hide + */ + @Override + public boolean areNotificationsVisiblyDifferent(Style other) { + if (other == null || getClass() != other.getClass()) { + return true; + } - public EnRouteStyle() { + final ProgressStyle progressStyle = (ProgressStyle) other; + + /** + * @see #setProgressIndeterminate + */ + if (!Objects.equals(mIndeterminate, progressStyle.mIndeterminate)) { + return true; + } + boolean nonIndeterminateCheckResult = false; + if (!mIndeterminate) { + nonIndeterminateCheckResult = !Objects.equals(mProgress, progressStyle.mProgress) + || !Objects.equals(mIsStyledByProgress, progressStyle.mIsStyledByProgress) + || !Objects.equals(mProgressSegments, progressStyle.mProgressSegments) + || !Objects.equals(mProgressSteps, progressStyle.mProgressSteps) + || !Objects.equals(mTrackerIcon, progressStyle.mTrackerIcon); + } + + return !Objects.equals(mStartIcon, progressStyle.mStartIcon) + || !Objects.equals(mEndIcon, progressStyle.mEndIcon) + || nonIndeterminateCheckResult; } /** - * Returns the overlay icon to be displayed on {@link Notification#mLargeIcon}. - * @see EnRouteStyle#setOverlayIcon + * Gets the segments that define the background layer of the progress bar. + * + * If no segments are provided, the progress bar will be rendered with a single segment + * with length 100 and default color. + * + * @see #setProgressSegments + * @see #addProgressSegment + * @see Segment */ - @Nullable - public Icon getOverlayIcon() { - return mOverlayIcon; + public @NonNull List<Segment> getProgressSegments() { + return mProgressSegments; } /** - * Optional icon to be displayed on {@link Notification#mLargeIcon}. + * Sets or replaces the segments of the progress bar. * - * This image will be cropped to a circle and will obscure - * a semicircle of the right side of the large icon. + * Segments allow for creating progress bars with multiple colors or sections + * to represent different stages or categories of progress. + * For example, Traffic conditions along a navigation journey. + * @see Segment */ - @NonNull - public EnRouteStyle setOverlayIcon(@Nullable Icon overlayIcon) { - mOverlayIcon = overlayIcon; + public @NonNull ProgressStyle setProgressSegments(@NonNull List<Segment> progressSegments) { + mProgressSegments = new ArrayList<>(progressSegments.size()); return this; } /** - * Returns the sub-text for {@link Notification#mLargeIcon}. - * @see EnRouteStyle#setLargeIconSubText + * Appends a segment to the end of the progress bar. + * + * Segments allow for creating progress bars with multiple colors or sections + * to represent different stages or categories of progress. + * For example, Traffic conditions along a navigation journey. + * @see Segment */ - @Nullable - public CharSequence getLargeIconSubText() { - return mLargeIconSubText; + public @NonNull ProgressStyle addProgressSegment(@NonNull Segment segment) { + if (mProgressSegments == null) { + mProgressSegments = new ArrayList<>(); + } + mProgressSegments.add(segment); + + return this; } /** - * Optional text which generally related to - * the {@link Notification.Builder#setLargeIcon} or {@link #setOverlayIcon} or both. + * Gets the steps that are displayed on the progress bar. + *. + * @see #setProgressSteps + * @see #addProgressStep + * @see Step */ - @NonNull - public EnRouteStyle setLargeIconSubText(@Nullable CharSequence largeIconSubText) { - mLargeIconSubText = stripStyling(largeIconSubText); + public @NonNull List<Step> getProgressSteps() { + return mProgressSteps; + } + + /** + * Replaces all the progress steps. + * + * Steps are designated points within a progressbar to visualize + * distinct stages or milestones. + * For example, you might use steps to mark stops in a multi-stop + * navigation journey, where each step represents a destination. + * @see Step + */ + public @NonNull ProgressStyle setProgressSteps(@NonNull List<Step> steps) { + mProgressSteps = new ArrayList<>(steps); return this; } - /** + /** + * Adds another step. + * + * Steps are designated points within a progressbar to visualize + * distinct stages or milestones. + * For example, you might use steps to mark stops in a multi-stop + * navigation journey, where each step represents a destination. + * + * Steps can be added in any order, as their + * position within the progress bar is determined by their individual + * {@link Step#getPosition()}. + * @see Step + */ + public @NonNull ProgressStyle addProgressStep(@NonNull Step step) { + if (mProgressSteps == null) { + mProgressSteps = new ArrayList<>(); + } + mProgressSteps.add(step); + + return this; + } + + /** + * Gets the progress value of the progress bar. + * @see #setProgress + */ + public int getProgress() { + return mProgress; + } + + /** + * Specifies the progress (in the same units as {@link Segment#getLength()}) + * of the tracker along the length of the bar. + * + * The max progress value is the sum of all Segment lengths. + * The default value is 0. + */ + public @NonNull ProgressStyle setProgress(int progress) { + mProgress = progress; + return this; + } + + /** + * Gets the sum of the lengths of all Segments in the style, which + * defines the maximum progress. Defaults to 100 when segments are omitted. + */ + public int getProgressMax() { + final List<Segment> progressSegment = mProgressSegments; + if (progressSegment == null || progressSegment.isEmpty()) { + return DEFAULT_PROGRESS_MAX; + } else { + int progressMax = 0; + int validSegmentCount = 0; + for (int i = 0; i < progressSegment.size() + && validSegmentCount < MAX_PROGRESS_SEGMENT_LIMIT; i++) { + int segmentLength = progressSegment.get(i).getLength(); + if (segmentLength > 0) { + try { + progressMax = Math.addExact(progressMax, segmentLength); + validSegmentCount++; + } catch (ArithmeticException e) { + Log.e(TAG, + "Notification.ProgressStyle segment total overflowed.", e); + return DEFAULT_PROGRESS_MAX; + } + } + } + + if (validSegmentCount == 0) { + return DEFAULT_PROGRESS_MAX; + } + + return progressMax; + } + + } + + /** + * Get indeterminate value of the progress bar. + * @see #setProgressIndeterminate + */ + public boolean isProgressIndeterminate() { + return mIndeterminate; + } + + /** + * Used to indicate an initialization state without a known progress amount. + * When specified, the following fields are ignored: + * @see #setProgress + * @see #setProgressSegments + * @see #setProgressSteps + * @see #setProgressTrackerIcon + * @see #setStyledByProgress + * + * If the app provides exactly one Segment, that segment's color will be + * used to style the indeterminate bar. + */ + public @NonNull ProgressStyle setProgressIndeterminate(boolean indeterminate) { + mIndeterminate = indeterminate; + return this; + } + + /** + * Gets whether the progress bar's style is based on its progress. + * @see #setStyledByProgress + */ + public boolean isStyledByProgress() { + return mIsStyledByProgress; + } + + /** + * Indicates whether the segments and steps will be styled differently + * based on whether they are behind or ahead of the current progress. + * When true, segments appearing ahead of the current progress will be given a + * slightly different appearance to indicate that it is part of the progress bar + * that is not "filled". + * When false, all segments will be given the filled appearance, and it will be + * the app's responsibility to use #setProgressTrackerIcon or segment colors + * to make the current progress clear to the user. + * the default value is true. + */ + public @NonNull ProgressStyle setStyledByProgress(boolean enabled) { + mIsStyledByProgress = enabled; + return this; + } + + + /** + * Gets the progress tracker icon for the progress bar. + * @see #setProgressTrackerIcon + */ + public @Nullable Icon getProgressTrackerIcon() { + return mTrackerIcon; + } + + /** + * An optional icon that can appear as an overlay on the bar at the point of + * current progress. + * Aspect ratio may be anywhere from 2:1 to 1:2; content outside that + * aspect ratio range will be cropped. + * This icon will be mirrored in RTL. + */ + public @NonNull ProgressStyle setProgressTrackerIcon(@Nullable Icon trackerIcon) { + mTrackerIcon = trackerIcon; + return this; + } + + /** + * Gets the progress bar start icon. + * @see #setProgressStartIcon + */ + public @Nullable Icon getProgressStartIcon() { + return mStartIcon; + } + + /** + * An optional square icon that appears at the start of the progress bar. + * This icon will be cropped to its central square. + * This icon will NOT be mirrored in RTL layouts. + */ + public @NonNull ProgressStyle setProgressStartIcon(@Nullable Icon startIcon) { + mStartIcon = startIcon; + return this; + } + + /** + * Gets the progress bar end icon. + * @see #setProgressEndIcon(Icon) + */ + public @Nullable Icon getProgressEndIcon() { + return mEndIcon; + } + + /** + * An optional square icon that appears at the end of the progress bar. + * This icon will be cropped to its central square. + * This icon will NOT be mirrored in RTL layouts. + */ + public @NonNull ProgressStyle setProgressEndIcon(@Nullable Icon endIcon) { + mEndIcon = endIcon; + return this; + } + + /** * @hide */ @Override - public boolean areNotificationsVisiblyDifferent(Style other) { - if (other == null || getClass() != other.getClass()) { - return true; + public void purgeResources() { + super.purgeResources(); + if (mTrackerIcon != null) { + mTrackerIcon.convertToAshmem(); + } + if (mStartIcon != null) { + mStartIcon.convertToAshmem(); } + if (mEndIcon != null) { + mEndIcon.convertToAshmem(); + } + } + + /** + * @hide + */ + @Override + public void reduceImageSizes(Context context) { + super.reduceImageSizes(context); + + final Resources resources = context.getResources(); - final EnRouteStyle enRouteStyle = (EnRouteStyle) other; - return !Objects.equals(mOverlayIcon, enRouteStyle.mOverlayIcon) - || !Objects.equals(mLargeIconSubText, enRouteStyle.mLargeIconSubText); + int progressIconSize = + resources.getDimensionPixelSize(R.dimen.notification_progress_icon_size); + if (mStartIcon != null) { + mStartIcon.scaleDownIfNecessary(progressIconSize, progressIconSize); + } + if (mEndIcon != null) { + mEndIcon.scaleDownIfNecessary(progressIconSize, progressIconSize); + } + if (mTrackerIcon != null) { + int progressTrackerWidth = resources.getDimensionPixelSize( + R.dimen.notification_progress_tracker_width); + int progressTrackerHeight = resources.getDimensionPixelSize( + R.dimen.notification_progress_tracker_height); + mTrackerIcon.scaleDownIfNecessary(progressTrackerWidth, progressTrackerHeight); + } } /** @@ -11177,8 +11554,33 @@ public class Notification implements Parcelable @Override public void addExtras(Bundle extras) { super.addExtras(extras); - extras.putParcelable(EXTRA_ENROUTE_OVERLAY_ICON, mOverlayIcon); - extras.putCharSequence(EXTRA_ENROUTE_LARGE_ICON_SUBTEXT, mLargeIconSubText); + extras.putParcelableArrayList(EXTRA_PROGRESS_SEGMENTS, + getProgressSegmentsAsBundleList(mProgressSegments)); + extras.putParcelableArrayList(EXTRA_PROGRESS_STEPS, + getProgressStepsAsBundleList(mProgressSteps)); + + extras.putInt(EXTRA_PROGRESS, mProgress); + extras.putBoolean(EXTRA_PROGRESS_INDETERMINATE, mIndeterminate); + extras.putInt(EXTRA_PROGRESS_MAX, getProgressMax()); + extras.putBoolean(EXTRA_STYLED_BY_PROGRESS, mIsStyledByProgress); + + if (mTrackerIcon != null) { + extras.putParcelable(EXTRA_PROGRESS_TRACKER_ICON, mTrackerIcon); + } else { + extras.remove(EXTRA_PROGRESS_TRACKER_ICON); + } + + if (mStartIcon != null) { + extras.putParcelable(EXTRA_PROGRESS_START_ICON, mStartIcon); + } else { + extras.remove(EXTRA_PROGRESS_START_ICON); + } + + if (mEndIcon != null) { + extras.putParcelable(EXTRA_PROGRESS_END_ICON, mEndIcon); + } else { + extras.remove(EXTRA_PROGRESS_END_ICON); + } } /** @@ -11187,35 +11589,285 @@ public class Notification implements Parcelable @Override protected void restoreFromExtras(Bundle extras) { super.restoreFromExtras(extras); - mOverlayIcon = extras.getParcelable(EXTRA_ENROUTE_OVERLAY_ICON, Icon.class); - mLargeIconSubText = extras.getCharSequence(EXTRA_ENROUTE_LARGE_ICON_SUBTEXT); + mProgressSegments = getProgressSegmentsFromBundleList( + extras.getParcelableArrayList(EXTRA_PROGRESS_SEGMENTS, Bundle.class)); + mProgress = extras.getInt(EXTRA_PROGRESS, 0); + mIndeterminate = extras.getBoolean(EXTRA_PROGRESS_INDETERMINATE, false); + mIsStyledByProgress = extras.getBoolean(EXTRA_STYLED_BY_PROGRESS, true); + mTrackerIcon = extras.getParcelable(EXTRA_PROGRESS_TRACKER_ICON, Icon.class); + mStartIcon = extras.getParcelable(EXTRA_PROGRESS_START_ICON, Icon.class); + mEndIcon = extras.getParcelable(EXTRA_PROGRESS_END_ICON, Icon.class); + mProgressSteps = getProgressStepsFromBundleList( + extras.getParcelableArrayList(EXTRA_PROGRESS_STEPS, Bundle.class)); } /** * @hide */ @Override - public void purgeResources() { - super.purgeResources(); - if (mOverlayIcon != null) { - mOverlayIcon.convertToAshmem(); + public boolean displayCustomViewInline() { + // This is a lie; True is returned for progress notifications to make sure + // that the custom view is not used instead of the template, but it will not + // actually be included. + return true; + } + + private static @NonNull ArrayList<Bundle> getProgressSegmentsAsBundleList( + @Nullable List<Segment> progressSegments) { + final ArrayList<Bundle> segments = new ArrayList<>(); + if (progressSegments != null && !progressSegments.isEmpty()) { + for (int i = 0; i < progressSegments.size(); i++) { + final Segment segment = progressSegments.get(i); + if (segment.getLength() <= 0) { + continue; + } + + final Bundle bundle = new Bundle(); + bundle.putInt(KEY_SEGMENT_LENGTH, segment.getLength()); + bundle.putInt(KEY_ELEMENT_STABLE_ID, segment.getStableId()); + bundle.putInt(KEY_ELEMENT_COLOR, segment.getColor()); + + segments.add(bundle); + } + } + + return segments; + } + + private static @NonNull List<Segment> getProgressSegmentsFromBundleList( + @Nullable List<Bundle> segmentBundleList) { + final ArrayList<Segment> segments = new ArrayList<>(); + if (segmentBundleList != null && !segmentBundleList.isEmpty()) { + for (int i = 0; i < segmentBundleList.size(); i++) { + final Bundle segmentBundle = segmentBundleList.get(i); + final int length = segmentBundle.getInt(KEY_SEGMENT_LENGTH); + if (length <= 0) { + continue; + } + + final int stableId = segmentBundle.getInt(KEY_ELEMENT_STABLE_ID); + final int color = segmentBundle.getInt(KEY_ELEMENT_COLOR, + Notification.COLOR_DEFAULT); + final Segment segment = new Segment(length) + .setStableId(stableId).setColor(color); + + segments.add(segment); + } + } + + return segments; + } + + private static @NonNull ArrayList<Bundle> getProgressStepsAsBundleList( + @Nullable List<Step> progressSteps) { + final ArrayList<Bundle> steps = new ArrayList<>(); + if (progressSteps != null && !progressSteps.isEmpty()) { + for (int i = 0; i < progressSteps.size(); i++) { + final Step step = progressSteps.get(i); + if (step.getPosition() < 0) { + continue; + } + + final Bundle bundle = new Bundle(); + bundle.putInt(KEY_STEP_POSITION, step.getPosition()); + bundle.putInt(KEY_ELEMENT_STABLE_ID, step.getStableId()); + bundle.putInt(KEY_ELEMENT_COLOR, step.getColor()); + + steps.add(bundle); + } } + + return steps; + } + + private static @NonNull List<Step> getProgressStepsFromBundleList( + @Nullable List<Bundle> stepBundleList) { + final ArrayList<Step> steps = new ArrayList<>(); + + if (stepBundleList != null && !stepBundleList.isEmpty()) { + for (int i = 0; i < stepBundleList.size(); i++) { + final Bundle segmentBundle = stepBundleList.get(i); + final int position = segmentBundle.getInt(KEY_STEP_POSITION); + if (position < 0) { + continue; + } + final int stableId = segmentBundle.getInt(KEY_ELEMENT_STABLE_ID); + final int color = segmentBundle.getInt(KEY_ELEMENT_COLOR, + Notification.COLOR_DEFAULT); + final Step step = new Step(position).setStableId(stableId).setColor(color); + steps.add(step); + } + } + + return steps; } /** - * @hide + * A segment of the progress bar, which defines its length and color. + * Segments allow for creating progress bars with multiple colors or sections + * to represent different stages or categories of progress. + * For example, Traffic conditions along a navigation journey. */ - @Override - public void reduceImageSizes(Context context) { - super.reduceImageSizes(context); - if (mOverlayIcon != null) { - final Resources resources = context.getResources(); - final boolean isLowRam = ActivityManager.isLowRamDeviceStatic(); + public static final class Segment { + private int mLength; + private int mStableId = 0; + @ColorInt + private int mColor = Notification.COLOR_DEFAULT; - int rightIconSize = resources.getDimensionPixelSize(isLowRam - ? R.dimen.notification_right_icon_size_low_ram - : R.dimen.notification_right_icon_size); - mOverlayIcon.scaleDownIfNecessary(rightIconSize, rightIconSize); + /** + * Create a segment with a non-zero length. + * @param length + * See {@link #getLength} + */ + public Segment(int length) { + mLength = length; + } + + /** + * The length of this Segment within the progress bar. + * This value has no units, it is just relative to the length of other segments, + * and the value provided to {@link ProgressStyle#setProgress}. + */ + public int getLength() { + return mLength; + } + + /** + * Gets the stable id of this Segment. + * + * @see #setStableId + */ + public int getStableId() { + return mStableId; + } + + /** + * Optional ID used to uniquely identify the element across updates. + */ + public @NonNull Segment setStableId(int stableId) { + mStableId = stableId; + return this; + } + + /** + * Returns the color of this Segment. + * + * @see #setColor + */ + @ColorInt + public int getColor() { + return mColor; + } + + /** + * Optional color of this Segment + */ + public @NonNull Segment setColor(@ColorInt int color) { + mColor = color; + return this; + } + + /** + * Needed for {@link Notification.Style#areNotificationsVisiblyDifferent} + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Segment segment = (Segment) o; + return mLength == segment.mLength && mStableId == segment.mStableId + && mColor == segment.mColor; + } + + @Override + public int hashCode() { + return Objects.hash(mLength, mStableId, mColor); + } + } + + /** + * A step within the progress bar, defining its position and color. + * Steps are designated points within a progressbar to visualize + * distinct stages or milestones. + * For example, you might use steps to mark stops in a multi-stop + * navigation journey, where each step represents a destination. + */ + public static final class Step { + + private int mPosition; + private int mStableId; + @ColorInt + private int mColor = Notification.COLOR_DEFAULT; + + /** + * Create a step element. + * The position of this step on the progress bar + * relative to {@link ProgressStyle#getProgressMax} + * @param position + * See {@link #getPosition} + */ + public Step(int position) { + mPosition = position; + } + + /** + * Gets the position of this Step. + * The position of this step on the progress bar + * relative to {@link ProgressStyle#getProgressMax}. + */ + public int getPosition() { + return mPosition; + } + + + /** + * Optional ID used to uniqurely identify the element across updates. + */ + public int getStableId() { + return mStableId; + } + + /** + * Optional ID used to uniqurely identify the element across updates. + */ + public @NonNull Step setStableId(int stableId) { + mStableId = stableId; + return this; + } + + /** + * Returns the color of this Segment. + * + * @see #setColor + */ + @ColorInt + public int getColor() { + return mColor; + } + + /** + * Optional color of this Segment + */ + public @NonNull Step setColor(@ColorInt int color) { + mColor = color; + return this; + } + + /** + * Needed for {@link Notification.Style#areNotificationsVisiblyDifferent} + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Step step = (Step) o; + return mPosition == step.mPosition && mStableId == step.mStableId + && mColor == step.mColor; + } + + @Override + public int hashCode() { + return Objects.hash(mPosition, mStableId, mColor); } } } diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index 0e761fce9346..c17da249f322 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -54,198 +54,8 @@ import java.util.concurrent.atomic.AtomicLong; * LRU cache that's invalidated when an opaque value in a property changes. Self-synchronizing, * but doesn't hold a lock across data fetches on query misses. * - * The intended use case is caching frequently-read, seldom-changed information normally - * retrieved across interprocess communication. Imagine that you've written a user birthday - * information daemon called "birthdayd" that exposes an {@code IUserBirthdayService} interface - * over binder. That binder interface looks something like this: - * - * <pre> - * parcelable Birthday { - * int month; - * int day; - * } - * interface IUserBirthdayService { - * Birthday getUserBirthday(int userId); - * } - * </pre> - * - * Suppose the service implementation itself looks like this... - * - * <pre> - * public class UserBirthdayServiceImpl implements IUserBirthdayService { - * private final HashMap<Integer, Birthday%> mUidToBirthday; - * {@literal @}Override - * public synchronized Birthday getUserBirthday(int userId) { - * return mUidToBirthday.get(userId); - * } - * private synchronized void updateBirthdays(Map<Integer, Birthday%> uidToBirthday) { - * mUidToBirthday.clear(); - * mUidToBirthday.putAll(uidToBirthday); - * } - * } - * </pre> - * - * ... and we have a client in frameworks (loaded into every app process) that looks - * like this: - * - * <pre> - * public class ActivityThread { - * ... - * public Birthday getUserBirthday(int userId) { - * return GetService("birthdayd").getUserBirthday(userId); - * } - * ... - * } - * </pre> - * - * With this code, every time an app calls {@code getUserBirthday(uid)}, we make a binder call - * to the birthdayd process and consult its database of birthdays. If we query user birthdays - * frequently, we do a lot of work that we don't have to do, since user birthdays - * change infrequently. - * - * PropertyInvalidatedCache is part of a pattern for optimizing this kind of - * information-querying code. Using {@code PropertyInvalidatedCache}, you'd write the client - * this way: - * - * <pre> - * public class ActivityThread { - * ... - * private final PropertyInvalidatedCache.QueryHandler<Integer, Birthday> mBirthdayQuery = - * new PropertyInvalidatedCache.QueryHandler<Integer, Birthday>() { - * {@literal @}Override - * public Birthday apply(Integer) { - * return GetService("birthdayd").getUserBirthday(userId); - * } - * }; - * private static final int BDAY_CACHE_MAX = 8; // Maximum birthdays to cache - * private static final String BDAY_CACHE_KEY = "cache_key.birthdayd"; - * private final PropertyInvalidatedCache<Integer, Birthday%> mBirthdayCache = new - * PropertyInvalidatedCache<Integer, Birthday%>( - * BDAY_CACHE_MAX, MODULE_SYSTEM, "getUserBirthday", mBirthdayQuery); - * - * public void disableUserBirthdayCache() { - * mBirthdayCache.disableForCurrentProcess(); - * } - * public void invalidateUserBirthdayCache() { - * mBirthdayCache.invalidateCache(); - * } - * public Birthday getUserBirthday(int userId) { - * return mBirthdayCache.query(userId); - * } - * ... - * } - * </pre> - * - * With this cache, clients perform a binder call to birthdayd if asking for a user's birthday - * for the first time; on subsequent queries, we return the already-known Birthday object. - * - * The second parameter to the IpcDataCache constructor is a string that identifies the "module" - * that owns the cache. There are some well-known modules (such as {@code MODULE_SYSTEM} but any - * string is permitted. The third parameters is the name of the API being cached; this, too, can - * any value. The fourth is the name of the cache. The cache is usually named after th API. - * Some things you must know about the three strings: - * <list> - * <ul> The system property that controls the cache is named {@code cache_key.<module>.<api>}. - * Usually, the SELinux rules permit a process to write a system property (and therefore - * invalidate a cache) based on the wildcard {@code cache_key.<module>.*}. This means that - * although the cache can be constructed with any module string, whatever string is chosen must be - * consistent with the SELinux configuration. - * <ul> The API name can be any string of alphanumeric characters. All caches with the same API - * are invalidated at the same time. If a server supports several caches and all are invalidated - * in common, then it is most efficient to assign the same API string to every cache. - * <ul> The cache name can be any string. In debug output, the name is used to distiguish between - * caches with the same API name. The cache name is also used when disabling caches in the - * current process. So, invalidation is based on the module+api but disabling (which is generally - * a once-per-process operation) is based on the cache name. - * </list> - * - * User birthdays do occasionally change, so we have to modify the server to invalidate this - * cache when necessary. That invalidation code looks like this: - * - * <pre> - * public class UserBirthdayServiceImpl { - * ... - * public UserBirthdayServiceImpl() { - * ... - * ActivityThread.currentActivityThread().disableUserBirthdayCache(); - * ActivityThread.currentActivityThread().invalidateUserBirthdayCache(); - * } - * - * private synchronized void updateBirthdays(Map<Integer, Birthday%> uidToBirthday) { - * mUidToBirthday.clear(); - * mUidToBirthday.putAll(uidToBirthday); - * ActivityThread.currentActivityThread().invalidateUserBirthdayCache(); - * } - * ... - * } - * </pre> - * - * The call to {@code PropertyInvalidatedCache.invalidateCache()} guarantees that all clients - * will re-fetch birthdays from binder during consequent calls to - * {@code ActivityThread.getUserBirthday()}. Because the invalidate call happens with the lock - * held, we maintain consistency between different client views of the birthday state. The use - * of PropertyInvalidatedCache in this idiomatic way introduces no new race conditions. - * - * PropertyInvalidatedCache has a few other features for doing things like incremental - * enhancement of cached values and invalidation of multiple caches (that all share the same - * property key) at once. - * - * {@code BDAY_CACHE_KEY} is the name of a property that we set to an opaque unique value each - * time we update the cache. SELinux configuration must allow everyone to read this property - * and it must allow any process that needs to invalidate the cache (here, birthdayd) to write - * the property. (These properties conventionally begin with the "cache_key." prefix.) - * - * The {@code UserBirthdayServiceImpl} constructor calls {@code disableUserBirthdayCache()} so - * that calls to {@code getUserBirthday} from inside birthdayd don't go through the cache. In - * this local case, there's no IPC, so use of the cache is (depending on exact - * circumstance) unnecessary. - * - * There may be queries for which it is more efficient to bypass the cache than to cache - * the result. This would be true, for example, if some queries would require frequent - * cache invalidation while other queries require infrequent invalidation. To expand on - * the birthday example, suppose that there is a userId that signifies "the next - * birthday". When passed this userId, the server returns the next birthday among all - * users - this value changes as time advances. The userId value can be cached, but the - * cache must be invalidated whenever a birthday occurs, and this invalidates all - * birthdays. If there is a large number of users, invalidation will happen so often that - * the cache provides no value. - * - * The class provides a bypass mechanism to handle this situation. - * <pre> - * public class ActivityThread { - * ... - * private final IpcDataCache.QueryHandler<Integer, Birthday> mBirthdayQuery = - * new IpcDataCache.QueryHandler<Integer, Birthday>() { - * {@literal @}Override - * public Birthday apply(Integer) { - * return GetService("birthdayd").getUserBirthday(userId); - * } - * {@literal @}Override - * public boolean shouldBypassQuery(Integer userId) { - * return userId == NEXT_BIRTHDAY; - * } - * }; - * ... - * } - * </pre> - * - * If the {@code shouldBypassQuery()} method returns true then the cache is not used for that - * particular query. The {@code shouldBypassQuery()} method is not abstract and the default - * implementation returns false. - * - * For security, there is a allowlist of processes that are allowed to invalidate a cache. - * The allowlist includes normal runtime processes but does not include test processes. - * Test processes must call {@code PropertyInvalidatedCache.disableForTestMode()} to disable - * all cache activity in that process. - * - * Caching can be disabled completely by initializing {@code sEnabled} to false and rebuilding. - * - * To test a binder cache, create one or more tests that exercise the binder method. This - * should be done twice: once with production code and once with a special image that sets - * {@code DEBUG} and {@code VERIFY} true. In the latter case, verify that no cache - * inconsistencies are reported. If a cache inconsistency is reported, however, it might be a - * false positive. This happens if the server side data can be read and written non-atomically - * with respect to cache invalidation. + * This interface is deprecated. New clients should use {@link IpcDataCache} instead. Internally, + * that class uses {@link PropertyInvalidatedCache} , but that design may change in the future. * * @param <Query> The class used to index cache entries: must be hashable and comparable * @param <Result> The class holding cache entries; use a boxed primitive if possible diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index c13a58f52ac8..ea4148c8ffa1 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -230,6 +230,7 @@ import android.print.IPrintManager; import android.print.PrintManager; import android.provider.E2eeContactKeysManager; import android.provider.ProviderFrameworkInitializer; +import android.ranging.RangingFrameworkInitializer; import android.safetycenter.SafetyCenterFrameworkInitializer; import android.scheduling.SchedulingFrameworkInitializer; import android.security.FileIntegrityManager; @@ -1825,6 +1826,12 @@ public final class SystemServiceRegistry { if (android.webkit.Flags.updateServiceIpcWrapper()) { WebViewBootstrapFrameworkInitializer.registerServiceWrappers(); } + // This is guarded by aconfig flag "com.android.ranging.flags.ranging_stack_enabled" + // when the build flag RELEASE_RANGING_STACK is enabled. When disabled, this calls the + // mock RangingFrameworkInitializer#registerServiceWrappers which is no-op. As the + // aconfig lib for ranging module is built only if RELEASE_RANGING_STACK is enabled, + // flagcannot be added here. + RangingFrameworkInitializer.registerServiceWrappers(); } finally { // If any of the above code throws, we're in a pretty bad shape and the process // will likely crash, but we'll reset it just in case there's an exception handler... diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index daa15f05d942..9be928f7efd0 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -213,19 +213,17 @@ import java.util.function.Consumer; * <a href="{@docRoot}guide/topics/admin/device-admin.html">Device Administration</a> * developer guide. * - * <p id="devicepolicycontroller">Through <a href="#managed_provisioning">Managed Provisioning</a>, - * Device Administrator apps can also be recognised as <b> - Device Policy Controllers</b>. Device Policy Controllers can be one of + * <p id="devicepolicycontroller">Device Administrator apps can also be recognised as <b> + * Device Policy Controllers</b>. Device Policy Controllers can be one of * two types: * <ul> * <li>A <i id="deviceowner">Device Owner</i>, which only ever exists on the - * {@link UserManager#isSystemUser System User} or {@link UserManager#isMainUser Main User}, is + * {@link UserManager#isSystemUser System User} or Main User, is * the most powerful type of Device Policy Controller and can affect policy across the device. * <li>A <i id="profileowner">Profile Owner<i>, which can exist on any user, can * affect policy on the user it is on, and when it is running on * {@link UserManager#isProfile a profile} has - * <a href="#profile-on-parent">limited</a> ability to affect policy on its - * {@link UserManager#getProfileParent parent}. + * <a href="#profile-on-parent">limited</a> ability to affect policy on its parent. * </ul> * * <p>Additional capabilities can be provided to Device Policy Controllers in @@ -233,7 +231,7 @@ import java.util.function.Consumer; * <ul> * <li>A Profile Owner on an <a href="#organization-owned">organization owned</a> device has access * to additional abilities, both <a href="#profile-on-parent-organization-owned">affecting policy on the profile's</a> - * {@link UserManager#getProfileParent parent} and also the profile itself. + * parent and also the profile itself. * <li>A Profile Owner running on the {@link UserManager#isSystemUser System User} has access to * additional capabilities which affect the {@link UserManager#isSystemUser System User} and * also the whole device. @@ -245,13 +243,12 @@ import java.util.function.Consumer; * Controller</a>. * * <p><a href="#permissions">Permissions</a> are generally only given to apps - * fulfilling particular key roles on the device (such as managing {@link DeviceLockManager -device locks}). + * fulfilling particular key roles on the device (such as managing + * {@link android.devicelock.DeviceLockManager device locks}). * * <p id="roleholder"><b>Device Policy Management Role Holder</b> - * <p>One app on the device fulfills the {@link RoleManager#ROLE_DEVICE_POLICY_MANAGEMENT Device -Policy Management Role} and is trusted with managing the overall state of - * Device Policy. This has access to much more powerful methods than + * <p>One app on the device fulfills the Device Policy Management Role and is trusted with managing + * the overall state of Device Policy. This has access to much more powerful methods than * <a href="#managingapps">managing apps</a>. * * <p id="querying"><b>Querying Device Policy</b> @@ -273,7 +270,7 @@ Policy Management Role} and is trusted with managing the overall state of * * <p id="managed_profile">A <b>Managed Profile</b> enables data separation. For example to use * a device both for personal and corporate usage. The managed profile and its - * {@link UserManager#getProfileParent parent} share a launcher. + * parent share a launcher. * * <p id="affiliated"><b>Affiliation</b> * <p>Using the {@link #setAffiliationIds} method, a @@ -6643,7 +6640,7 @@ public class DevicePolicyManager { * @param flags May be 0 or {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY}. * @throws SecurityException if the calling application does not own an active administrator * that uses {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK} and the does not hold - * the {@link android.Manifest.permission#LOCK_DEVICE} permission, or + * the LOCK_DEVICE permission, or * the {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY} flag is passed by an * application that is not a profile owner of a managed profile. * @throws IllegalArgumentException if the {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY} flag is diff --git a/core/java/android/app/admin/PolicySizeVerifier.java b/core/java/android/app/admin/PolicySizeVerifier.java index 7f8e50ec4420..1e03e1fd206d 100644 --- a/core/java/android/app/admin/PolicySizeVerifier.java +++ b/core/java/android/app/admin/PolicySizeVerifier.java @@ -22,7 +22,9 @@ import android.os.Parcelable; import android.os.PersistableBundle; import com.android.internal.util.Preconditions; +import com.android.modules.utils.ModifiedUtf8; +import java.io.UTFDataFormatException; import java.util.ArrayDeque; import java.util.Queue; @@ -33,8 +35,6 @@ import java.util.Queue; */ public class PolicySizeVerifier { - // Binary XML serializer doesn't support longer strings - public static final int MAX_POLICY_STRING_LENGTH = 65535; // FrameworkParsingPackageUtils#MAX_FILE_NAME_SIZE, Android packages are used in dir names. public static final int MAX_PACKAGE_NAME_LENGTH = 223; @@ -47,8 +47,11 @@ public class PolicySizeVerifier { * Throw if string argument is too long to be serialized. */ public static void enforceMaxStringLength(String str, String argName) { - Preconditions.checkArgument( - str.length() <= MAX_POLICY_STRING_LENGTH, argName + " loo long"); + try { + long len = ModifiedUtf8.countBytes(str, /* throw error if too long */ true); + } catch (UTFDataFormatException e) { + throw new IllegalArgumentException(argName + " too long"); + } } /** diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java index 4682f3d30e1e..6797a51e59f4 100644 --- a/core/java/android/app/appfunctions/AppFunctionManager.java +++ b/core/java/android/app/appfunctions/AppFunctionManager.java @@ -22,13 +22,21 @@ import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANA import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.annotation.UserHandleAware; +import android.app.appsearch.AppSearchManager; import android.content.Context; +import android.os.CancellationSignal; +import android.os.ICancellationSignal; +import android.os.OutcomeReceiver; +import android.os.ParcelableException; import android.os.RemoteException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -43,10 +51,44 @@ import java.util.function.Consumer; @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER) @SystemService(Context.APP_FUNCTION_SERVICE) public final class AppFunctionManager { + + /** + * The default state of the app function. Call {@link #setAppFunctionEnabled} with this to reset + * enabled state to the default value. + */ + public static final int APP_FUNCTION_STATE_DEFAULT = 0; + + /** + * The app function is enabled. To enable an app function, call {@link #setAppFunctionEnabled} + * with this value. + */ + public static final int APP_FUNCTION_STATE_ENABLED = 1; + + /** + * The app function is disabled. To disable an app function, call {@link #setAppFunctionEnabled} + * with this value. + */ + public static final int APP_FUNCTION_STATE_DISABLED = 2; + private final IAppFunctionManager mService; private final Context mContext; /** + * The enabled state of the app function. + * + * @hide + */ + @IntDef( + prefix = {"APP_FUNCTION_STATE_"}, + value = { + APP_FUNCTION_STATE_DEFAULT, + APP_FUNCTION_STATE_ENABLED, + APP_FUNCTION_STATE_DISABLED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface EnabledState {} + + /** * Creates an instance. * * @param service An interface to the backing service. @@ -73,7 +115,43 @@ public final class AppFunctionManager { * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code * android.permission.EXECUTE_APP_FUNCTIONS}, the execution result will contain {@code * ExecuteAppFunctionResponse.RESULT_DENIED}. + * @deprecated Use {@link #executeAppFunction(ExecuteAppFunctionRequest, Executor, + * CancellationSignal, Consumer)} instead. This method will be removed once usage references + * are updated. */ + @RequiresPermission( + anyOf = { + Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, + Manifest.permission.EXECUTE_APP_FUNCTIONS + }, + conditional = true) + @UserHandleAware + @Deprecated + public void executeAppFunction( + @NonNull ExecuteAppFunctionRequest request, + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<ExecuteAppFunctionResponse> callback) { + executeAppFunction(request, executor, new CancellationSignal(), callback); + } + + /** + * Executes the app function. + * + * <p>Note: Applications can execute functions they define. To execute functions defined in + * another component, apps would need to have {@code + * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code + * android.permission.EXECUTE_APP_FUNCTIONS}. + * + * @param request the request to execute the app function + * @param executor the executor to run the callback + * @param cancellationSignal the cancellation signal to cancel the execution. + * @param callback the callback to receive the function execution result. if the calling app + * does not own the app function or does not have {@code + * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code + * android.permission.EXECUTE_APP_FUNCTIONS}, the execution result will contain {@code + * ExecuteAppFunctionResponse.RESULT_DENIED}. + */ + // TODO(b/357551503): Document the behavior when the cancellation signal is issued. // TODO(b/360864791): Document that apps can opt-out from being executed by callers with // EXECUTE_APP_FUNCTIONS and how a caller knows whether a function is opted out. // TODO(b/357551503): Update documentation when get / set APIs are implemented that this will @@ -88,6 +166,7 @@ public final class AppFunctionManager { public void executeAppFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull @CallbackExecutor Executor executor, + @NonNull CancellationSignal cancellationSignal, @NonNull Consumer<ExecuteAppFunctionResponse> callback) { Objects.requireNonNull(request); Objects.requireNonNull(executor); @@ -96,27 +175,147 @@ public final class AppFunctionManager { ExecuteAppFunctionAidlRequest aidlRequest = new ExecuteAppFunctionAidlRequest( request, mContext.getUser(), mContext.getPackageName()); + + try { + ICancellationSignal cancellationTransport = + mService.executeAppFunction( + aidlRequest, + new IExecuteAppFunctionCallback.Stub() { + @Override + public void onResult(ExecuteAppFunctionResponse result) { + try { + executor.execute(() -> callback.accept(result)); + } catch (RuntimeException e) { + // Ideally shouldn't happen since errors are wrapped into + // the + // response, but we catch it here for additional safety. + callback.accept( + ExecuteAppFunctionResponse.newFailure( + getResultCode(e), + e.getMessage(), + /* extras= */ null)); + } + } + }); + if (cancellationTransport != null) { + cancellationSignal.setRemote(cancellationTransport); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * 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 + * 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: + * + * <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 targetPackage the package name of the app function's owner + * @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 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); + } + + /** + * Sets the enabled state of the app function owned by the calling package. + * + * <p>If 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 enable (unique within the + * calling package). In most cases, identifiers are automatically generated by the + * AppFunctions SDK + * @param newEnabledState the new state of the app function + * @param executor the executor to run the callback + * @param callback the callback to receive the result of the function enablement. The call was + * successful if no exception was thrown. + */ + @UserHandleAware + public void setAppFunctionEnabled( + @NonNull String functionIdentifier, + @EnabledState int newEnabledState, + @NonNull Executor executor, + @NonNull OutcomeReceiver<Void, Exception> callback) { + Objects.requireNonNull(functionIdentifier); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + CallbackWrapper callbackWrapper = new CallbackWrapper(executor, callback); try { - mService.executeAppFunction( - aidlRequest, - new IExecuteAppFunctionCallback.Stub() { - @Override - public void onResult(ExecuteAppFunctionResponse result) { - try { - executor.execute(() -> callback.accept(result)); - } catch (RuntimeException e) { - // Ideally shouldn't happen since errors are wrapped into the - // response, but we catch it here for additional safety. - callback.accept( - ExecuteAppFunctionResponse.newFailure( - getResultCode(e), - e.getMessage(), - /* extras= */ null)); - } - } - }); + mService.setAppFunctionEnabled( + mContext.getPackageName(), + functionIdentifier, + mContext.getUser(), + newEnabledState, + callbackWrapper); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } + + private static class CallbackWrapper extends IAppFunctionEnabledCallback.Stub { + + private final OutcomeReceiver<Void, Exception> mCallback; + private final Executor mExecutor; + + CallbackWrapper( + @NonNull Executor callbackExecutor, + @NonNull OutcomeReceiver<Void, Exception> callback) { + mCallback = callback; + mExecutor = callbackExecutor; + } + + @Override + public void onSuccess() { + mExecutor.execute(() -> mCallback.onResult(null)); + } + + @Override + public void onError(@NonNull ParcelableException exception) { + mExecutor.execute(() -> { + if (IllegalArgumentException.class.isAssignableFrom( + exception.getCause().getClass())) { + mCallback.onError((IllegalArgumentException) exception.getCause()); + } else if (SecurityException.class.isAssignableFrom( + exception.getCause().getClass())) { + mCallback.onError((SecurityException) exception.getCause()); + } else { + mCallback.onError(exception); + } + }); + } + } } diff --git a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java index d6f45e4c9f6a..fe2db49684fd 100644 --- a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java +++ b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java @@ -22,7 +22,7 @@ import static android.app.appfunctions.AppFunctionStaticMetadataHelper.APP_FUNCT import static android.app.appfunctions.AppFunctionStaticMetadataHelper.STATIC_PROPERTY_ENABLED_BY_DEFAULT; import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER; -import android.annotation.CallbackExecutor; +import android.Manifest; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.app.appsearch.AppSearchManager; @@ -33,6 +33,7 @@ import android.app.appsearch.SearchResult; import android.app.appsearch.SearchResults; import android.app.appsearch.SearchSpec; import android.os.OutcomeReceiver; +import android.text.TextUtils; import java.io.IOException; import java.util.List; @@ -50,73 +51,69 @@ public class AppFunctionManagerHelper { /** * Returns (through a callback) a boolean indicating whether the app function is enabled. * - * <p>This method can only check app functions that are owned by the caller owned by packages - * visible to the caller. + * 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: * * <ul> - * <li>{@link IllegalArgumentException}, if the function is not found - * <li>{@link SecurityException}, if the caller does not have permission to query the target - * package + * <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 targetPackage the package name of the app function's owner - * @param appSearchExecutor the executor to run the metadata search mechanism through AppSearch - * @param callbackExecutor the executor to run the callback - * @param callback the callback to receive the function enabled check result + * target package) and in most cases, these are automatically + * generated by the AppFunctions + * SDK + * @param targetPackage the package name of the app function's owner + * @param executor executor the executor to run the request + * @param callback the callback to receive the function enabled check result * @hide */ public static void isAppFunctionEnabled( @NonNull String functionIdentifier, @NonNull String targetPackage, @NonNull AppSearchManager appSearchManager, - @NonNull Executor appSearchExecutor, - @NonNull @CallbackExecutor Executor callbackExecutor, + @NonNull Executor executor, @NonNull OutcomeReceiver<Boolean, Exception> callback) { Objects.requireNonNull(functionIdentifier); Objects.requireNonNull(targetPackage); Objects.requireNonNull(appSearchManager); - Objects.requireNonNull(appSearchExecutor); - Objects.requireNonNull(callbackExecutor); + Objects.requireNonNull(executor); Objects.requireNonNull(callback); appSearchManager.createGlobalSearchSession( - appSearchExecutor, + executor, (searchSessionResult) -> { if (!searchSessionResult.isSuccess()) { - callbackExecutor.execute( - () -> - callback.onError( - failedResultToException(searchSessionResult))); + callback.onError(failedResultToException(searchSessionResult)); return; } try (GlobalSearchSession searchSession = searchSessionResult.getResultValue()) { SearchResults results = searchJoinedStaticWithRuntimeAppFunctions( - searchSession, targetPackage, functionIdentifier); + Objects.requireNonNull(searchSession), + targetPackage, + functionIdentifier); results.getNextPage( - appSearchExecutor, - listAppSearchResult -> - callbackExecutor.execute( - () -> { - if (listAppSearchResult.isSuccess()) { - callback.onResult( - getEnabledStateFromSearchResults( - Objects.requireNonNull( - listAppSearchResult + executor, + listAppSearchResult -> { + if (listAppSearchResult.isSuccess()) { + callback.onResult( + getEffectiveEnabledStateFromSearchResults( + Objects.requireNonNull( + listAppSearchResult .getResultValue()))); - } else { - callback.onError( - failedResultToException( - listAppSearchResult)); - } - })); + } else { + callback.onError( + failedResultToException(listAppSearchResult)); + } + }); + results.close(); } catch (Exception e) { - callbackExecutor.execute(() -> callback.onError(e)); + callback.onError(e); } }); } @@ -124,56 +121,58 @@ public class AppFunctionManagerHelper { /** * Searches joined app function static and runtime metadata using the function Id and the * package. - * - * @hide */ private static @NonNull SearchResults searchJoinedStaticWithRuntimeAppFunctions( @NonNull GlobalSearchSession session, @NonNull String targetPackage, @NonNull String functionIdentifier) { SearchSpec runtimeSearchSpec = - getAppFunctionRuntimeMetadataSearchSpecByFunctionId(targetPackage); + getAppFunctionRuntimeMetadataSearchSpecByPackageName(targetPackage); JoinSpec joinSpec = new JoinSpec.Builder(PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID) - .setNestedSearch(functionIdentifier, runtimeSearchSpec) + .setNestedSearch( + buildFilerRuntimeMetadataByFunctionIdQuery(functionIdentifier), + runtimeSearchSpec) .build(); SearchSpec joinedStaticWithRuntimeSearchSpec = new SearchSpec.Builder() - .setJoinSpec(joinSpec) .addFilterPackageNames(APP_FUNCTION_INDEXER_PACKAGE) .addFilterSchemas( AppFunctionStaticMetadataHelper.getStaticSchemaNameForPackage( targetPackage)) - .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) + .setJoinSpec(joinSpec) + .setVerbatimSearchEnabled(true) .build(); - return session.search(functionIdentifier, joinedStaticWithRuntimeSearchSpec); + return session.search( + buildFilerStaticMetadataByFunctionIdQuery(functionIdentifier), + joinedStaticWithRuntimeSearchSpec); } /** - * Finds whether the function is enabled or not from the search results returned by {@link - * #searchJoinedStaticWithRuntimeAppFunctions}. + * Returns whether the function is effectively enabled or not from the search results returned + * by {@link #searchJoinedStaticWithRuntimeAppFunctions}. * + * @param joinedStaticRuntimeResults search results joining AppFunctionStaticMetadata + * and AppFunctionRuntimeMetadata. * @throws IllegalArgumentException if the function is not found in the results - * @hide */ - private static boolean getEnabledStateFromSearchResults( + private static boolean getEffectiveEnabledStateFromSearchResults( @NonNull List<SearchResult> joinedStaticRuntimeResults) { if (joinedStaticRuntimeResults.isEmpty()) { - // Function not found. throw new IllegalArgumentException("App function not found."); } else { List<SearchResult> runtimeMetadataResults = joinedStaticRuntimeResults.getFirst().getJoinedResults(); - if (!runtimeMetadataResults.isEmpty()) { - Boolean result = - (Boolean) - runtimeMetadataResults - .getFirst() - .getGenericDocument() - .getProperty(PROPERTY_ENABLED); - if (result != null) { - return result; - } + if (runtimeMetadataResults.isEmpty()) { + throw new IllegalArgumentException("App function not found."); + } + boolean[] enabled = + runtimeMetadataResults + .getFirst() + .getGenericDocument() + .getPropertyBooleanArray(PROPERTY_ENABLED); + if (enabled != null && enabled.length != 0) { + return enabled[0]; } // Runtime metadata not found. Using the default value in the static metadata. return joinedStaticRuntimeResults @@ -186,36 +185,39 @@ public class AppFunctionManagerHelper { /** * Returns search spec that queries app function metadata for a specific package name by its * function identifier. - * - * @hide */ - public static @NonNull SearchSpec getAppFunctionRuntimeMetadataSearchSpecByFunctionId( + private static @NonNull SearchSpec getAppFunctionRuntimeMetadataSearchSpecByPackageName( @NonNull String targetPackage) { return new SearchSpec.Builder() .addFilterPackageNames(APP_FUNCTION_INDEXER_PACKAGE) .addFilterSchemas( AppFunctionRuntimeMetadata.getRuntimeSchemaNameForPackage(targetPackage)) - .addFilterProperties( - AppFunctionRuntimeMetadata.getRuntimeSchemaNameForPackage(targetPackage), - List.of(AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID)) - .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY) + .setVerbatimSearchEnabled(true) .build(); } - /** - * Converts a failed app search result codes into an exception. - * - * @hide - */ - public static @NonNull Exception failedResultToException( + private static String buildFilerRuntimeMetadataByFunctionIdQuery(String functionIdentifier) { + return TextUtils.formatSimple("%s:\"%s\"", + AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID, + functionIdentifier); + } + + private static String buildFilerStaticMetadataByFunctionIdQuery(String functionIdentifier) { + return TextUtils.formatSimple("%s:\"%s\"", + AppFunctionStaticMetadataHelper.PROPERTY_FUNCTION_ID, + functionIdentifier); + } + + /** Converts a failed app search result codes into an exception. */ + private static @NonNull Exception failedResultToException( @NonNull AppSearchResult appSearchResult) { return switch (appSearchResult.getResultCode()) { - case AppSearchResult.RESULT_INVALID_ARGUMENT -> - new IllegalArgumentException(appSearchResult.getErrorMessage()); - case AppSearchResult.RESULT_IO_ERROR -> - new IOException(appSearchResult.getErrorMessage()); - case AppSearchResult.RESULT_SECURITY_ERROR -> - new SecurityException(appSearchResult.getErrorMessage()); + case AppSearchResult.RESULT_INVALID_ARGUMENT -> new IllegalArgumentException( + appSearchResult.getErrorMessage()); + case AppSearchResult.RESULT_IO_ERROR -> new IOException( + appSearchResult.getErrorMessage()); + case AppSearchResult.RESULT_SECURITY_ERROR -> new SecurityException( + appSearchResult.getErrorMessage()); default -> new IllegalStateException(appSearchResult.getErrorMessage()); }; } diff --git a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java index 83b5aa05c383..8b7f326ee816 100644 --- a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java +++ b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java @@ -204,11 +204,17 @@ public class AppFunctionRuntimeMetadata extends GenericDocument { packageName, functionId)); } + public Builder(AppFunctionRuntimeMetadata original) { + this(original.getPackageName(), original.getFunctionId()); + setEnabled(original.getEnabled()); + } + /** * Sets an indicator specifying if the function is enabled or not. This would override the * default enabled state in the static metadata ({@link - * AppFunctionStaticMetadataHelper#STATIC_PROPERTY_ENABLED_BY_DEFAULT}). Sets this to - * null to clear the override. + * AppFunctionStaticMetadataHelper#STATIC_PROPERTY_ENABLED_BY_DEFAULT}). Sets this to null + * to clear the override. + * TODO(369683073) Replace the tristate Boolean with IntDef EnabledState. */ @NonNull public Builder setEnabled(@Nullable Boolean enabled) { diff --git a/core/java/android/app/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java index 0d981ea5a679..8e417737515e 100644 --- a/core/java/android/app/appfunctions/AppFunctionService.java +++ b/core/java/android/app/appfunctions/AppFunctionService.java @@ -29,7 +29,12 @@ import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.Binder; +import android.os.Bundle; import android.os.IBinder; +import android.os.ICancellationSignal; +import android.os.CancellationSignal; +import android.os.RemoteCallback; +import android.os.RemoteException; import java.util.function.Consumer; @@ -74,6 +79,7 @@ public abstract class AppFunctionService extends Service { */ void perform( @NonNull ExecuteAppFunctionRequest request, + @NonNull CancellationSignal cancellationSignal, @NonNull Consumer<ExecuteAppFunctionResponse> callback); } @@ -85,6 +91,7 @@ public abstract class AppFunctionService extends Service { @Override public void executeAppFunction( @NonNull ExecuteAppFunctionRequest request, + @NonNull ICancellationCallback cancellationCallback, @NonNull IExecuteAppFunctionCallback callback) { if (context.checkCallingPermission(BIND_APP_FUNCTION_SERVICE) == PERMISSION_DENIED) { @@ -93,7 +100,10 @@ public abstract class AppFunctionService extends Service { SafeOneTimeExecuteAppFunctionCallback safeCallback = new SafeOneTimeExecuteAppFunctionCallback(callback); try { - onExecuteFunction.perform(request, safeCallback::onResult); + onExecuteFunction.perform( + request, + buildCancellationSignal(cancellationCallback), + safeCallback::onResult); } catch (Exception ex) { // Apps should handle exceptions. But if they don't, report the error on // behalf of them. @@ -105,6 +115,21 @@ public abstract class AppFunctionService extends Service { }; } + private static CancellationSignal buildCancellationSignal( + @NonNull ICancellationCallback cancellationCallback) { + final ICancellationSignal cancellationSignalTransport = + CancellationSignal.createTransport(); + CancellationSignal cancellationSignal = + CancellationSignal.fromTransport(cancellationSignalTransport); + try { + cancellationCallback.sendCancellationTransport(cancellationSignalTransport); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + return cancellationSignal ; + } + private final Binder mBinder = createBinder( AppFunctionService.this, AppFunctionService.this::onExecuteFunction); @@ -115,6 +140,7 @@ public abstract class AppFunctionService extends Service { return mBinder; } + /** * Called by the system to execute a specific app function. * @@ -134,9 +160,45 @@ public abstract class AppFunctionService extends Service { * * @param request The function execution request. * @param callback A callback to report back the result. + * + * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal, + * Consumer)} instead. This method will be removed once usage references are updated. */ @MainThread + @Deprecated public abstract void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull Consumer<ExecuteAppFunctionResponse> callback); + + /** + * Called by the system to execute a specific app function. + * + * <p>This method is triggered when the system requests your AppFunctionService to handle a + * particular function you have registered and made available. + * + * <p>To ensure proper routing of function requests, assign a unique identifier to each + * function. This identifier doesn't need to be globally unique, but it must be unique within + * your app. For example, a function to order food could be identified as "orderFood". In most + * cases this identifier should come from the ID automatically generated by the AppFunctions + * SDK. You can determine the specific function to invoke by calling {@link + * ExecuteAppFunctionRequest#getFunctionIdentifier()}. + * + * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker + * thread and dispatch the result with the given callback. You should always report back the + * result using the callback, no matter if the execution was successful or not. + * + * <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel + * the execution of function if requested by the system. + * + * @param request The function execution request. + * @param cancellationSignal A signal to cancel the execution. + * @param callback A callback to report back the result. + */ + @MainThread + public void onExecuteFunction( + @NonNull ExecuteAppFunctionRequest request, + @NonNull CancellationSignal cancellationSignal, + @NonNull Consumer<ExecuteAppFunctionResponse> callback) { + onExecuteFunction(request, callback); + } } diff --git a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java index f6580e63d757..4ed0a1b50a08 100644 --- a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java +++ b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java @@ -99,6 +99,9 @@ public final class ExecuteAppFunctionResponse implements Parcelable { /** The operation was timed out. */ public static final int RESULT_TIMED_OUT = 5; + /** The caller tried to execute a disabled app function. */ + public static final int RESULT_DISABLED = 6; + /** The result code of the app function execution. */ @ResultCode private final int mResultCode; @@ -274,6 +277,7 @@ public final class ExecuteAppFunctionResponse implements Parcelable { RESULT_INTERNAL_ERROR, RESULT_INVALID_ARGUMENT, RESULT_TIMED_OUT, + RESULT_DISABLED, }) @Retention(RetentionPolicy.SOURCE) public @interface ResultCode {} diff --git a/core/java/android/app/appfunctions/GenericDocumentWrapper.java b/core/java/android/app/appfunctions/GenericDocumentWrapper.java index 84b1837f4a2f..b29b64e44d21 100644 --- a/core/java/android/app/appfunctions/GenericDocumentWrapper.java +++ b/core/java/android/app/appfunctions/GenericDocumentWrapper.java @@ -16,10 +16,13 @@ package android.app.appfunctions; +import android.annotation.Nullable; import android.app.appsearch.GenericDocument; import android.os.Parcel; import android.os.Parcelable; +import android.util.MathUtils; +import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import java.util.Objects; @@ -31,24 +34,33 @@ import java.util.Objects; * <p>{#link {@link Parcel#writeBlob(byte[])}} could take care of whether to pass data via binder * directly or Android shared memory if the data is large. * + * <p>This class performs lazy unparcelling. The `GenericDocument` is only unparcelled + * from the underlying `Parcel` when {@link #getValue()} is called. This optimization + * allows the system server to pass through the generic document, without unparcel and parcel it. + * * @hide * @see Parcel#writeBlob(byte[]) */ public final class GenericDocumentWrapper implements Parcelable { + @Nullable + @GuardedBy("mLock") + private GenericDocument mGenericDocument; + @GuardedBy("mLock") + @Nullable private Parcel mParcel; + private final Object mLock = new Object(); + public static final Creator<GenericDocumentWrapper> CREATOR = new Creator<>() { @Override public GenericDocumentWrapper createFromParcel(Parcel in) { - byte[] dataBlob = Objects.requireNonNull(in.readBlob()); - Parcel unmarshallParcel = Parcel.obtain(); - try { - unmarshallParcel.unmarshall(dataBlob, 0, dataBlob.length); - unmarshallParcel.setDataPosition(0); - return new GenericDocumentWrapper( - GenericDocument.createFromParcel(unmarshallParcel)); - } finally { - unmarshallParcel.recycle(); - } + int length = in.readInt(); + int offset = in.dataPosition(); + in.setDataPosition(MathUtils.addOrThrow(offset, length)); + + Parcel p = Parcel.obtain(); + p.appendFrom(in, offset, length); + p.setDataPosition(0); + return new GenericDocumentWrapper(p); } @Override @@ -56,16 +68,42 @@ public final class GenericDocumentWrapper implements Parcelable { return new GenericDocumentWrapper[size]; } }; - @NonNull private final GenericDocument mGenericDocument; public GenericDocumentWrapper(@NonNull GenericDocument genericDocument) { mGenericDocument = Objects.requireNonNull(genericDocument); + mParcel = null; + } + + public GenericDocumentWrapper(@NonNull Parcel parcel) { + mGenericDocument = null; + mParcel = Objects.requireNonNull(parcel); } /** Returns the wrapped {@link android.app.appsearch.GenericDocument} */ @NonNull public GenericDocument getValue() { - return mGenericDocument; + unparcel(); + synchronized (mLock) { + return Objects.requireNonNull(mGenericDocument); + } + } + + private void unparcel() { + synchronized (mLock) { + if (mGenericDocument != null) { + return; + } + byte[] dataBlob = Objects.requireNonNull(Objects.requireNonNull(mParcel).readBlob()); + Parcel unmarshallParcel = Parcel.obtain(); + try { + unmarshallParcel.unmarshall(dataBlob, 0, dataBlob.length); + unmarshallParcel.setDataPosition(0); + mGenericDocument = GenericDocument.createFromParcel(unmarshallParcel); + mParcel = null; + } finally { + unmarshallParcel.recycle(); + } + } } @Override @@ -75,13 +113,32 @@ public final class GenericDocumentWrapper implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - Parcel parcel = Parcel.obtain(); - try { - mGenericDocument.writeToParcel(parcel, flags); - byte[] bytes = parcel.marshall(); - dest.writeBlob(bytes); - } finally { - parcel.recycle(); + synchronized (mLock) { + if (mGenericDocument != null) { + int lengthPos = dest.dataPosition(); + // write a placeholder for length + dest.writeInt(-1); + Parcel tempParcel = Parcel.obtain(); + byte[] bytes; + try { + mGenericDocument.writeToParcel(tempParcel, flags); + bytes = tempParcel.marshall(); + } finally { + tempParcel.recycle(); + } + int startPos = dest.dataPosition(); + dest.writeBlob(bytes); + int endPos = dest.dataPosition(); + dest.setDataPosition(lengthPos); + // Overwrite the length placeholder + dest.writeInt(endPos - startPos); + dest.setDataPosition(endPos); + + } else { + Parcel originalParcel = Objects.requireNonNull(mParcel); + dest.writeInt(originalParcel.dataSize()); + dest.appendFrom(originalParcel, 0, originalParcel.dataSize()); + } } } } diff --git a/core/java/android/app/appfunctions/IAppFunctionEnabledCallback.aidl b/core/java/android/app/appfunctions/IAppFunctionEnabledCallback.aidl new file mode 100644 index 000000000000..ced415541e49 --- /dev/null +++ b/core/java/android/app/appfunctions/IAppFunctionEnabledCallback.aidl @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.appfunctions; + +import android.os.ParcelableException; + +/** + * @hide + */ +oneway interface IAppFunctionEnabledCallback { + void onSuccess(); + void onError(in ParcelableException exception); +} diff --git a/core/java/android/app/appfunctions/IAppFunctionManager.aidl b/core/java/android/app/appfunctions/IAppFunctionManager.aidl index 28827bb3052c..72335e40c207 100644 --- a/core/java/android/app/appfunctions/IAppFunctionManager.aidl +++ b/core/java/android/app/appfunctions/IAppFunctionManager.aidl @@ -17,8 +17,11 @@ package android.app.appfunctions; import android.app.appfunctions.ExecuteAppFunctionAidlRequest; +import android.app.appfunctions.IAppFunctionEnabledCallback; import android.app.appfunctions.IExecuteAppFunctionCallback; +import android.os.ICancellationSignal; +import android.os.UserHandle; /** * Defines the interface for apps to interact with the app function execution service * {@code AppFunctionManagerService} running in the system server process. @@ -32,8 +35,19 @@ interface IAppFunctionManager { * @param callback the callback to report the result. */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf = {android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED,android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional = true)") - void executeAppFunction( + ICancellationSignal executeAppFunction( in ExecuteAppFunctionAidlRequest request, in IExecuteAppFunctionCallback callback ); -}
\ No newline at end of file + + /** + * Sets an AppFunction's enabled state provided by {@link AppFunctionService} through the system. + */ + void setAppFunctionEnabled( + in String callingPackage, + in String functionIdentifier, + in UserHandle userHandle, + int enabledState, + in IAppFunctionEnabledCallback callback + ); +} diff --git a/core/java/android/app/appfunctions/IAppFunctionService.aidl b/core/java/android/app/appfunctions/IAppFunctionService.aidl index cc5a20cfa194..291f33ccb1b8 100644 --- a/core/java/android/app/appfunctions/IAppFunctionService.aidl +++ b/core/java/android/app/appfunctions/IAppFunctionService.aidl @@ -16,7 +16,7 @@ package android.app.appfunctions; -import android.os.Bundle; +import android.app.appfunctions.ICancellationCallback; import android.app.appfunctions.IExecuteAppFunctionCallback; import android.app.appfunctions.ExecuteAppFunctionRequest; @@ -34,10 +34,12 @@ oneway interface IAppFunctionService { * Called by the system to execute a specific app function. * * @param request the function execution request. + * @param cancellationCallback a callback to send back the cancellation transport. * @param callback a callback to report back the result. */ void executeAppFunction( in ExecuteAppFunctionRequest request, + in ICancellationCallback cancellationCallback, in IExecuteAppFunctionCallback callback ); } diff --git a/core/java/android/app/appfunctions/ICancellationCallback.aidl b/core/java/android/app/appfunctions/ICancellationCallback.aidl new file mode 100644 index 000000000000..03235aca017a --- /dev/null +++ b/core/java/android/app/appfunctions/ICancellationCallback.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.app.appfunctions; + +import android.os.ICancellationSignal; + +/** {@hide} */ +oneway interface ICancellationCallback { + void sendCancellationTransport(in ICancellationSignal cancellationTransport); +}
\ No newline at end of file diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index 108b5f40863c..b13901721909 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -31,6 +31,16 @@ flag { } flag { + name: "modes_ui_empty_shade" + namespace: "systemui" + description: "Shows mode that is currently blocking notifications in the empty shade; dependent on flags modes_api and modes_ui" + bug: "366003631" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "modes_ui_test" namespace: "systemui" description: "Guards new CTS tests for Modes; dependent on flags modes_api and modes_ui" diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index abb562d8ddaf..d8142fd9687c 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -1042,10 +1042,11 @@ public class AppWidgetManager { } /** - * Get the available info about the AppWidget. + * Returns the {@link AppWidgetProviderInfo} for the specified AppWidget. * - * @return A appWidgetId. If the appWidgetId has not been bound to a provider yet, or - * you don't have access to that appWidgetId, null is returned. + * @return Information regarding the provider of speficied widget, returns null if the + * appWidgetId has not been bound to a provider yet, or you don't have access + * to that widget. */ public AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId) { if (mService == null) { @@ -1390,7 +1391,7 @@ public class AppWidgetManager { * * @param provider The {@link ComponentName} for the {@link * android.content.BroadcastReceiver BroadcastReceiver} provider for your AppWidget. - * @param extras In not null, this is passed to the launcher app. For eg {@link + * @param extras IF not null, this is passed to the launcher app. e.g. {@link * #EXTRA_APPWIDGET_PREVIEW} can be used for a custom preview. * @param successCallback If not null, this intent will be sent when the widget is created. * diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 031380dc1962..044178c4f6aa 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -20,6 +20,7 @@ import static android.app.sdksandbox.SdkSandboxManager.ACTION_START_SANDBOXED_AC import static android.content.ContentProvider.maybeAddUserId; import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE; import static android.security.Flags.FLAG_FRP_ENFORCEMENT; +import static android.security.Flags.preventIntentRedirect; import android.Manifest; import android.accessibilityservice.AccessibilityService; @@ -7687,9 +7688,17 @@ public class Intent implements Parcelable, Cloneable { /** @hide */ public static final int LOCAL_FLAG_FROM_SYSTEM = 1 << 5; + /** + * This flag indicates the creator token of this intent has been verified. + * + * @hide + */ + public static final int LOCAL_FLAG_CREATOR_TOKEN_VERIFIED = 1 << 6; + /** @hide */ @IntDef(flag = true, prefix = { "EXTENDED_FLAG_" }, value = { EXTENDED_FLAG_FILTER_MISMATCH, + EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN, }) @Retention(RetentionPolicy.SOURCE) public @interface ExtendedFlags {} @@ -7703,6 +7712,13 @@ public class Intent implements Parcelable, Cloneable { @TestApi public static final int EXTENDED_FLAG_FILTER_MISMATCH = 1 << 0; + /** + * This flag indicates the creator token of this intent is either missing or invalid. + * + * @hide + */ + public static final int EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN = 1 << 1; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // toUri() and parseUri() options. @@ -7870,6 +7886,7 @@ public class Intent implements Parcelable, Cloneable { this.mPackage = o.mPackage; this.mComponent = o.mComponent; this.mOriginalIntent = o.mOriginalIntent; + this.mCreatorTokenInfo = o.mCreatorTokenInfo; if (o.mCategories != null) { this.mCategories = new ArraySet<>(o.mCategories); @@ -12176,6 +12193,60 @@ public class Intent implements Parcelable, Cloneable { return (mExtras != null) ? mExtras.describeContents() : 0; } + private static class CreatorTokenInfo { + // Stores a creator token for an intent embedded as an extra intent in a top level intent, + private IBinder mCreatorToken; + // Stores all extra keys whose values are intents for a top level intent. + private ArraySet<String> mExtraIntentKeys; + } + + private @Nullable CreatorTokenInfo mCreatorTokenInfo; + + /** @hide */ + public void removeCreatorTokenInfo() { + mCreatorTokenInfo = null; + } + + /** @hide */ + public @Nullable IBinder getCreatorToken() { + return mCreatorTokenInfo == null ? null : mCreatorTokenInfo.mCreatorToken; + } + + /** @hide */ + public Set<String> getExtraIntentKeys() { + return mCreatorTokenInfo == null ? null : mCreatorTokenInfo.mExtraIntentKeys; + } + + /** @hide */ + public void setCreatorToken(@NonNull IBinder creatorToken) { + if (mCreatorTokenInfo == null) { + mCreatorTokenInfo = new CreatorTokenInfo(); + } + mCreatorTokenInfo.mCreatorToken = creatorToken; + } + + /** + * Collects keys in the extra bundle whose value are intents. + * @hide + */ + public void collectExtraIntentKeys() { + if (!preventIntentRedirect()) return; + + if (mExtras != null && !mExtras.isParcelled() && !mExtras.isEmpty()) { + for (String key : mExtras.keySet()) { + if (mExtras.get(key) instanceof Intent) { + if (mCreatorTokenInfo == null) { + mCreatorTokenInfo = new CreatorTokenInfo(); + } + if (mCreatorTokenInfo.mExtraIntentKeys == null) { + mCreatorTokenInfo.mExtraIntentKeys = new ArraySet<>(); + } + mCreatorTokenInfo.mExtraIntentKeys.add(key); + } + } + } + } + public void writeToParcel(Parcel out, int flags) { out.writeString8(mAction); Uri.writeToParcel(out, mData); @@ -12225,6 +12296,16 @@ public class Intent implements Parcelable, Cloneable { } else { out.writeInt(0); } + + if (preventIntentRedirect()) { + if (mCreatorTokenInfo == null) { + out.writeInt(0); + } else { + out.writeInt(1); + out.writeStrongBinder(mCreatorTokenInfo.mCreatorToken); + out.writeArraySet(mCreatorTokenInfo.mExtraIntentKeys); + } + } } public static final @android.annotation.NonNull Parcelable.Creator<Intent> CREATOR @@ -12282,6 +12363,14 @@ public class Intent implements Parcelable, Cloneable { if (in.readInt() != 0) { mOriginalIntent = new Intent(in); } + + if (preventIntentRedirect()) { + if (in.readInt() != 0) { + mCreatorTokenInfo = new CreatorTokenInfo(); + mCreatorTokenInfo.mCreatorToken = in.readStrongBinder(); + mCreatorTokenInfo.mExtraIntentKeys = (ArraySet<String>) in.readArraySet(null); + } + } } /** diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index 60b409ac6346..160cbdffe5bb 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -301,4 +301,11 @@ flag { description: "Feature flag to remove hack code of using PackageManager.MATCH_ANY_USER flag without cross user permission." bug: "332664521" is_fixed_read_only: true -}
\ No newline at end of file +} + +flag { + name: "delete_packages_silently_backport" + namespace: "package_manager_service" + description: "Feature flag to enable the holder of SYSTEM_APP_PROTECTION_SERVICE role to silently delete packages. To be deprecated by delete_packages_silently." + bug: "361776825" +} diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index 21627920f598..1b21bdf7ba45 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -181,7 +181,7 @@ public final class CameraManager { * @hide */ @TestApi - @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public static final int ROTATION_OVERRIDE_NONE = ICameraService.ROTATION_OVERRIDE_NONE; /** @@ -191,7 +191,7 @@ public final class CameraManager { * @hide */ @TestApi - @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT = ICameraService.ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT; @@ -201,7 +201,7 @@ public final class CameraManager { * @hide */ @TestApi - @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public static final int ROTATION_OVERRIDE_ROTATION_ONLY = ICameraService.ROTATION_OVERRIDE_ROTATION_ONLY; @@ -1562,7 +1562,7 @@ public final class CameraManager { */ public static int getRotationOverride(@Nullable Context context, @Nullable PackageManager packageManager, @Nullable String packageName) { - if (com.android.window.flags.Flags.cameraCompatForFreeform()) { + if (com.android.window.flags.Flags.enableCameraCompatForDesktopWindowing()) { return getRotationOverrideInternal(context, packageManager, packageName); } else { return shouldOverrideToPortrait(packageManager, packageName) @@ -1574,7 +1574,7 @@ public final class CameraManager { /** * @hide */ - @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @TestApi public static int getRotationOverrideInternal(@Nullable Context context, @Nullable PackageManager packageManager, @Nullable String packageName) { diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index a2d24f643bfb..73b5d947c0fe 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -432,6 +432,11 @@ public abstract class DisplayManagerInternal { public abstract IntArray getDisplayGroupIds(); /** + * Get all available display ids. + */ + public abstract IntArray getDisplayIds(); + + /** * Called upon presentation started/ended on the display. * @param displayId the id of the display where presentation started. * @param isShown whether presentation is shown. diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java index 79cbd19b248d..05e91e447a43 100644 --- a/core/java/android/hardware/soundtrigger/SoundTrigger.java +++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java @@ -1529,6 +1529,8 @@ public class SoundTrigger { * config that can be used by * {@link SoundTriggerModule#startRecognition(int, RecognitionConfig)} * + * @deprecated should use builder-based constructor instead. + * TODO(b/368042125): remove this method. * @param captureRequested Whether the DSP should capture the trigger sound. * @param allowMultipleTriggers Whether the service should restart listening after the DSP * triggers. @@ -1539,6 +1541,8 @@ public class SoundTrigger { * * @hide */ + @Deprecated + @SuppressWarnings("Todo") @TestApi public RecognitionConfig(boolean captureRequested, boolean allowMultipleTriggers, @SuppressLint("ArrayReturn") @Nullable KeyphraseRecognitionExtra[] keyphrases, @@ -1695,6 +1699,89 @@ public class SoundTrigger { result = prime * result + mAudioCapabilities; return result; } + + /** + * Builder class for {@link RecognitionConfig} objects. + */ + public static final class Builder { + private boolean mCaptureRequested; + private boolean mAllowMultipleTriggers; + @Nullable private KeyphraseRecognitionExtra[] mKeyphrases; + @Nullable private byte[] mData; + private int mAudioCapabilities; + + /** + * Constructs a new Builder with the default values. + */ + public Builder() { + } + + /** + * Sets capture requested state. + * @param captureRequested The new requested state. + * @return the same Builder instance. + */ + public @NonNull Builder setCaptureRequested(boolean captureRequested) { + mCaptureRequested = captureRequested; + return this; + } + + /** + * Sets allow multiple triggers state. + * @param allowMultipleTriggers The new allow multiple triggers state. + * @return the same Builder instance. + */ + public @NonNull Builder setAllowMultipleTriggers(boolean allowMultipleTriggers) { + mAllowMultipleTriggers = allowMultipleTriggers; + return this; + } + + /** + * Sets the keyphrases field. + * @param keyphrases The new keyphrases. + * @return the same Builder instance. + */ + public @NonNull Builder setKeyphrases( + @NonNull Collection<KeyphraseRecognitionExtra> keyphrases) { + mKeyphrases = keyphrases.toArray(new KeyphraseRecognitionExtra[keyphrases.size()]); + return this; + } + + /** + * Sets the data field. + * @param data The new data. + * @return the same Builder instance. + */ + public @NonNull Builder setData(@Nullable byte[] data) { + mData = data; + return this; + } + + /** + * Sets the audio capabilities field. + * @param audioCapabilities The new audio capabilities. + * @return the same Builder instance. + */ + public @NonNull Builder setAudioCapabilities(int audioCapabilities) { + mAudioCapabilities = audioCapabilities; + return this; + } + + /** + * Combines all of the parameters that have been set and return a new + * {@link RecognitionConfig} object. + * @return a new {@link RecognitionConfig} object + */ + public @NonNull RecognitionConfig build() { + RecognitionConfig config = new RecognitionConfig( + /* captureRequested= */ mCaptureRequested, + /* allowMultipleTriggers= */ mAllowMultipleTriggers, + /* keyphrases= */ mKeyphrases, + /* data= */ mData, + /* audioCapabilities= */ mAudioCapabilities); + return config; + } + }; } /** diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index 97e9f34064ba..ed75491b8e21 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -1111,6 +1111,21 @@ public class Binder implements IBinder { } /** + * Called whenever the stub implementation throws an exception which isn't propagated to the + * remote caller by the binder. If this method isn't overridden, this exception is swallowed, + * and some default return values are propagated to the caller. + * + * <br> <b> This should not throw. </b> Doing so would defeat the purpose of this handler, and + * suppress the exception it is handling. + * + * @param code The transaction code being handled + * @param e The exception which was thrown. + * @hide + */ + protected void onUnhandledException(int code, int flags, Exception e) { + } + + /** * @param in The raw file descriptor that an input data stream can be read from. * @param out The raw file descriptor that normal command messages should be written to. * @param err The raw file descriptor that command error messages should be written to. @@ -1408,10 +1423,15 @@ public class Binder implements IBinder { } else { Log.w(TAG, "Caught a RuntimeException from the binder stub implementation.", e); } + onUnhandledException(code, flags, e); } else { // Clear the parcel before writing the exception. reply.setDataSize(0); reply.setDataPosition(0); + // The writeException below won't do anything useful if this is the case. + if (Parcel.getExceptionCode(e) == 0) { + onUnhandledException(code, flags, e); + } reply.writeException(e); } res = true; diff --git a/core/java/android/os/IpcDataCache.java b/core/java/android/os/IpcDataCache.java index 0776cf405cfe..e2a72dd5e385 100644 --- a/core/java/android/os/IpcDataCache.java +++ b/core/java/android/os/IpcDataCache.java @@ -48,6 +48,20 @@ import java.util.concurrent.atomic.AtomicLong; * LRU cache that's invalidated when an opaque value in a property changes. Self-synchronizing, * but doesn't hold a lock across data fetches on query misses. * + * Clients should be aware of the following commonly-seen issues: + * <ul> + * + * <li>Client calls will not go through the cache before the first invalidation signal is + * received. Therefore, servers should signal an invalidation as soon as they have data to offer to + * clients. + * + * <li>Cache invalidation is restricted to well-known processes, which means that test code cannot + * invalidate a cache. {@link #disableForTestMode()} and {@link #testPropertyName} must be used in + * test processes that attempt cache invalidation. See + * {@link PropertyInvalidatedCacheTest#testBasicCache()} for an example. + * + * </ul> + * * The intended use case is caching frequently-read, seldom-changed information normally retrieved * across interprocess communication. Imagine that you've written a user birthday information * daemon called "birthdayd" that exposes an {@code IUserBirthdayService} interface over @@ -136,20 +150,20 @@ import java.util.concurrent.atomic.AtomicLong; * string is permitted. The third parameters is the name of the API being cached; this, too, can * any value. The fourth is the name of the cache. The cache is usually named after th API. * Some things you must know about the three strings: - * <list> - * <ul> The system property that controls the cache is named {@code cache_key.<module>.<api>}. + * <ul> + * <li> The system property that controls the cache is named {@code cache_key.<module>.<api>}. * Usually, the SELinux rules permit a process to write a system property (and therefore * invalidate a cache) based on the wildcard {@code cache_key.<module>.*}. This means that * although the cache can be constructed with any module string, whatever string is chosen must be * consistent with the SELinux configuration. - * <ul> The API name can be any string of alphanumeric characters. All caches with the same API + * <li> The API name can be any string of alphanumeric characters. All caches with the same API * are invalidated at the same time. If a server supports several caches and all are invalidated * in common, then it is most efficient to assign the same API string to every cache. - * <ul> The cache name can be any string. In debug output, the name is used to distiguish between + * <li> The cache name can be any string. In debug output, the name is used to distiguish between * caches with the same API name. The cache name is also used when disabling caches in the * current process. So, invalidation is based on the module+api but disabling (which is generally * a once-per-process operation) is based on the cache name. - * </list> + * </ul> * * User birthdays do occasionally change, so we have to modify the server to invalidate this * cache when necessary. That invalidation code looks like this: diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index b8a8be159d12..d82af55e2771 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -10750,6 +10750,16 @@ public final class Settings { "lock_screen_show_only_unseen_notifications"; /** + * Indicates whether to minimalize the number of notifications to show on the lockscreen. + * <p> + * Type: int (0 for false, 1 for true) + * + * @hide + */ + public static final String LOCK_SCREEN_NOTIFICATION_MINIMALISM = + "lock_screen_notification_minimalism"; + + /** * Indicates whether snooze options should be shown on notifications * <p> * Type: int (0 for false, 1 for true) diff --git a/core/java/android/ranging/mock/RangingFrameworkInitializer.java b/core/java/android/ranging/mock/RangingFrameworkInitializer.java new file mode 100644 index 000000000000..540f51954a9c --- /dev/null +++ b/core/java/android/ranging/mock/RangingFrameworkInitializer.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.ranging; + +/** +* Mock RangingFrameworkInitializer. +* +* @hide +*/ + +// TODO(b/331206299): Remove this after RANGING_STACK_ENABLED is ramped up to next. +public final class RangingFrameworkInitializer { + private RangingFrameworkInitializer() {} + /** + * @hide + */ + public static void registerServiceWrappers() { + // No-op. + } +} diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig index 56d3669ac50c..5457bbee8ad3 100644 --- a/core/java/android/security/responsible_apis_flags.aconfig +++ b/core/java/android/security/responsible_apis_flags.aconfig @@ -24,6 +24,17 @@ flag { } flag { + name: "asm_reintroduce_grace_period" + namespace: "responsible_apis" + description: "Allow launches within the grace period for ASM apps" + bug: "367702727" + metadata { + purpose: PURPOSE_BUGFIX + } +} + + +flag { name: "content_uri_permission_apis" is_exported: true namespace: "responsible_apis" @@ -52,3 +63,11 @@ flag { description: "Opt the system into enforcement of BAL" bug: "339403750" } + +flag { + name: "prevent_intent_redirect" + namespace: "responsible_apis" + description: "Prevent intent redirect attacks" + bug: "361143368" + is_fixed_read_only: true +}
\ No newline at end of file diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 66776ce04ad0..ac208b57788d 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -2064,7 +2064,11 @@ public final class ViewRootImpl implements ViewParent, if (mAttachInfo.mThreadedRenderer == null) return; if (mAttachInfo.mThreadedRenderer.setForceDark(determineForceDarkType())) { // TODO: Don't require regenerating all display lists to apply this setting - invalidateWorld(mView); + if (forceInvertColor()) { + destroyAndInvalidate(); + } else { + invalidateWorld(mView); + } } } @@ -11911,15 +11915,23 @@ public final class ViewRootImpl implements ViewParent, public void onHighTextContrastStateChanged(boolean enabled) { ThreadedRenderer.setHighContrastText(enabled); - // Destroy Displaylists so they can be recreated with high contrast recordings - destroyHardwareResources(); - - // Schedule redraw, which will rerecord + redraw all text - invalidate(); + destroyAndInvalidate(); } } /** + * Destroy Displaylists so they can be recreated with new recordings, in case you are changing + * the way things are rendered (e.g. high contrast, force dark), then invalidate to trigger a + * redraw. + */ + private void destroyAndInvalidate() { + destroyHardwareResources(); + + // Schedule redraw, which will rerecord + redraw all text + invalidate(); + } + + /** * This class is an interface this ViewAncestor provides to the * AccessibilityManagerService to the latter can interact with * the view hierarchy in this ViewAncestor. diff --git a/core/java/android/view/WindowLayout.java b/core/java/android/view/WindowLayout.java index dda399357d8c..d5ccca992b4f 100644 --- a/core/java/android/view/WindowLayout.java +++ b/core/java/android/view/WindowLayout.java @@ -157,10 +157,10 @@ public class WindowLayout { // which prevents overlap with the DisplayCutout. if (!attachedInParent && !floatingInScreenWindow) { mTempRect.set(outParentFrame); - outParentFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars); + intersectOrClamp(outParentFrame, displayCutoutSafeExceptMaybeBars); frames.isParentFrameClippedByDisplayCutout = !mTempRect.equals(outParentFrame); } - outDisplayFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars); + intersectOrClamp(outDisplayFrame, displayCutoutSafeExceptMaybeBars); } final boolean noLimits = (attrs.flags & FLAG_LAYOUT_NO_LIMITS) != 0; @@ -283,6 +283,19 @@ public class WindowLayout { + " requestedInvisibleTypes=" + WindowInsets.Type.toString(~requestedVisibleTypes)); } + /** + * If both rectangles intersect, set inOutRect to that intersection. Otherwise, clamp inOutRect + * to the side (or the corner) that the other rectangle is away from. + * Unlike {@link Rect#intersectUnchecked(Rect)}, this method guarantees that the new rectangle + * is valid and contained in inOutRect if rectangles involved are valid. + */ + private static void intersectOrClamp(Rect inOutRect, Rect other) { + inOutRect.left = Math.min(Math.max(inOutRect.left, other.left), inOutRect.right); + inOutRect.top = Math.min(Math.max(inOutRect.top, other.top), inOutRect.bottom); + inOutRect.right = Math.max(Math.min(inOutRect.right, other.right), inOutRect.left); + inOutRect.bottom = Math.max(Math.min(inOutRect.bottom, other.bottom), inOutRect.top); + } + public static void extendFrameByCutout(Rect displayCutoutSafe, Rect displayFrame, Rect inOutFrame, Rect tempRect) { if (displayCutoutSafe.contains(inOutFrame)) { diff --git a/core/java/android/view/accessibility/AccessibilityWindowInfo.java b/core/java/android/view/accessibility/AccessibilityWindowInfo.java index c92593f81558..7b6e070f0008 100644 --- a/core/java/android/view/accessibility/AccessibilityWindowInfo.java +++ b/core/java/android/view/accessibility/AccessibilityWindowInfo.java @@ -880,10 +880,6 @@ public final class AccessibilityWindowInfo implements Parcelable { * @hide */ public static String typeToString(int type) { - if (Flags.enableTypeWindowControl() && type == TYPE_WINDOW_CONTROL) { - return "TYPE_WINDOW_CONTROL"; - } - switch (type) { case TYPE_APPLICATION: { return "TYPE_APPLICATION"; @@ -903,8 +899,12 @@ public final class AccessibilityWindowInfo implements Parcelable { case TYPE_MAGNIFICATION_OVERLAY: { return "TYPE_MAGNIFICATION_OVERLAY"; } - default: + default: { + if (Flags.enableTypeWindowControl() && type == TYPE_WINDOW_CONTROL) { + return "TYPE_WINDOW_CONTROL"; + } return "<UNKNOWN:" + type + ">"; + } } } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 2f649c21fe08..1e5c6d8177e1 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -465,13 +465,6 @@ public final class InputMethodManager { private static final long USE_ASYNC_SHOW_HIDE_METHOD = 352594277L; // This is a bug id. /** - * Version-gating is guarded by bug-fix flag. - */ - private static final boolean ASYNC_SHOW_HIDE_METHOD_ENABLED = - !Flags.compatchangeForZerojankproxy() - || CompatChanges.isChangeEnabled(USE_ASYNC_SHOW_HIDE_METHOD); - - /** * If {@code true}, avoid calling the * {@link com.android.server.inputmethod.InputMethodManagerService InputMethodManagerService} * by skipping the call to {@link IInputMethodManager#startInputOrWindowGainedFocus} @@ -614,6 +607,15 @@ public final class InputMethodManager { @UnsupportedAppUsage Rect mCursorRect = new Rect(); + /** + * Version-gating is guarded by bug-fix flag. + */ + // Note: this is non-static so that it only gets initialized once CompatChanges has + // access to the correct application context. + private final boolean mAsyncShowHideMethodEnabled = + !Flags.compatchangeForZerojankproxy() + || CompatChanges.isChangeEnabled(USE_ASYNC_SHOW_HIDE_METHOD); + /** Cached value for {@link #isStylusHandwritingAvailable} for userId. */ @GuardedBy("mH") private PropertyInvalidatedCache<Integer, Boolean> mStylusHandwritingAvailableCache; @@ -2419,7 +2421,7 @@ public final class InputMethodManager { mCurRootView.getLastClickToolType(), resultReceiver, reason, - ASYNC_SHOW_HIDE_METHOD_ENABLED); + mAsyncShowHideMethodEnabled); } } } @@ -2463,7 +2465,7 @@ public final class InputMethodManager { mCurRootView.getLastClickToolType(), resultReceiver, reason, - ASYNC_SHOW_HIDE_METHOD_ENABLED); + mAsyncShowHideMethodEnabled); } } @@ -2572,7 +2574,7 @@ public final class InputMethodManager { return true; } else { return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, - statsToken, flags, resultReceiver, reason, ASYNC_SHOW_HIDE_METHOD_ENABLED); + statsToken, flags, resultReceiver, reason, mAsyncShowHideMethodEnabled); } } } @@ -2615,7 +2617,7 @@ public final class InputMethodManager { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, view.getWindowToken(), - statsToken, flags, null, reason, ASYNC_SHOW_HIDE_METHOD_ENABLED); + statsToken, flags, null, reason, mAsyncShowHideMethodEnabled); } } @@ -3392,7 +3394,7 @@ public final class InputMethodManager { servedInputConnection == null ? null : servedInputConnection.asIRemoteAccessibilityInputConnection(), view.getContext().getApplicationInfo().targetSdkVersion, targetUserId, - mImeDispatcher, ASYNC_SHOW_HIDE_METHOD_ENABLED); + mImeDispatcher, mAsyncShowHideMethodEnabled); } else { res = IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus( startInputReason, mClient, windowGainingFocus, startInputFlags, diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index b0e38e256430..cff42fbcfcc5 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -399,13 +399,10 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto return -1; } case "enable-text" -> { - if (mViewerConfigReader != null) { - mViewerConfigReader.loadViewerConfig(groups, logger); - } - return setTextLogging(true, logger, groups); + return startLoggingToLogcat(groups, logger); } case "disable-text" -> { - return setTextLogging(false, logger, groups); + return stopLoggingToLogcat(groups, logger); } default -> { return unknownCommand(pw); diff --git a/core/java/com/android/internal/protolog/Utils.java b/core/java/com/android/internal/protolog/Utils.java index 1e6ba309c046..00ef80ab2bdd 100644 --- a/core/java/com/android/internal/protolog/Utils.java +++ b/core/java/com/android/internal/protolog/Utils.java @@ -93,8 +93,7 @@ public class Utils { os.write(TAG, tag); break; default: - throw new RuntimeException( - "Unexpected field id " + pis.getFieldNumber()); + Log.e(LOG_TAG, "Unexpected field id " + pis.getFieldNumber()); } } @@ -126,8 +125,7 @@ public class Utils { os.write(LOCATION, pis.readString(LOCATION)); break; default: - throw new RuntimeException( - "Unexpected field id " + pis.getFieldNumber()); + Log.e(LOG_TAG, "Unexpected field id " + pis.getFieldNumber()); } } diff --git a/core/jni/android_util_XmlBlock.cpp b/core/jni/android_util_XmlBlock.cpp index 5a444bb1d0ff..c364451057bc 100644 --- a/core/jni/android_util_XmlBlock.cpp +++ b/core/jni/android_util_XmlBlock.cpp @@ -83,7 +83,7 @@ static jlong android_content_XmlBlock_nativeCreateParseState(JNIEnv* env, jobjec return 0; } - ResXMLParser* st = new ResXMLParser(*osb); + ResXMLParser* st = new(std::nothrow) ResXMLParser(*osb); if (st == NULL) { jniThrowException(env, "java/lang/OutOfMemoryError", NULL); return 0; diff --git a/core/proto/android/server/vibrator/vibratormanagerservice.proto b/core/proto/android/server/vibrator/vibratormanagerservice.proto index e7f0560612cc..258832e3e7ff 100644 --- a/core/proto/android/server/vibrator/vibratormanagerservice.proto +++ b/core/proto/android/server/vibrator/vibratormanagerservice.proto @@ -157,10 +157,8 @@ message VibratorManagerServiceDumpProto { option (.android.msg_privacy).dest = DEST_AUTOMATIC; repeated int32 vibrator_ids = 1; optional VibrationProto current_vibration = 2; - optional bool is_vibrating = 3; optional int32 is_vibrator_controller_registered = 27; optional VibrationProto current_external_vibration = 4; - optional bool vibrator_under_external_control = 5; optional bool low_power_mode = 6; optional bool vibrate_on = 24; reserved 25; // prev keyboard_vibration_on @@ -183,4 +181,6 @@ message VibratorManagerServiceDumpProto { repeated VibrationProto previous_vibrations = 16; repeated VibrationParamProto previous_vibration_params = 28; reserved 17; // prev previous_external_vibrations + reserved 3; // prev is_vibrating, check current_vibration instead + reserved 5; // prev vibrator_under_external_control, check current_external_vibration instead }
\ No newline at end of file diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 07efad89010a..92c390656da5 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4457,6 +4457,11 @@ <!-- Bytes that the PinnerService will pin for WebView --> <integer name="config_pinnerWebviewPinBytes">0</integer> + <!-- Maximum memory that PinnerService will pin for apps expressed + as a percentage of total device memory [0,100]. + Example: 10, means 10% of total memory will be the maximum pinned memory --> + <integer name="config_pinnerMaxPinnedMemoryPercentage">10</integer> + <!-- Number of days preloaded file cache should be preserved on a device before it can be deleted --> <integer name="config_keepPreloadsMinDays">7</integer> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index f397ef2b151c..6683dc044c9a 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -808,6 +808,12 @@ This is bigger than displayed because listeners can use it for other displays e.g. wearables. --> <dimen name="notification_person_icon_max_size">144dp</dimen> + <!-- The size of the progress bar icon --> + <dimen name="notification_progress_icon_size">20dp</dimen> + <!-- The size of the progress tracker width --> + <dimen name="notification_progress_tracker_width">40dp</dimen> + <!-- The size of the progress tracker height --> + <dimen name="notification_progress_tracker_height">20dp</dimen> <!-- The maximum size of the small notification icon on low memory devices. --> <dimen name="notification_small_icon_size_low_ram">@dimen/notification_small_icon_size</dimen> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 06b36b8f74af..5f40a6c7eba4 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3467,6 +3467,7 @@ <java-symbol type="integer" name="config_pinnerHomePinBytes" /> <java-symbol type="bool" name="config_pinnerAssistantApp" /> <java-symbol type="integer" name="config_pinnerWebviewPinBytes" /> + <java-symbol type="integer" name="config_pinnerMaxPinnedMemoryPercentage" /> <java-symbol type="string" name="config_doubleTouchGestureEnableFile" /> @@ -3854,6 +3855,9 @@ <java-symbol type="dimen" name="notification_custom_view_max_image_height"/> <java-symbol type="dimen" name="notification_custom_view_max_image_width"/> <java-symbol type="dimen" name="notification_person_icon_max_size" /> + <java-symbol type="dimen" name="notification_progress_icon_size" /> + <java-symbol type="dimen" name="notification_progress_tracker_width" /> + <java-symbol type="dimen" name="notification_progress_tracker_height" /> <java-symbol type="dimen" name="notification_small_icon_size_low_ram"/> <java-symbol type="dimen" name="notification_big_picture_max_height_low_ram"/> diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index d98836f8ce20..9821d433500f 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -25,6 +25,7 @@ filegroup { "BinderProxyCountingTestApp/src/**/*.java", "BinderProxyCountingTestService/src/**/*.java", "BinderDeathRecipientHelperApp/src/**/*.java", + "AppThatCallsBinderMethods/src/**/*.kt", ], visibility: ["//visibility:private"], } @@ -104,6 +105,7 @@ android_test { "mockito-target-extended-minus-junit4", "TestParameterInjector", "android.content.res.flags-aconfig-java", + "android.security.flags-aconfig-java", ], libs: [ @@ -143,6 +145,7 @@ android_test { ":BinderProxyCountingTestApp", ":BinderProxyCountingTestService", ":AppThatUsesAppOps", + ":AppThatCallsBinderMethods", ], } diff --git a/core/tests/coretests/AndroidTest.xml b/core/tests/coretests/AndroidTest.xml index b1f1e2c2db05..05ab783c01bb 100644 --- a/core/tests/coretests/AndroidTest.xml +++ b/core/tests/coretests/AndroidTest.xml @@ -26,6 +26,7 @@ <option name="test-file-name" value="BinderProxyCountingTestApp.apk" /> <option name="test-file-name" value="BinderProxyCountingTestService.apk" /> <option name="test-file-name" value="AppThatUsesAppOps.apk" /> + <option name="test-file-name" value="AppThatCallsBinderMethods.apk" /> </target_preparer> <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> diff --git a/core/tests/coretests/AppThatCallsBinderMethods/Android.bp b/core/tests/coretests/AppThatCallsBinderMethods/Android.bp new file mode 100644 index 000000000000..dcc0d4f76bf2 --- /dev/null +++ b/core/tests/coretests/AppThatCallsBinderMethods/Android.bp @@ -0,0 +1,20 @@ +// 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. + +android_test_helper_app { + name: "AppThatCallsBinderMethods", + srcs: ["src/**/*.kt"], + platform_apis: true, + static_libs: ["coretests-aidl"], +} diff --git a/core/tests/coretests/AppThatCallsBinderMethods/AndroidManifest.xml b/core/tests/coretests/AppThatCallsBinderMethods/AndroidManifest.xml new file mode 100644 index 000000000000..b2f6d7897681 --- /dev/null +++ b/core/tests/coretests/AppThatCallsBinderMethods/AndroidManifest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.frameworks.coretests.methodcallerhelperapp"> + <application> + <receiver android:name="com.android.frameworks.coretests.methodcallerhelperapp.CallMethodsReceiver" + android:exported="true"/> + </application> + +</manifest> diff --git a/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/CallMethodsReceiver.kt b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/CallMethodsReceiver.kt new file mode 100644 index 000000000000..638cc3b7692f --- /dev/null +++ b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/CallMethodsReceiver.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.frameworks.coretests.methodcallerhelperapp + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log + +import com.android.frameworks.coretests.aidl.ITestInterface + +/** + * Receiver used to call methods when a binder is received + * {@link android.os.BinderUncaughtExceptionHandlerTest}. + */ +class CallMethodsReceiver : BroadcastReceiver() { + private val TAG = "CallMethodsReceiver" + + override fun onReceive(context: Context, intent: Intent) { + try { + when (intent.getAction()) { + ACTION_CALL_METHOD -> intent.getExtras()!!.let { + Log.i(TAG, "Received ACTION_CALL_METHOD with extras: $it") + val iface = it.getBinder(EXTRA_BINDER)!!.let(ITestInterface.Stub::asInterface)!! + val name = it.getString(EXTRA_METHOD_NAME)!! + try { + when (name) { + "foo" -> iface.foo(5) + "onewayFoo" -> iface.onewayFoo(5) + "bar" -> iface.bar(5) + else -> Log.e(TAG, "Unknown method name") + } + } catch (e: Exception) { + // Exceptions expected + } + } + else -> Log.e(TAG, "Unknown action " + intent.getAction()) + } + } catch (e: Exception) { + Log.e(TAG, "Exception: ", e) + } + } +} diff --git a/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/Constants.kt b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/Constants.kt new file mode 100644 index 000000000000..37c6268164a7 --- /dev/null +++ b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/Constants.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.frameworks.coretests.methodcallerhelperapp + +const val PACKAGE_NAME = "com.android.frameworks.coretests.methodcallerhelperapp" +const val RECEIVER_NAME = "CallMethodsReceiver" +const val ACTION_CALL_METHOD = PACKAGE_NAME + ".ACTION_CALL_METHOD" +const val EXTRA_METHOD_NAME = PACKAGE_NAME + ".EXTRA_METHOD_NAME" +const val EXTRA_BINDER = PACKAGE_NAME + ".EXTRA_BINDER" diff --git a/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/ITestInterface.aidl b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/ITestInterface.aidl new file mode 100644 index 000000000000..ffcf178beda4 --- /dev/null +++ b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/ITestInterface.aidl @@ -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.frameworks.coretests.aidl; + +/** + * Just an interface with a oneway, void and non-oneway method. + */ +interface ITestInterface { + // Method order matters, since we verify transaction codes + int foo(int a); + oneway void onewayFoo(int a); + void bar(int a); +} diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index 0f73df92ca93..edcea241e620 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -97,7 +97,6 @@ import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.TextAppearanceSpan; import android.util.Pair; -import android.util.Slog; import android.widget.RemoteViews; import androidx.test.InstrumentationRegistry; @@ -118,6 +117,7 @@ import org.junit.Test; import org.junit.rules.TestRule; import org.junit.runner.RunWith; +import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -2114,6 +2114,300 @@ public class NotificationTest { assertThat(n.getWhen()).isEqualTo(9); } + @Test + public void getNotificationStyleClass_forPlatformClassName_returnsPlatformClass() { + final List<Class<? extends Notification.Style>> platformStyleClasses = List.of( + Notification.BigTextStyle.class, Notification.BigPictureStyle.class, + Notification.MessagingStyle.class, Notification.CallStyle.class, + Notification.InboxStyle.class, Notification.MediaStyle.class, + Notification.DecoratedCustomViewStyle.class, + Notification.DecoratedMediaCustomViewStyle.class + ); + + for (Class<? extends Notification.Style> platformStyleClass : platformStyleClasses) { + assertThat(Notification.getNotificationStyleClass(platformStyleClass.getName())) + .isEqualTo(platformStyleClass); + } + } + + @Test + public void getNotificationStyleClass_forNotPlatformClassName_returnsNull() { + assertThat(Notification.getNotificationStyleClass(NotAPlatformStyle.class.getName())) + .isNull(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_richOngoingEnabled_platformClass() { + assertThat( + Notification.getNotificationStyleClass(Notification.ProgressStyle.class.getName())) + .isEqualTo(Notification.ProgressStyle.class); + } + + @Test + @DisableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_richOngoingDisabled_notPlatformClass() { + assertThat( + Notification.getNotificationStyleClass(Notification.ProgressStyle.class.getName())) + .isNull(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onSegmentChange_visiblyDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .addProgressSegment(new Notification.ProgressStyle.Segment(100)) + .addProgressSegment(new Notification.ProgressStyle.Segment(50) + .setColor(Color.RED))); + + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .addProgressSegment(new Notification.ProgressStyle.Segment(100)) + .addProgressSegment(new Notification.ProgressStyle.Segment(50) + .setColor(Color.BLUE))); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void indeterminateProgressStyle_onSegmentChange_visiblyNotDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true) + .addProgressSegment(new Notification.ProgressStyle.Segment(100)) + .addProgressSegment(new Notification.ProgressStyle.Segment(50) + .setColor(Color.RED))); + + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true) + .addProgressSegment(new Notification.ProgressStyle.Segment(100)) + .addProgressSegment(new Notification.ProgressStyle.Segment(50) + .setColor(Color.BLUE))); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onStartIconChange_visiblyDifferent() { + final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource( + mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96)); + + final Icon icon2 = Icon.createWithBitmap( + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressStartIcon(icon1)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressStartIcon(icon2)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onEndIconChange_visiblyDifferent() { + final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource( + mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96)); + + final Icon icon2 = Icon.createWithBitmap( + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressEndIcon(icon1)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressEndIcon(icon2)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onProgressChange_visiblyDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgress(20)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgress(21)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void indeterminateProgressStyle_onProgressChange_visiblyNotDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .setProgressIndeterminate(true).setProgress(20)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .setProgressIndeterminate(true).setProgress(21)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onIsStyledByProgressChange_visiblyDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setStyledByProgress(true)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setStyledByProgress(false)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void indeterminateProgressStyle_onIsStyledByProgressChange_visiblyNotDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .setProgressIndeterminate(true).setStyledByProgress(true)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .setProgressIndeterminate(true).setStyledByProgress(false)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onProgressStepChange_visiblyDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .addProgressStep(new Notification.ProgressStyle.Step(10))); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .addProgressStep(new Notification.ProgressStyle.Step(12))); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void indeterminateProgressStyle_onProgressStepChange_visiblyNotDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true) + .addProgressStep(new Notification.ProgressStyle.Step(10))); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true) + .addProgressStep(new Notification.ProgressStyle.Step(12))); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onTrackerIconChange_visiblyDifferent() { + final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource( + mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96)); + + final Icon icon2 = Icon.createWithBitmap( + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressTrackerIcon(icon1)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressTrackerIcon(icon2)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void indeterminateProgressStyle_onTrackerIconChange_visiblyNotDifferent() { + final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource( + mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96)); + + final Icon icon2 = Icon.createWithBitmap( + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true) + .setProgressTrackerIcon(icon1)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .setProgressIndeterminate(true).setProgressTrackerIcon(icon2)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onIndeterminateChange_visiblyDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(false)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_getProgressMax_default100() { + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + assertThat(progressStyle.getProgressMax()).isEqualTo(100); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_getProgressMax_nooSegments_returnsDefault() { + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + progressStyle.setProgressSegments(Collections.emptyList()); + assertThat(progressStyle.getProgressMax()).isEqualTo(100); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_getProgressMax_returnsSumOfSegmentLength() { + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + progressStyle + .addProgressSegment(new Notification.ProgressStyle.Segment(10)) + .addProgressSegment(new Notification.ProgressStyle.Segment(20)); + + assertThat(progressStyle.getProgressMax()).isEqualTo(30); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_getProgressMax_onSegmentOverflow_returnsDefault() { + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + progressStyle + .addProgressSegment(new Notification.ProgressStyle.Segment(Integer.MAX_VALUE)) + .addProgressSegment(new Notification.ProgressStyle.Segment(10)); + + assertThat(progressStyle.getProgressMax()).isEqualTo(100); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_indeterminate_defaultValueFalse() { + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + + assertThat(progressStyle1.isProgressIndeterminate()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_styledByProgress_defaultValueTrue() { + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + + assertThat(progressStyle1.isStyledByProgress()).isTrue(); + } private void assertValid(Notification.Colors c) { // Assert that all colors are populated assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID); @@ -2214,4 +2508,11 @@ public class NotificationTest { new Intent(action).setPackage(mContext.getPackageName()), PendingIntent.FLAG_MUTABLE); } + + private static class NotAPlatformStyle extends Notification.Style { + @Override + public boolean areNotificationsVisiblyDifferent(Notification.Style other) { + return false; + } + } } diff --git a/core/tests/coretests/src/android/content/IntentTest.java b/core/tests/coretests/src/android/content/IntentTest.java new file mode 100644 index 000000000000..d169ce3c07d0 --- /dev/null +++ b/core/tests/coretests/src/android/content/IntentTest.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertEquals; + +import android.net.Uri; +import android.os.Binder; +import android.os.IBinder; +import android.os.Parcel; +import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.security.Flags; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Build/Install/Run: + * atest FrameworksCoreTests:IntentTest + */ +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class IntentTest { + private static final String TEST_ACTION = "android.content.IntentTest_test"; + private static final String TEST_EXTRA_NAME = "testExtraName"; + private static final Uri TEST_URI = Uri.parse("content://com.example/people"); + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Test + @RequiresFlagsEnabled(Flags.FLAG_PREVENT_INTENT_REDIRECT) + public void testReadFromParcelWithExtraIntentKeys() { + Intent intent = new Intent("TEST_ACTION"); + intent.putExtra(TEST_EXTRA_NAME, new Intent(TEST_ACTION)); + intent.putExtra(TEST_EXTRA_NAME + "2", 1); + + intent.collectExtraIntentKeys(); + final Parcel parcel = Parcel.obtain(); + intent.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + final Intent target = new Intent(); + target.readFromParcel(parcel); + + assertEquals(intent.getAction(), target.getAction()); + assertEquals(intent.getExtraIntentKeys(), target.getExtraIntentKeys()); + assertThat(intent.getExtraIntentKeys()).hasSize(1); + } + + @Test + public void testCreatorTokenInfo() { + Intent intent = new Intent(TEST_ACTION); + IBinder creatorToken = new Binder(); + + intent.setCreatorToken(creatorToken); + assertThat(intent.getCreatorToken()).isEqualTo(creatorToken); + + intent.removeCreatorTokenInfo(); + assertThat(intent.getCreatorToken()).isNull(); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_PREVENT_INTENT_REDIRECT) + public void testCollectExtraIntentKeys() { + Intent intent = new Intent(TEST_ACTION); + Intent extraIntent = new Intent(TEST_ACTION, TEST_URI); + intent.putExtra(TEST_EXTRA_NAME, extraIntent); + + intent.collectExtraIntentKeys(); + + assertThat(intent.getExtraIntentKeys()).hasSize(1); + assertThat(intent.getExtraIntentKeys()).contains(TEST_EXTRA_NAME); + } + +} diff --git a/core/tests/coretests/src/android/os/BinderUncaughtExceptionHandlerTest.kt b/core/tests/coretests/src/android/os/BinderUncaughtExceptionHandlerTest.kt new file mode 100644 index 000000000000..791c209e4473 --- /dev/null +++ b/core/tests/coretests/src/android/os/BinderUncaughtExceptionHandlerTest.kt @@ -0,0 +1,247 @@ +/* + * 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 + +import android.content.Intent +import android.platform.test.annotations.DisabledOnRavenwood +import android.platform.test.annotations.Presubmit + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry + +import com.android.frameworks.coretests.aidl.ITestInterface +import com.android.frameworks.coretests.methodcallerhelperapp.* + +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +import org.mockito.ArgumentMatcher +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq +import org.mockito.ArgumentMatchers.intThat +import org.mockito.Mockito.after +import org.mockito.Mockito.doThrow +import org.mockito.Mockito.never +import org.mockito.Mockito.timeout +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.Spy +import org.mockito.junit.MockitoJUnit +import org.mockito.quality.Strictness.STRICT_STUBS + +private const val TIMEOUT_DURATION_MS = 2000L +private const val FALSE_NEG_DURATION_MS = 500L +private const val FLAG_ONEWAY = 1 +// From ITestInterface.Stub class, these values are package private +private const val TRANSACTION_foo = 1 +private const val TRANSACTION_onewayFoo = 2 +private const val TRANSACTION_bar = 3 + +/** Tests functionality of {@link android.os.Binder.onUnhandledException}. */ +@DisabledOnRavenwood(reason = "multi-app") +@Presubmit +@RunWith(AndroidJUnit4::class) +class BinderUncaughtExceptionHandlerTest { + + val mContext = InstrumentationRegistry.getInstrumentation().getTargetContext() + + @Rule @JvmField val rule = MockitoJUnit.rule().strictness(STRICT_STUBS) + + @Spy var mInterfaceImpl: ITestImpl = ITestImpl() + + // This subclass is needed for visibility issues (via protected), since the method we are + // verifying lives on the boot classpath, it is not enough to be in the same package. + open class ITestImpl : ITestInterface.Stub() { + override fun onUnhandledException(code: Int, flags: Int, e: Exception?) = + onUnhandledExceptionVisible(code, flags, e) + + public open fun onUnhandledExceptionVisible(code: Int, flags: Int, e: Exception?) {} + + @Throws(RemoteException::class) + override open fun foo(x: Int): Int = throw UnsupportedOperationException() + + @Throws(RemoteException::class) + override open fun onewayFoo(x: Int): Unit = throw UnsupportedOperationException() + + @Throws(RemoteException::class) + override open fun bar(x: Int): Unit = throw UnsupportedOperationException() + } + + class OnewayMatcher(private val isOneway: Boolean) : ArgumentMatcher<Int> { + override fun matches(argument: Int?) = + (argument!! and FLAG_ONEWAY) == if (isOneway) 1 else 0 + + override fun toString() = "Expected oneway: $isOneway" + } + + @Test + fun testRegularMethod_ifThrowsRuntimeException_HandlerCalled() { + val myException = RuntimeException("Test exception") + doThrow(myException).`when`(mInterfaceImpl).foo(anyInt()) + + dispatchActionCall("foo") + + verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS)) + .onUnhandledExceptionVisible( + /* transactionCode = */ eq(TRANSACTION_foo), + /* flags= */ intThat(OnewayMatcher(false)), + /* exception= */ eq(myException), + ) + // No unexpected calls + verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any()) + } + + @Test + fun testRegularMethod_ifThrowsRemoteException_HandlerCalled() { + val myException = RemoteException("Test exception") + doThrow(myException).`when`(mInterfaceImpl).foo(anyInt()) + + dispatchActionCall("foo") + + verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS)) + .onUnhandledExceptionVisible( + /* transactionCode = */ eq(TRANSACTION_foo), + /* flags= */ intThat(OnewayMatcher(false)), + /* exception= */ eq(myException), + ) + // No unexpected calls + verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any()) + } + + @Test + fun testRegularMethod_ifThrowsSecurityException_HandlerNotCalled() { + val myException = SecurityException("Test exception") + doThrow(myException).`when`(mInterfaceImpl).foo(anyInt()) + + dispatchActionCall("foo") + + // No unexpected calls + verify(mInterfaceImpl, after(FALSE_NEG_DURATION_MS).never()) + .onUnhandledExceptionVisible(anyInt(), anyInt(), any()) + } + + @Test + fun testVoidMethod_ifThrowsRuntimeException_HandlerCalled() { + val myException = RuntimeException("Test exception") + doThrow(myException).`when`(mInterfaceImpl).bar(anyInt()) + + dispatchActionCall("bar") + + verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS)) + .onUnhandledExceptionVisible( + /* transactionCode = */ eq(TRANSACTION_bar), + /* flags= */ intThat(OnewayMatcher(false)), + /* exception= */ eq(myException), + ) + // No unexpected calls + verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any()) + } + + @Test + fun testVoidMethod_ifThrowsRemoteException_HandlerCalled() { + val myException = RemoteException("Test exception") + doThrow(myException).`when`(mInterfaceImpl).bar(anyInt()) + + dispatchActionCall("bar") + + verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS)) + .onUnhandledExceptionVisible( + /* transactionCode = */ eq(TRANSACTION_bar), + /* flags= */ intThat(OnewayMatcher(false)), + /* exception= */ eq(myException), + ) + // No unexpected calls + verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any()) + } + + @Test + fun testVoidMethod_ifThrowsSecurityException_HandlerNotCalled() { + val myException = SecurityException("Test exception") + doThrow(myException).`when`(mInterfaceImpl).bar(anyInt()) + + dispatchActionCall("bar") + + // No unexpected calls + verify(mInterfaceImpl, after(FALSE_NEG_DURATION_MS).never()) + .onUnhandledExceptionVisible(anyInt(), anyInt(), any()) + } + + @Test + fun testOnewayMethod_ifThrowsRuntimeException_HandlerCalled() { + val myException = RuntimeException("Test exception") + doThrow(myException).doNothing().`when`(mInterfaceImpl).onewayFoo(anyInt()) + + dispatchActionCall("onewayFoo") + + verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS)) + .onUnhandledExceptionVisible( + /* transactionCode = */ eq(TRANSACTION_onewayFoo), + /* flags= */ intThat(OnewayMatcher(true)), + /* exception= */ eq(myException), + ) + // No unexpected calls + verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any()) + } + + @Test + fun testOnewayMethod_ifThrowsRemoteException_HandlerCalled() { + val myException = RemoteException("Test exception") + doThrow(myException).`when`(mInterfaceImpl).onewayFoo(anyInt()) + + dispatchActionCall("onewayFoo") + + verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS)) + .onUnhandledExceptionVisible( + /* transactionCode = */ eq(TRANSACTION_onewayFoo), + /* flags= */ intThat(OnewayMatcher(true)), + /* exception= */ eq(myException), + ) + // No unexpected calls + verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any()) + } + + // All exceptions are uncaught for oneway + @Test + fun testOnewayMethod_ifThrowsSecurityException_HandlerCalled() { + val myException = SecurityException("Test exception") + doThrow(myException).`when`(mInterfaceImpl).onewayFoo(anyInt()) + + dispatchActionCall("onewayFoo") + + verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS)) + .onUnhandledExceptionVisible( + /* transactionCode = */ eq(TRANSACTION_onewayFoo), + /* flags= */ intThat(OnewayMatcher(true)), + /* exception= */ eq(myException), + ) + // No unexpected calls + verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any()) + } + + private fun dispatchActionCall(methodName: String) = + Intent(ACTION_CALL_METHOD).apply { + putExtras( + Bundle().apply { + putBinder(EXTRA_BINDER, mInterfaceImpl as IBinder) + putString(EXTRA_METHOD_NAME, methodName) + } + ) + setClassName(PACKAGE_NAME, CallMethodsReceiver::class.java.getName()) + }.let { mContext.sendBroadcast(it) } +} diff --git a/core/tests/coretests/src/android/view/WindowLayoutTests.java b/core/tests/coretests/src/android/view/WindowLayoutTests.java index 5cac98daee80..d4693e6e7130 100644 --- a/core/tests/coretests/src/android/view/WindowLayoutTests.java +++ b/core/tests/coretests/src/android/view/WindowLayoutTests.java @@ -413,4 +413,19 @@ public class WindowLayoutTests { assertInsetByTopBottom(0, 0, mFrames.parentFrame); assertInsetByTopBottom(0, 0, mFrames.frame); } + + @Test + public void windowBoundsOutsideDisplayCutoutSafe() { + addDisplayCutout(); + mAttrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; + mWindowBounds.set(0, -1000, DISPLAY_WIDTH, 0); + computeFrames(); + + assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0, + mFrames.displayFrame); + assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0, + mFrames.parentFrame); + assertRect(WATERFALL_INSETS.left, 0, DISPLAY_WIDTH - WATERFALL_INSETS.right, 0, + mFrames.frame); + } } diff --git a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java index 005538a4d401..d9e90fa001c5 100644 --- a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java +++ b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java @@ -29,6 +29,7 @@ import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; import android.widget.LinearLayout; +import android.widget.TextView; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; @@ -78,6 +79,16 @@ public class ResourceFlaggingTest { } @Test + public void testNegatedDisabledFlag() { + assertThat(mResources.getBoolean(R.bool.bool5)).isTrue(); + } + + @Test + public void testNegatedEnabledFlag() { + assertThat(mResources.getBoolean(R.bool.bool6)).isTrue(); + } + + @Test public void testFlagEnabledDifferentCompilationUnit() { assertThat(mResources.getBoolean(R.bool.bool3)).isTrue(); } @@ -94,6 +105,26 @@ public class ResourceFlaggingTest { } @Test + public void testDirectoryEnabledFlag() { + assertThat(mResources.getBoolean(R.bool.bool8)).isTrue(); + } + + @Test + public void testDirectoryDisabledFlag() { + assertThat(mResources.getBoolean(R.bool.bool7)).isTrue(); + } + + @Test + public void testDirectoryNegatedEnabledFlag() { + assertThat(mResources.getBoolean(R.bool.bool9)).isTrue(); + } + + @Test + public void testDirectoryNegatedDisabledFlag() { + assertThat(mResources.getBoolean(R.bool.bool10)).isTrue(); + } + + @Test public void testLayoutWithDisabledElements() { LinearLayout ll = (LinearLayout) getLayoutInflater().inflate(R.layout.layout1, null); assertThat(ll).isNotNull(); @@ -102,6 +133,24 @@ public class ResourceFlaggingTest { assertThat((View) ll.findViewById(R.id.text2)).isNotNull(); } + @Test + public void testEnabledFlagLayoutOverrides() { + LinearLayout ll = (LinearLayout) getLayoutInflater().inflate(R.layout.layout3, null); + assertThat(ll).isNotNull(); + assertThat((View) ll.findViewById(R.id.text1)).isNotNull(); + assertThat(((TextView) ll.findViewById(R.id.text1)).getText()).isEqualTo("foobar"); + } + + @Test(expected = Resources.NotFoundException.class) + public void testDisabledLayout() { + getLayoutInflater().inflate(R.layout.layout2, null); + } + + @Test(expected = Resources.NotFoundException.class) + public void testDisabledDrawable() { + mResources.getDrawable(R.drawable.removedpng); + } + private LayoutInflater getLayoutInflater() { ContextWrapper c = new ContextWrapper(mContext) { private LayoutInflater mInflater; diff --git a/graphics/java/android/graphics/PathIterator.java b/graphics/java/android/graphics/PathIterator.java index 48b29f4e81f4..d7caabf9f91b 100644 --- a/graphics/java/android/graphics/PathIterator.java +++ b/graphics/java/android/graphics/PathIterator.java @@ -44,6 +44,8 @@ public class PathIterator implements Iterator<PathIterator.Segment> { private final Path mPath; private final int mPathGenerationId; private static final int POINT_ARRAY_SIZE = 8; + private static final boolean IS_DALVIK = "dalvik".equalsIgnoreCase( + System.getProperty("java.vm.name")); private static final NativeAllocationRegistry sRegistry = NativeAllocationRegistry.createMalloced( @@ -80,9 +82,14 @@ public class PathIterator implements Iterator<PathIterator.Segment> { mPath = path; mNativeIterator = nCreate(mPath.mNativePath); mPathGenerationId = mPath.getGenerationId(); - final VMRuntime runtime = VMRuntime.getRuntime(); - mPointsArray = (float[]) runtime.newNonMovableArray(float.class, POINT_ARRAY_SIZE); - mPointsAddress = runtime.addressOf(mPointsArray); + if (IS_DALVIK) { + final VMRuntime runtime = VMRuntime.getRuntime(); + mPointsArray = (float[]) runtime.newNonMovableArray(float.class, POINT_ARRAY_SIZE); + mPointsAddress = runtime.addressOf(mPointsArray); + } else { + mPointsArray = new float[POINT_ARRAY_SIZE]; + mPointsAddress = 0; + } sRegistry.registerNativeAllocation(this, mNativeIterator); } @@ -177,7 +184,8 @@ public class PathIterator implements Iterator<PathIterator.Segment> { throw new ConcurrentModificationException( "Iterator cannot be used on modified Path"); } - @Verb int verb = nNext(mNativeIterator, mPointsAddress); + @Verb int verb = IS_DALVIK + ? nNext(mNativeIterator, mPointsAddress) : nNextHost(mNativeIterator, mPointsArray); if (verb == VERB_DONE) { mDone = true; } @@ -287,6 +295,9 @@ public class PathIterator implements Iterator<PathIterator.Segment> { private static native long nCreate(long nativePath); private static native long nGetFinalizer(); + /* nNextHost should be used for host runtimes, e.g. LayoutLib */ + private static native int nNextHost(long nativeIterator, float[] points); + // ------------------ Critical JNI ------------------------ @CriticalNative diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java index 37f0067de453..089613853555 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java @@ -16,12 +16,11 @@ package androidx.window.common; -import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER; +import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN; import static androidx.window.common.layout.CommonFoldingFeature.parseListFromString; -import android.annotation.NonNull; import android.content.Context; import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateManager; @@ -31,16 +30,23 @@ import android.text.TextUtils; import android.util.Log; import android.util.SparseIntArray; +import androidx.annotation.BinderThread; +import androidx.annotation.GuardedBy; +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import androidx.window.common.layout.CommonFoldingFeature; import androidx.window.common.layout.DisplayFoldFeatureCommon; import com.android.internal.R; +import com.android.window.flags.Flags; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.Executor; import java.util.function.Consumer; /** @@ -55,13 +61,6 @@ public final class DeviceStateManagerFoldingFeatureProducer private static final boolean DEBUG = false; /** - * Emulated device state - * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to - * {@link CommonFoldingFeature.State} map. - */ - private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray(); - - /** * Device state received via * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)}. * The identifier returned through {@link DeviceState#getIdentifier()} may not correspond 1:1 @@ -71,23 +70,40 @@ public final class DeviceStateManagerFoldingFeatureProducer * "rear display". Concurrent mode for example is activated via public API and can be active in * both the "open" and "half folded" device states. */ - private DeviceState mCurrentDeviceState = new DeviceState( - new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER, - "INVALID").build()); + // TODO: b/337820752 - Add @GuardedBy("mCurrentDeviceStateLock") after flag cleanup. + private DeviceState mCurrentDeviceState = INVALID_DEVICE_STATE; - private List<DeviceState> mSupportedStates; + /** + * Lock to synchronize access to {@link #mCurrentDeviceState}. + * + * <p>This lock is used to ensure thread-safety when accessing and modifying the + * {@link #mCurrentDeviceState} field. It is acquired by both the binder thread (if + * {@link Flags#wlinfoOncreate()} is enabled) and the main thread (if + * {@link Flags#wlinfoOncreate()} is disabled) to prevent race conditions and + * ensure data consistency. + */ + private final Object mCurrentDeviceStateLock = new Object(); @NonNull private final RawFoldingFeatureProducer mRawFoldSupplier; - private final boolean mIsHalfOpenedSupported; - - private final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() { + @NonNull + private final DeviceStateMapper mDeviceStateMapper; + + @VisibleForTesting + final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() { + // The GuardedBy analysis is intra-procedural, meaning it doesn’t consider the getData() + // implementation. See https://errorprone.info/bugpattern/GuardedBy for limitations. + @SuppressWarnings("GuardedBy") + @BinderThread // When Flags.wlinfoOncreate() is enabled. + @MainThread // When Flags.wlinfoOncreate() is disabled. @Override public void onDeviceStateChanged(@NonNull DeviceState state) { - mCurrentDeviceState = state; - mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer - .this::notifyFoldingFeatureChange); + synchronized (mCurrentDeviceStateLock) { + mCurrentDeviceState = state; + mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer.this + ::notifyFoldingFeatureChangeLocked); + } } }; @@ -95,41 +111,14 @@ public final class DeviceStateManagerFoldingFeatureProducer @NonNull RawFoldingFeatureProducer rawFoldSupplier, @NonNull DeviceStateManager deviceStateManager) { mRawFoldSupplier = rawFoldSupplier; - String[] deviceStatePosturePairs = context.getResources() - .getStringArray(R.array.config_device_state_postures); - mSupportedStates = deviceStateManager.getSupportedDeviceStates(); - boolean isHalfOpenedSupported = false; - for (String deviceStatePosturePair : deviceStatePosturePairs) { - String[] deviceStatePostureMapping = deviceStatePosturePair.split(":"); - if (deviceStatePostureMapping.length != 2) { - if (DEBUG) { - Log.e(TAG, "Malformed device state posture pair: " - + deviceStatePosturePair); - } - continue; - } + mDeviceStateMapper = + new DeviceStateMapper(context, deviceStateManager.getSupportedDeviceStates()); - int deviceState; - int posture; - try { - deviceState = Integer.parseInt(deviceStatePostureMapping[0]); - posture = Integer.parseInt(deviceStatePostureMapping[1]); - } catch (NumberFormatException e) { - if (DEBUG) { - Log.e(TAG, "Failed to parse device state or posture: " - + deviceStatePosturePair, - e); - } - continue; - } - isHalfOpenedSupported = isHalfOpenedSupported - || posture == CommonFoldingFeature.COMMON_STATE_HALF_OPENED; - mDeviceStateToPostureMap.put(deviceState, posture); - } - mIsHalfOpenedSupported = isHalfOpenedSupported; - if (mDeviceStateToPostureMap.size() > 0) { + if (!mDeviceStateMapper.isDeviceStateToPostureMapEmpty()) { + final Executor executor = + Flags.wlinfoOncreate() ? Runnable::run : context.getMainExecutor(); Objects.requireNonNull(deviceStateManager) - .registerCallback(context.getMainExecutor(), mDeviceStateCallback); + .registerCallback(executor, mDeviceStateCallback); } } @@ -137,50 +126,51 @@ public final class DeviceStateManagerFoldingFeatureProducer * Add a callback to mCallbacks if there is no device state. This callback will be run * once a device state is set. Otherwise,run the callback immediately. */ - private void runCallbackWhenValidState(@NonNull Consumer<List<CommonFoldingFeature>> callback, - String displayFeaturesString) { - if (isCurrentStateValid()) { - callback.accept(calculateFoldingFeature(displayFeaturesString)); + private void runCallbackWhenValidState(@NonNull DeviceState state, + @NonNull Consumer<List<CommonFoldingFeature>> callback, + @NonNull String displayFeaturesString) { + if (mDeviceStateMapper.isDeviceStateValid(state)) { + callback.accept(calculateFoldingFeature(state, displayFeaturesString)); } else { // This callback will be added to mCallbacks and removed once it runs once. - AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback = + final AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback = new AcceptOnceConsumer<>(this, callback); addDataChangedCallback(singleRunCallback); } } - /** - * Checks to find {@link DeviceStateManagerFoldingFeatureProducer#mCurrentDeviceState} in the - * {@link DeviceStateManagerFoldingFeatureProducer#mDeviceStateToPostureMap} which was - * initialized in the constructor of {@link DeviceStateManagerFoldingFeatureProducer}. - * Returns a boolean value of whether the device state is valid. - */ - private boolean isCurrentStateValid() { - // If the device state is not found in the map, indexOfKey returns a negative number. - return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState.getIdentifier()) >= 0; - } - + // The GuardedBy analysis is intra-procedural, meaning it doesn’t consider the implementation of + // addDataChangedCallback(). See https://errorprone.info/bugpattern/GuardedBy for limitations. + @SuppressWarnings("GuardedBy") @Override protected void onListenersChanged() { super.onListenersChanged(); - if (hasListeners()) { - mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange); - } else { - mCurrentDeviceState = new DeviceState( - new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER, - "INVALID").build()); - mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange); + synchronized (mCurrentDeviceStateLock) { + if (hasListeners()) { + mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChangeLocked); + } else { + mCurrentDeviceState = INVALID_DEVICE_STATE; + mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChangeLocked); + } + } + } + + @NonNull + private DeviceState getCurrentDeviceState() { + synchronized (mCurrentDeviceStateLock) { + return mCurrentDeviceState; } } @NonNull @Override public Optional<List<CommonFoldingFeature>> getCurrentData() { - Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData(); - if (!isCurrentStateValid()) { + final Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData(); + final DeviceState state = getCurrentDeviceState(); + if (!mDeviceStateMapper.isDeviceStateValid(state) || displayFeaturesString.isEmpty()) { return Optional.empty(); } else { - return displayFeaturesString.map(this::calculateFoldingFeature); + return Optional.of(calculateFoldingFeature(state, displayFeaturesString.get())); } } @@ -191,7 +181,7 @@ public final class DeviceStateManagerFoldingFeatureProducer */ @NonNull public List<CommonFoldingFeature> getFoldsWithUnknownState() { - Optional<String> optionalFoldingFeatureString = mRawFoldSupplier.getCurrentData(); + final Optional<String> optionalFoldingFeatureString = mRawFoldSupplier.getCurrentData(); if (optionalFoldingFeatureString.isPresent()) { return CommonFoldingFeature.parseListFromString( @@ -201,7 +191,6 @@ public final class DeviceStateManagerFoldingFeatureProducer return Collections.emptyList(); } - /** * Returns the list of supported {@link DisplayFoldFeatureCommon} calculated from the * {@link DeviceStateManagerFoldingFeatureProducer}. @@ -218,16 +207,16 @@ public final class DeviceStateManagerFoldingFeatureProducer return foldFeatures; } - /** * Returns {@code true} if the device supports half-opened mode, {@code false} otherwise. */ public boolean isHalfOpenedSupported() { - return mIsHalfOpenedSupported; + return mDeviceStateMapper.mIsHalfOpenedSupported; } /** * Adds the data to the storeFeaturesConsumer when the data is ready. + * * @param storeFeaturesConsumer a consumer to collect the data when it is first available. */ @Override @@ -236,38 +225,123 @@ public final class DeviceStateManagerFoldingFeatureProducer if (TextUtils.isEmpty(displayFeaturesString)) { storeFeaturesConsumer.accept(new ArrayList<>()); } else { - runCallbackWhenValidState(storeFeaturesConsumer, displayFeaturesString); + final DeviceState state = getCurrentDeviceState(); + runCallbackWhenValidState(state, storeFeaturesConsumer, displayFeaturesString); } }); } - private void notifyFoldingFeatureChange(String displayFeaturesString) { - if (!isCurrentStateValid()) { + @GuardedBy("mCurrentDeviceStateLock") + private void notifyFoldingFeatureChangeLocked(String displayFeaturesString) { + final DeviceState state = mCurrentDeviceState; + if (!mDeviceStateMapper.isDeviceStateValid(state)) { return; } if (TextUtils.isEmpty(displayFeaturesString)) { notifyDataChanged(new ArrayList<>()); } else { - notifyDataChanged(calculateFoldingFeature(displayFeaturesString)); + notifyDataChanged(calculateFoldingFeature(state, displayFeaturesString)); } } - private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) { - return parseListFromString(displayFeaturesString, currentHingeState()); + @NonNull + private List<CommonFoldingFeature> calculateFoldingFeature(@NonNull DeviceState deviceState, + @NonNull String displayFeaturesString) { + @CommonFoldingFeature.State + final int hingeState = mDeviceStateMapper.getHingeState(deviceState); + return parseListFromString(displayFeaturesString, hingeState); } - @CommonFoldingFeature.State - private int currentHingeState() { - @CommonFoldingFeature.State - int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState.getIdentifier(), - COMMON_STATE_UNKNOWN); + /** + * Internal class to map device states to corresponding postures. + * + * <p>This class encapsulates the logic for mapping device states to postures. The mapping is + * immutable after initialization to ensure thread safety. + */ + private static class DeviceStateMapper { + /** + * Emulated device state + * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to + * {@link CommonFoldingFeature.State} map. + * + * <p>This map must be immutable after initialization to ensure thread safety, as it may be + * accessed from multiple threads. Modifications should only occur during object + * construction. + */ + private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray(); + + /** + * The list of device states that are supported. + * + * <p>This list must be immutable after initialization to ensure thread safety. + */ + @NonNull + private final List<DeviceState> mSupportedStates; + + final boolean mIsHalfOpenedSupported; + + DeviceStateMapper(@NonNull Context context, @NonNull List<DeviceState> supportedStates) { + mSupportedStates = supportedStates; + + final String[] deviceStatePosturePairs = context.getResources() + .getStringArray(R.array.config_device_state_postures); + boolean isHalfOpenedSupported = false; + for (String deviceStatePosturePair : deviceStatePosturePairs) { + final String[] deviceStatePostureMapping = deviceStatePosturePair.split(":"); + if (deviceStatePostureMapping.length != 2) { + if (DEBUG) { + Log.e(TAG, "Malformed device state posture pair: " + + deviceStatePosturePair); + } + continue; + } - if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) { - posture = mDeviceStateToPostureMap.get( - DeviceStateUtil.calculateBaseStateIdentifier(mCurrentDeviceState, - mSupportedStates), COMMON_STATE_UNKNOWN); + final int deviceState; + final int posture; + try { + deviceState = Integer.parseInt(deviceStatePostureMapping[0]); + posture = Integer.parseInt(deviceStatePostureMapping[1]); + } catch (NumberFormatException e) { + if (DEBUG) { + Log.e(TAG, "Failed to parse device state or posture: " + + deviceStatePosturePair, + e); + } + continue; + } + isHalfOpenedSupported = isHalfOpenedSupported + || posture == CommonFoldingFeature.COMMON_STATE_HALF_OPENED; + mDeviceStateToPostureMap.put(deviceState, posture); + } + mIsHalfOpenedSupported = isHalfOpenedSupported; + } + + boolean isDeviceStateToPostureMapEmpty() { + return mDeviceStateToPostureMap.size() == 0; + } + + /** + * Validates if the provided deviceState exists in the {@link #mDeviceStateToPostureMap} + * which was initialized in the constructor of {@link DeviceStateMapper}. + * Returns a boolean value of whether the device state is valid. + */ + boolean isDeviceStateValid(@NonNull DeviceState deviceState) { + // If the device state is not found in the map, indexOfKey returns a negative number. + return mDeviceStateToPostureMap.indexOfKey(deviceState.getIdentifier()) >= 0; } - return posture; + @CommonFoldingFeature.State + int getHingeState(@NonNull DeviceState deviceState) { + @CommonFoldingFeature.State + final int posture = + mDeviceStateToPostureMap.get(deviceState.getIdentifier(), COMMON_STATE_UNKNOWN); + if (posture != CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) { + return posture; + } + + final int baseStateIdentifier = + DeviceStateUtil.calculateBaseStateIdentifier(deviceState, mSupportedStates); + return mDeviceStateToPostureMap.get(baseStateIdentifier, COMMON_STATE_UNKNOWN); + } } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java index bfccb29bc952..e3a1d8ac48e2 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java @@ -142,6 +142,19 @@ class BackupHelper { } } + void abortTaskContainerRebuilding(@NonNull WindowContainerTransaction wct) { + // Clean-up the legacy states in the system + for (int i = mTaskFragmentInfos.size() - 1; i >= 0; i--) { + final TaskFragmentInfo info = mTaskFragmentInfos.valueAt(i); + mPresenter.deleteTaskFragment(wct, info.getFragmentToken()); + } + mPresenter.setSavedState(new Bundle()); + + mParcelableTaskContainerDataList.clear(); + mTaskFragmentInfos.clear(); + mTaskFragmentParentInfos.clear(); + } + boolean hasPendingStateToRestore() { return !mParcelableTaskContainerDataList.isEmpty(); } @@ -196,6 +209,7 @@ class BackupHelper { mController.onTaskFragmentParentRestored(wct, taskContainer.getTaskId(), mTaskFragmentParentInfos.get(taskContainer.getTaskId())); + mTaskFragmentParentInfos.remove(taskContainer.getTaskId()); restoredAny = true; } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index db4bb0e5e75e..8345b409ae52 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -56,6 +56,7 @@ import static androidx.window.extensions.embedding.TaskFragmentContainer.Overlay import android.annotation.CallbackExecutor; import android.app.Activity; import android.app.ActivityClient; +import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.AppGlobals; @@ -280,7 +281,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen mSplitRules.clear(); mSplitRules.addAll(rules); - if (!Flags.aeBackStackRestore() || !mPresenter.isRebuildTaskContainersNeeded()) { + if (!Flags.aeBackStackRestore() || !mPresenter.isWaitingToRebuildTaskContainers()) { return; } @@ -2893,6 +2894,36 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return; } synchronized (mLock) { + if (mPresenter.isWaitingToRebuildTaskContainers()) { + Log.w(TAG, "Rebuilding aborted, clean up and restart"); + + // Retrieve the Task intent. + final int taskId = getTaskId(activity); + Intent taskIntent = null; + final ActivityManager am = activity.getSystemService(ActivityManager.class); + final List<ActivityManager.AppTask> appTasks = am.getAppTasks(); + for (ActivityManager.AppTask appTask : appTasks) { + if (appTask.getTaskInfo().taskId == taskId) { + taskIntent = appTask.getTaskInfo().baseIntent.cloneFilter(); + break; + } + } + + // Clean up and abort the restoration + // TODO(b/369488857): also to remove the non-organized activities in the Task? + final TransactionRecord transactionRecord = + mTransactionManager.startNewTransaction(); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); + mPresenter.abortTaskContainerRebuilding(wct); + transactionRecord.apply(false /* shouldApplyIndependently */); + + // Start the Task root activity. + if (taskIntent != null) { + activity.startActivity(taskIntent); + } + return; + } + final IBinder activityToken = activity.getActivityToken(); final IBinder initialTaskFragmentToken = getTaskFragmentTokenFromActivityClientRecord(activity); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 0c0ded9bad74..b498ee2ff438 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -187,10 +187,14 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { mBackupHelper.scheduleBackup(); } - boolean isRebuildTaskContainersNeeded() { + boolean isWaitingToRebuildTaskContainers() { return mBackupHelper.hasPendingStateToRestore(); } + void abortTaskContainerRebuilding(@NonNull WindowContainerTransaction wct) { + mBackupHelper.abortTaskContainerRebuilding(wct); + } + boolean rebuildTaskContainers(@NonNull WindowContainerTransaction wct, @NonNull Set<EmbeddingRule> rules) { return mBackupHelper.rebuildTaskContainers(wct, rules); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 74cce68f270b..b453f1d4e936 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -156,7 +156,7 @@ class TaskContainer { mSplitController = splitController; for (ParcelableTaskFragmentContainerData tfData : data.getParcelableTaskFragmentContainerDataList()) { - final TaskFragmentInfo info = taskFragmentInfoMap.get(tfData.mToken); + final TaskFragmentInfo info = taskFragmentInfoMap.remove(tfData.mToken); if (info != null && !info.isEmpty()) { final TaskFragmentContainer container = new TaskFragmentContainer(tfData, splitController, this); @@ -377,8 +377,16 @@ class TaskContainer { @Nullable TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { - return getContainer(container -> container.hasAppearedActivity(activityToken) - || container.hasPendingAppearedActivity(activityToken)); + // When the new activity is launched to the topmost TF because the source activity + // was in that TF, and the source activity is finished before resolving the new activity, + // we will try to see if the new activity match a rule with the split activities below. + // If matched, it can be reparented. + final TaskFragmentContainer taskFragmentContainer + = getContainer(container -> container.hasPendingAppearedActivity(activityToken)); + if (taskFragmentContainer != null) { + return taskFragmentContainer; + } + return getContainer(container -> container.hasAppearedActivity(activityToken)); } @Nullable diff --git a/libs/WindowManager/Jetpack/tests/unittest/Android.bp b/libs/WindowManager/Jetpack/tests/unittest/Android.bp index bd430c0e610b..09185ee203b8 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/Android.bp +++ b/libs/WindowManager/Jetpack/tests/unittest/Android.bp @@ -29,6 +29,7 @@ android_test { srcs: [ "**/*.java", + "**/*.kt", ], static_libs: [ @@ -41,6 +42,7 @@ android_test { "androidx.test.ext.junit", "flag-junit", "mockito-target-extended-minus-junit4", + "mockito-kotlin-nodeps", "truth", "testables", "platform-test-annotations", diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt new file mode 100644 index 000000000000..90887a747a6f --- /dev/null +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt @@ -0,0 +1,341 @@ +/* + * 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 androidx.window.common + +import android.content.Context +import android.content.res.Resources +import android.hardware.devicestate.DeviceState +import android.hardware.devicestate.DeviceStateManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.window.common.layout.CommonFoldingFeature +import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_FLAT +import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_HALF_OPENED +import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_NO_FOLDING_FEATURES +import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN +import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE +import androidx.window.common.layout.DisplayFoldFeatureCommon +import androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED +import androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN +import com.android.internal.R +import com.android.window.flags.Flags +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import java.util.concurrent.Executor +import java.util.function.Consumer +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.stub +import org.mockito.kotlin.verify + +/** + * Test class for [DeviceStateManagerFoldingFeatureProducer]. + * + * Build/Install/Run: + * atest WMJetpackUnitTests:DeviceStateManagerFoldingFeatureProducerTest + */ +@RunWith(AndroidJUnit4::class) +class DeviceStateManagerFoldingFeatureProducerTest { + @get:Rule + val setFlagsRule: SetFlagsRule = SetFlagsRule() + + private val mMockDeviceStateManager = mock<DeviceStateManager>() + private val mMockResources = mock<Resources> { + on { getStringArray(R.array.config_device_state_postures) } doReturn DEVICE_STATE_POSTURES + } + private val mMockContext = mock<Context> { + on { resources } doReturn mMockResources + } + private val mRawFoldSupplier = mock<RawFoldingFeatureProducer> { + on { currentData } doReturn Optional.of(DISPLAY_FEATURES) + on { getData(any<Consumer<String>>()) } doAnswer { invocation -> + val callback = invocation.getArgument(0) as Consumer<String> + callback.accept(DISPLAY_FEATURES) + } + } + + @Test + @DisableFlags(Flags.FLAG_WLINFO_ONCREATE) + fun testRegisterCallback_whenWlinfoOncreateIsDisabled_usesMainExecutor() { + DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + verify(mMockDeviceStateManager).registerCallback(eq(mMockContext.mainExecutor), any()) + } + + @Test + @EnableFlags(Flags.FLAG_WLINFO_ONCREATE) + fun testRegisterCallback_whenWlinfoOncreateIsEnabled_usesRunnableRun() { + val executorCaptor = ArgumentCaptor.forClass(Executor::class.java) + val runnable = mock<Runnable>() + + DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + verify(mMockDeviceStateManager).registerCallback(executorCaptor.capture(), any()) + executorCaptor.value.execute(runnable) + verify(runnable).run() + } + + @Test + fun testGetCurrentData_validCurrentState_returnsFoldingFeatureWithState() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED) + + val currentData = ffp.getCurrentData() + + assertThat(currentData).isPresent() + assertThat(currentData.get()).containsExactlyElementsIn(HALF_OPENED_FOLDING_FEATURES) + } + + @Test + fun testGetCurrentData_invalidCurrentState_returnsEmptyOptionalFoldingFeature() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + val currentData = ffp.getCurrentData() + + assertThat(currentData).isEmpty() + } + + @Test + fun testGetFoldsWithUnknownState_validFoldingFeature_returnsFoldingFeaturesWithUnknownState() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + val result = ffp.getFoldsWithUnknownState() + + assertThat(result).containsExactlyElementsIn(UNKNOWN_STATE_FOLDING_FEATURES) + } + + @Test + fun testGetFoldsWithUnknownState_emptyFoldingFeature_returnsEmptyList() { + mRawFoldSupplier.stub { + on { currentData } doReturn Optional.empty() + } + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + val result = ffp.getFoldsWithUnknownState() + + assertThat(result).isEmpty() + } + + @Test + fun testGetDisplayFeatures_validFoldingFeature_returnsDisplayFoldFeatures() { + mRawFoldSupplier.stub { + on { currentData } doReturn Optional.of(DISPLAY_FEATURES_HALF_OPENED_HINGE) + } + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + val result = ffp.displayFeatures + + assertThat(result).containsExactly( + DisplayFoldFeatureCommon( + DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN, + setOf(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED), + ), + ) + } + + @Test + fun testIsHalfOpenedSupported_withHalfOpenedPostures_returnsTrue() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + assertThat(ffp.isHalfOpenedSupported).isTrue() + } + + @Test + fun testIsHalfOpenedSupported_withEmptyPostures_returnsFalse() { + mMockResources.stub { + on { getStringArray(R.array.config_device_state_postures) } doReturn emptyArray() + } + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + assertThat(ffp.isHalfOpenedSupported).isFalse() + } + + @Test + fun testGetData_emptyDisplayFeaturesString_callsConsumerWithEmptyList() { + mRawFoldSupplier.stub { + on { getData(any<Consumer<String>>()) } doAnswer { invocation -> + val callback = invocation.getArgument(0) as Consumer<String> + callback.accept("") + } + } + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>() + + ffp.getData(storeFeaturesConsumer) + + verify(storeFeaturesConsumer).accept(emptyList()) + } + + @Test + fun testGetData_validState_callsConsumerWithFoldingFeatures() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED) + val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>() + + ffp.getData(storeFeaturesConsumer) + + verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES) + } + + @Test + fun testGetData_invalidState_addsAcceptOnceConsumerToDataChangedCallback() { + val ffp = DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>() + + ffp.getData(storeFeaturesConsumer) + + verify(storeFeaturesConsumer, never()).accept(any()) + ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED) + ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_OPENED) + verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES) + } + + @Test + fun testDeviceStateMapper_malformedDeviceStatePosturePair_skipsPair() { + val malformedDeviceStatePostures = arrayOf( + // Missing the posture. + "0", + // Empty string. + "", + // Too many elements. + "0:1:2", + ) + mMockResources.stub { + on { getStringArray(R.array.config_device_state_postures) } doReturn + malformedDeviceStatePostures + } + + DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + verify(mMockDeviceStateManager, never()).registerCallback(any(), any()) + } + + @Test + fun testDeviceStateMapper_invalidNumberFormat_skipsPair() { + val invalidNumberFormatDeviceStatePostures = arrayOf("a:1", "0:b", "a:b", ":1") + mMockResources.stub { + on { getStringArray(R.array.config_device_state_postures) } doReturn + invalidNumberFormatDeviceStatePostures + } + + DeviceStateManagerFoldingFeatureProducer( + mMockContext, + mRawFoldSupplier, + mMockDeviceStateManager, + ) + + verify(mMockDeviceStateManager, never()).registerCallback(any(), any()) + } + + companion object { + // Supported device states configuration. + private enum class SupportedDeviceStates { + CLOSED, HALF_OPENED, OPENED, REAR_DISPLAY, CONCURRENT; + + override fun toString() = ordinal.toString() + + fun toDeviceState(): DeviceState = + DeviceState(DeviceState.Configuration.Builder(ordinal, name).build()) + } + + // Map of supported device states supplied by DeviceStateManager to WM Jetpack posture. + private val DEVICE_STATE_POSTURES = + arrayOf( + "${SupportedDeviceStates.CLOSED}:$COMMON_STATE_NO_FOLDING_FEATURES", + "${SupportedDeviceStates.HALF_OPENED}:$COMMON_STATE_HALF_OPENED", + "${SupportedDeviceStates.OPENED}:$COMMON_STATE_FLAT", + "${SupportedDeviceStates.REAR_DISPLAY}:$COMMON_STATE_NO_FOLDING_FEATURES", + "${SupportedDeviceStates.CONCURRENT}:$COMMON_STATE_USE_BASE_STATE", + ) + private val DEVICE_STATE_HALF_OPENED = SupportedDeviceStates.HALF_OPENED.toDeviceState() + private val DEVICE_STATE_OPENED = SupportedDeviceStates.OPENED.toDeviceState() + + // WindowsManager Jetpack display features. + private val DISPLAY_FEATURES = "fold-[1104,0,1104,1848]" + private val DISPLAY_FEATURES_HALF_OPENED_HINGE = "$DISPLAY_FEATURES-half-opened" + private val HALF_OPENED_FOLDING_FEATURES = CommonFoldingFeature.parseListFromString( + DISPLAY_FEATURES, + COMMON_STATE_HALF_OPENED, + ) + private val UNKNOWN_STATE_FOLDING_FEATURES = CommonFoldingFeature.parseListFromString( + DISPLAY_FEATURES, + COMMON_STATE_UNKNOWN, + ) + } +} diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java index 7fab371cb790..bc4916a607a3 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -535,7 +535,8 @@ public class TaskFragmentContainerTest { // container1. container2.setInfo(mTransaction, mInfo); - assertTrue(container2.hasActivity(mActivity.getActivityToken())); + assertTrue(container1.hasActivity(mActivity.getActivityToken())); + assertFalse(container2.hasActivity(mActivity.getActivityToken())); // When the pending appeared record is removed from container1, we respect the appeared // record in container2. container1.removePendingAppearedActivity(mActivity.getActivityToken()); diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 63a288079401..cf0a975b6c30 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -156,6 +156,13 @@ flag { } flag { + name: "enable_flexible_two_app_split" + namespace: "multitasking" + description: "Enables only 2 app 90:10 split" + bug: "349828130" +} + +flag { name: "enable_flexible_split" namespace: "multitasking" description: "Enables flexibile split feature for split screen" diff --git a/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml b/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml new file mode 100644 index 000000000000..07e5ac1a604b --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/app_handle_education_tooltip_icon.xml @@ -0,0 +1,27 @@ +<?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="24dp" + android:height="24dp" + android:tint="?android:attr/textColorTertiary" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="@android:color/system_on_tertiary_fixed" + android:pathData="M419,880Q391,880 366.5,868Q342,856 325,834L107,557L126,537Q146,516 174,512Q202,508 226,523L300,568L300,240Q300,223 311.5,211.5Q323,200 340,200Q357,200 369,211.5Q381,223 381,240L381,712L284,652L388,785Q394,792 402,796Q410,800 419,800L640,800Q673,800 696.5,776.5Q720,753 720,720L720,560Q720,543 708.5,531.5Q697,520 680,520L461,520L461,440L680,440Q730,440 765,475Q800,510 800,560L800,720Q800,786 753,833Q706,880 640,880L419,880ZM167,340Q154,318 147,292.5Q140,267 140,240Q140,157 198.5,98.5Q257,40 340,40Q423,40 481.5,98.5Q540,157 540,240Q540,267 533,292.5Q526,318 513,340L444,300Q452,286 456,271.5Q460,257 460,240Q460,190 425,155Q390,120 340,120Q290,120 255,155Q220,190 220,240Q220,257 224,271.5Q228,286 236,300L167,340ZM502,620L502,620L502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620Q502,620 502,620Q502,620 502,620L502,620L502,620Z" /> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.xml new file mode 100644 index 000000000000..a12a74658953 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_background.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. + --> + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <shape android:shape="rectangle"> + <corners android:radius="30dp" /> + <solid android:color="@android:color/system_tertiary_fixed" /> + </shape> + </item> +</layer-list> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_left_arrow.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_left_arrow.xml new file mode 100644 index 000000000000..aadffb5a0003 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_left_arrow.xml @@ -0,0 +1,27 @@ +<?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. + --> + +<!-- An arrow that points towards left. --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="10dp" + android:height="12dp" + android:viewportWidth="10" + android:viewportHeight="12"> + <path + android:pathData="M2.858,4.285C1.564,5.062 1.564,6.938 2.858,7.715L10,12L10,0L2.858,4.285Z" + android:fillColor="@android:color/system_tertiary_fixed"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_top_arrow.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_top_arrow.xml new file mode 100644 index 000000000000..e3c9a662671e --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_tooltip_top_arrow.xml @@ -0,0 +1,26 @@ +<!-- + ~ 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. + --> + +<!-- An arrow that points upwards. --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="12dp" + android:height="9dp" + android:viewportWidth="12" + android:viewportHeight="9"> + <path + android:pathData="M7.715,1.858C6.938,0.564 5.062,0.564 4.285,1.858L0,9L12,9L7.715,1.858Z" + android:fillColor="@android:color/system_tertiary_fixed"/> +</vector> diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.xml new file mode 100644 index 000000000000..a269b9ee1dd5 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_left_arrow_tooltip.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:layout_width="wrap_content" + android:layout_height="wrap_content" + android:elevation="1dp" + android:orientation="horizontal"> + + <!-- ImageView for the arrow icon, positioned horizontally at the start of the tooltip + container. --> + <ImageView + android:id="@+id/arrow_icon" + android:layout_width="10dp" + android:layout_height="12dp" + android:layout_gravity="center_vertical" + android:src="@drawable/desktop_windowing_education_tooltip_left_arrow" /> + + <!-- Layout for the tooltip, excluding the arrow. Separating the tooltip content from the arrow + allows scaling of only the tooltip container when the content changes, without affecting the + arrow. --> + <include layout="@layout/desktop_windowing_education_tooltip_container" /> +</LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml new file mode 100644 index 000000000000..bdee8836dc2e --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_tooltip_container.xml @@ -0,0 +1,43 @@ +<?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/tooltip_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@drawable/desktop_windowing_education_tooltip_background" + android:orientation="horizontal" + android:padding="@dimen/desktop_windowing_education_tooltip_padding"> + + <ImageView + android:id="@+id/tooltip_icon" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_gravity="center_vertical" + android:src="@drawable/app_handle_education_tooltip_icon" /> + + <TextView + android:id="@+id/tooltip_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_marginStart="2dp" + android:lineHeight="20dp" + android:maxWidth="150dp" + android:textColor="@android:color/system_on_tertiary_fixed" + android:textFontWeight="500" + android:textSize="14sp" /> +</LinearLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml new file mode 100644 index 000000000000..c73c1dad0e18 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_top_arrow_tooltip.xml @@ -0,0 +1,35 @@ +<?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:layout_width="wrap_content" + android:layout_height="wrap_content" + android:elevation="1dp" + android:orientation="vertical"> + + <!-- ImageView for the arrow icon, positioned vertically above the tooltip container. --> + <ImageView + android:id="@+id/arrow_icon" + android:layout_width="12dp" + android:layout_height="9dp" + android:layout_gravity="center_horizontal" + android:src="@drawable/desktop_windowing_education_tooltip_top_arrow" /> + + <!-- Layout for the tooltip, excluding the arrow. Separating the tooltip content from the arrow + allows scaling of only the tooltip container when the content changes, without affecting the + arrow. --> + <include layout="@layout/desktop_windowing_education_tooltip_container" /> +</LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml index 045b975a854e..462a49ccb1eb 100644 --- a/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml +++ b/libs/WindowManager/Shell/res/layout/letterbox_restart_dialog_layout.xml @@ -99,11 +99,11 @@ </LinearLayout> - <FrameLayout + + <LinearLayout android:minHeight="@dimen/letterbox_restart_dialog_button_height" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" - style="?android:attr/buttonBarButtonStyle" android:layout_gravity="end"> <Button @@ -133,7 +133,7 @@ android:text="@string/letterbox_restart_restart" android:contentDescription="@string/letterbox_restart_restart"/> - </FrameLayout> + </LinearLayout> </LinearLayout> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 3d8718332199..c7109f5be132 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -608,6 +608,9 @@ <!-- The horizontal inset to apply to the close button's ripple drawable --> <dimen name="desktop_mode_header_close_ripple_inset_horizontal">6dp</dimen> + <!-- The padding added to all sides of windowing education tooltip --> + <dimen name="desktop_windowing_education_tooltip_padding">8dp</dimen> + <!-- The acceptable area ratio of fg icon area/bg icon area, i.e. (72 x 72) / (108 x 108) --> <item type="dimen" format="float" name="splash_icon_enlarge_foreground_threshold">0.44</item> <!-- Scaling factor applied to splash icons without provided background i.e. (192 / 160) --> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index bda56860d3ba..56f25dae3df2 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -219,6 +219,15 @@ compatibility control. [CHAR LIMIT=NONE] --> <string name="camera_compat_dismiss_button_description">No camera issues? Tap to dismiss.</string> + <!-- App handle education tooltip text for tooltip pointing to app handle --> + <string name="windowing_app_handle_education_tooltip">Tap to open the app menu</string> + + <!-- App handle education tooltip text for tooltip pointing to windowing image button --> + <string name="windowing_desktop_mode_image_button_education_tooltip">Tap to show multiple apps together</string> + + <!-- App handle education tooltip text for tooltip pointing to app chip --> + <string name="windowing_desktop_mode_exit_education_tooltip">Return to fullscreen from the app menu</string> + <!-- The title of the letterbox education dialog. [CHAR LIMIT=NONE] --> <string name="letterbox_education_dialog_title">See and do more</string> @@ -307,12 +316,11 @@ <!-- Maximize menu snap buttons string. --> <string name="desktop_mode_maximize_menu_snap_text">Snap Screen</string> <!-- Snap resizing non-resizable string. --> - <string name="desktop_mode_non_resizable_snap_text">This app can\'t be resized</string> + <string name="desktop_mode_non_resizable_snap_text">App can\'t be moved here</string> <!-- Accessibility text for the Maximize Menu's maximize button [CHAR LIMIT=NONE] --> <string name="desktop_mode_maximize_menu_maximize_button_text">Maximize</string> <!-- Accessibility text for the Maximize Menu's snap left button [CHAR LIMIT=NONE] --> <string name="desktop_mode_maximize_menu_snap_left_button_text">Snap left</string> <!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] --> <string name="desktop_mode_maximize_menu_snap_right_button_text">Snap right</string> - </resources> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java new file mode 100644 index 000000000000..26aae2d2aa78 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared; + +import com.android.wm.shell.shared.annotations.ExternalThread; + +/** + * Listener to get focus-related transition callbacks. + */ +@ExternalThread +public interface FocusTransitionListener { + /** + * Called when a transition changes the top, focused display. + */ + void onFocusedDisplayChanged(int displayId); +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl new file mode 100644 index 000000000000..b91d5b6e2769 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared; + +/** + * Listener interface that to get focus-related transition callbacks. + */ +oneway interface IFocusTransitionListener { + + /** + * Called when a transition changes the top, focused display. + */ + void onFocusedDisplayChanged(int displayId); +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl index 3256abf09116..02615a96a86c 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl @@ -20,6 +20,7 @@ import android.view.SurfaceControl; import android.window.RemoteTransition; import android.window.TransitionFilter; +import com.android.wm.shell.shared.IFocusTransitionListener; import com.android.wm.shell.shared.IHomeTransitionListener; /** @@ -59,4 +60,9 @@ interface IShellTransitions { */ oneway void registerRemoteForTakeover(in TransitionFilter filter, in RemoteTransition remoteTransition) = 6; + + /** + * Set listener that will receive callbacks about transitions involving focus switch. + */ + oneway void setFocusTransitionListener(in IFocusTransitionListener listener) = 7; } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java index 6d4ab4c1bd09..2db4311fb771 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java @@ -22,6 +22,8 @@ import android.window.TransitionFilter; import com.android.wm.shell.shared.annotations.ExternalThread; +import java.util.concurrent.Executor; + /** * Interface to manage remote transitions. */ @@ -44,4 +46,15 @@ public interface ShellTransitions { * Unregisters a remote transition for all operations. */ default void unregisterRemote(@NonNull RemoteTransition remoteTransition) {} + + /** + * Sets listener that will receive callbacks about transitions involving focus switch. + */ + default void setFocusTransitionListener(@NonNull FocusTransitionListener listener, + Executor executor) {} + + /** + * Unsets listener that will receive callbacks about transitions involving focus switch. + */ + default void unsetFocusTransitionListener(@NonNull FocusTransitionListener listener) {} } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/OWNERS b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/OWNERS new file mode 100644 index 000000000000..bfb6d4ac5849 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/OWNERS @@ -0,0 +1,4 @@ +jeremysim@google.com +winsonc@google.com +peanutbutter@google.com +shuminghao@google.com diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java index 498dc8bdd24d..7f1e4a873f64 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java @@ -66,14 +66,54 @@ public class SplitScreenConstants { public @interface SplitPosition { } - /** A snap target in the first half of the screen, where the split is roughly 30-70. */ - public static final int SNAP_TO_30_70 = 0; + /** + * A snap target for two apps, where the split is 33-66. With FLAG_ENABLE_FLEXIBLE_SPLIT, + * only used on tablets. + */ + public static final int SNAP_TO_2_33_66 = 0; + + /** A snap target for two apps, where the split is 50-50. */ + public static final int SNAP_TO_2_50_50 = 1; + + /** + * A snap target for two apps, where the split is 66-33. With FLAG_ENABLE_FLEXIBLE_SPLIT, + * only used on tablets. + */ + public static final int SNAP_TO_2_66_33 = 2; - /** The 50-50 snap target */ - public static final int SNAP_TO_50_50 = 1; + /** + * A snap target for two apps, where the split is 90-10. The "10" app extends off the screen, + * and is actually the same size as the onscreen app, but the visible portion takes up 10% of + * the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, used on phones and foldables. + */ + public static final int SNAP_TO_2_90_10 = 3; - /** A snap target in the latter half of the screen, where the split is roughly 70-30. */ - public static final int SNAP_TO_70_30 = 2; + /** + * A snap target for two apps, where the split is 10-90. The "10" app extends off the screen, + * and is actually the same size as the onscreen app, but the visible portion takes up 10% of + * the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, used on phones and foldables. + */ + public static final int SNAP_TO_2_10_90 = 4; + + /** + * A snap target for three apps, where the split is 33-33-33. With FLAG_ENABLE_FLEXIBLE_SPLIT, + * only used on tablets. + */ + public static final int SNAP_TO_3_33_33_33 = 5; + + /** + * A snap target for three apps, where the split is 45-45-10. The "10" app extends off the + * screen, and is actually the same size as the onscreen apps, but the visible portion takes + * up 10% of the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, only used on unfolded foldables. + */ + public static final int SNAP_TO_3_45_45_10 = 6; + + /** + * A snap target for three apps, where the split is 10-45-45. The "10" app extends off the + * screen, and is actually the same size as the onscreen apps, but the visible portion takes + * up 10% of the screen. With FLAG_ENABLE_FLEXIBLE_SPLIT, only used on unfolded foldables. + */ + public static final int SNAP_TO_3_10_45_45 = 7; /** * These snap targets are used for split pairs in a stable, non-transient state. They may be @@ -81,9 +121,14 @@ public class SplitScreenConstants { * {@link SnapPosition}. */ @IntDef(prefix = { "SNAP_TO_" }, value = { - SNAP_TO_30_70, - SNAP_TO_50_50, - SNAP_TO_70_30 + SNAP_TO_2_33_66, + SNAP_TO_2_50_50, + SNAP_TO_2_66_33, + SNAP_TO_2_90_10, + SNAP_TO_2_10_90, + SNAP_TO_3_33_33_33, + SNAP_TO_3_45_45_10, + SNAP_TO_3_10_45_45, }) public @interface PersistentSnapPosition {} @@ -91,9 +136,14 @@ public class SplitScreenConstants { * Checks if the snapPosition in question is a {@link PersistentSnapPosition}. */ public static boolean isPersistentSnapPosition(@SnapPosition int snapPosition) { - return snapPosition == SNAP_TO_30_70 - || snapPosition == SNAP_TO_50_50 - || snapPosition == SNAP_TO_70_30; + return snapPosition == SNAP_TO_2_33_66 + || snapPosition == SNAP_TO_2_50_50 + || snapPosition == SNAP_TO_2_66_33 + || snapPosition == SNAP_TO_2_90_10 + || snapPosition == SNAP_TO_2_10_90 + || snapPosition == SNAP_TO_3_33_33_33 + || snapPosition == SNAP_TO_3_45_45_10 + || snapPosition == SNAP_TO_3_10_45_45; } /** The divider doesn't snap to any target and is freely placeable. */ @@ -109,9 +159,14 @@ public class SplitScreenConstants { public static final int SNAP_TO_MINIMIZE = 13; @IntDef(prefix = { "SNAP_TO_" }, value = { - SNAP_TO_30_70, - SNAP_TO_50_50, - SNAP_TO_70_30, + SNAP_TO_2_33_66, + SNAP_TO_2_50_50, + SNAP_TO_2_66_33, + SNAP_TO_2_90_10, + SNAP_TO_2_10_90, + SNAP_TO_3_33_33_33, + SNAP_TO_3_45_45_10, + SNAP_TO_3_10_45_45, SNAP_TO_NONE, SNAP_TO_START_AND_DISMISS, SNAP_TO_END_AND_DISMISS, 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 f7f45ae36eda..9f100facc163 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 @@ -19,9 +19,9 @@ package com.android.wm.shell.common.split; import static android.view.WindowManager.DOCKED_LEFT; import static android.view.WindowManager.DOCKED_RIGHT; -import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_30_70; -import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50; -import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_70_30; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_33_66; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_66_33; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_MINIMIZE; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE; @@ -283,10 +283,10 @@ public class DividerSnapAlgorithm { private void addNonDismissingTargets(boolean isHorizontalDivision, int topPosition, int bottomPosition, int dividerMax) { - maybeAddTarget(topPosition, topPosition - getStartInset(), SNAP_TO_30_70); + maybeAddTarget(topPosition, topPosition - getStartInset(), SNAP_TO_2_33_66); addMiddleTarget(isHorizontalDivision); maybeAddTarget(bottomPosition, - dividerMax - getEndInset() - (bottomPosition + mDividerSize), SNAP_TO_70_30); + dividerMax - getEndInset() - (bottomPosition + mDividerSize), SNAP_TO_2_66_33); } private void addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax) { @@ -332,7 +332,7 @@ public class DividerSnapAlgorithm { private void addMiddleTarget(boolean isHorizontalDivision) { int position = DockedDividerUtils.calculateMiddlePosition(isHorizontalDivision, mInsets, mDisplayWidth, mDisplayHeight, mDividerSize); - mTargets.add(new SnapTarget(position, SNAP_TO_50_50)); + mTargets.add(new SnapTarget(position, SNAP_TO_2_50_50)); } private void addMinimizedTarget(boolean isHorizontalDivision, int dockedSide) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index bec2ea58e106..4227a6e2903f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -123,6 +123,7 @@ import com.android.wm.shell.sysui.ShellInterface; import com.android.wm.shell.taskview.TaskViewFactory; import com.android.wm.shell.taskview.TaskViewFactoryController; import com.android.wm.shell.taskview.TaskViewTransitions; +import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.HomeTransitionObserver; import com.android.wm.shell.transition.MixedTransitionHandler; import com.android.wm.shell.transition.Transitions; @@ -742,14 +743,15 @@ public abstract class WMShellBaseModule { @ShellMainThread Handler mainHandler, @ShellAnimationThread ShellExecutor animExecutor, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, - HomeTransitionObserver homeTransitionObserver) { + HomeTransitionObserver homeTransitionObserver, + FocusTransitionObserver focusTransitionObserver) { if (!context.getResources().getBoolean(R.bool.config_registerShellTransitionsOnInit)) { // TODO(b/238217847): Force override shell init if registration is disabled shellInit = new ShellInit(mainExecutor); } return new Transitions(context, shellInit, shellCommandHandler, shellController, organizer, pool, displayController, mainExecutor, mainHandler, animExecutor, - rootTaskDisplayAreaOrganizer, homeTransitionObserver); + rootTaskDisplayAreaOrganizer, homeTransitionObserver, focusTransitionObserver); } @WMSingleton @@ -761,6 +763,12 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides + static FocusTransitionObserver provideFocusTransitionObserver() { + return new FocusTransitionObserver(); + } + + @WMSingleton + @Provides static TaskViewTransitions provideTaskViewTransitions(Transitions transitions) { return new TaskViewTransitions(transitions); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 759ed035895e..0e8c4e70e05d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -456,6 +456,7 @@ class DesktopModeTaskRepository ( pw.println( "${innerPrefix}freeformTasksInZOrder=${data.freeformTasksInZOrder.toDumpString()}" ) + pw.println("${innerPrefix}minimizedTasks=${data.minimizedTasks.toDumpString()}") } } 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 968f40c3df5d..afa27f9f1309 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 @@ -43,6 +43,7 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.DragEvent import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE +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 @@ -1061,7 +1062,10 @@ class DesktopTasksController( // Check if freeform task launch during recents should be handled shouldHandleMidRecentsFreeformLaunch -> handleMidRecentsFreeformTaskLaunch(task) // Check if the closing task needs to be handled - TransitionUtil.isClosingType(request.type) -> handleTaskClosing(task) + TransitionUtil.isClosingType(request.type) -> handleTaskClosing( + task, + request.type + ) // Check if the top task shouldn't be allowed to enter desktop mode isIncompatibleTask(task) -> handleIncompatibleTaskLaunch(task) // Check if fullscreen task should be updated @@ -1288,7 +1292,10 @@ class DesktopTasksController( } /** Handle task closing by removing wallpaper activity if it's the last active task */ - private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? { + private fun handleTaskClosing( + task: RunningTaskInfo, + transitionType: Int + ): WindowContainerTransaction? { logV("handleTaskClosing") if (!isDesktopModeShowing(task.displayId)) return null @@ -1301,9 +1308,10 @@ class DesktopTasksController( removeWallpaperActivity(wct) } taskRepository.addClosingTask(task.displayId, task.taskId) - // If a CLOSE or TO_BACK is triggered on a desktop task, remove the task. + // If a CLOSE is triggered on a desktop task, remove the task. if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue() && - taskRepository.isVisibleTask(task.taskId) + taskRepository.isVisibleTask(task.taskId) && + transitionType == TRANSIT_CLOSE ) { wct.removeTask(task.token) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index 0841628853a3..4796c4d0655a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -16,16 +16,19 @@ package com.android.wm.shell.desktopmode +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.Context import android.os.IBinder import android.view.SurfaceControl import android.view.WindowManager +import android.view.WindowManager.TRANSIT_TO_BACK import android.window.TransitionInfo import android.window.WindowContainerTransaction +import android.window.flags.DesktopModeFlags +import android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY import com.android.internal.protolog.ProtoLog import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE -import android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions @@ -64,6 +67,30 @@ class DesktopTasksTransitionObserver( ) { // TODO: b/332682201 Update repository state updateWallpaperToken(info) + + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { + handleBackNavigation(info) + } + } + + private fun handleBackNavigation(info: TransitionInfo) { + // When default back navigation happens, transition type is TO_BACK and the change is + // TO_BACK. Mark the task going to back as minimized. + if (info.type == TRANSIT_TO_BACK) { + for (change in info.changes) { + val taskInfo = change.taskInfo + if (taskInfo == null || taskInfo.taskId == -1) { + continue + } + + if (desktopModeTaskRepository.getVisibleTaskCount(taskInfo.displayId) > 0 && + change.mode == TRANSIT_TO_BACK && + taskInfo.windowingMode == WINDOWING_MODE_FREEFORM + ) { + desktopModeTaskRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId) + } + } + } } override fun onTransitionStarting(transition: IBinder) { 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 2138acc51eb2..cbb08b804dfe 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 @@ -1344,6 +1344,9 @@ public class PipTransition extends PipTransitionController { final SurfaceControl leash = pipChange.getLeash(); final Rect destBounds = mPipOrganizer.getCurrentOrAnimatingBounds(); final boolean isInPip = mPipTransitionState.isInPip(); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Update pip for unhandled transition, change=%s, destBounds=%s, isInPip=%b", + TAG, pipChange, destBounds, isInPip); mSurfaceTransactionHelper .crop(startTransaction, leash, destBounds) .round(startTransaction, leash, isInPip) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index d3bed59f7994..a2439a937512 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -361,8 +361,11 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final int anim = getRotationAnimationHint(change, info, mDisplayController); isSeamlessDisplayChange = anim == ROTATION_ANIMATION_SEAMLESS; if (!(isSeamlessDisplayChange || anim == ROTATION_ANIMATION_JUMPCUT)) { - startRotationAnimation(startTransaction, change, info, anim, animations, - onAnimFinish); + final int flags = wallpaperTransit != WALLPAPER_TRANSITION_NONE + && Flags.commonSurfaceAnimator() + ? ScreenRotationAnimation.FLAG_HAS_WALLPAPER : 0; + startRotationAnimation(startTransaction, change, info, anim, flags, + animations, onAnimFinish); isDisplayRotationAnimationStarted = true; continue; } @@ -414,7 +417,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { if (change.getParent() == null && !change.hasFlags(FLAG_IS_DISPLAY) && change.getStartRotation() != change.getEndRotation()) { startRotationAnimation(startTransaction, change, info, - ROTATION_ANIMATION_ROTATE, animations, onAnimFinish); + ROTATION_ANIMATION_ROTATE, 0 /* flags */, animations, onAnimFinish); continue; } } @@ -699,12 +702,12 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } private void startRotationAnimation(SurfaceControl.Transaction startTransaction, - TransitionInfo.Change change, TransitionInfo info, int animHint, + TransitionInfo.Change change, TransitionInfo info, int animHint, int flags, ArrayList<Animator> animations, Runnable onAnimFinish) { final int rootIdx = TransitionUtil.rootIndexFor(change, info); final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext, mTransactionPool, startTransaction, change, info.getRoot(rootIdx).getLeash(), - animHint); + animHint, flags); // The rotation animation may consist of 3 animations: fade-out screenshot, fade-in real // content, and background color. The item of "animGroup" will be removed if the sub // animation is finished. Then if the list becomes empty, the rotation animation is done. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java new file mode 100644 index 000000000000..2f5059f3161c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java @@ -0,0 +1,142 @@ +/* + * 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.transition; + +import static android.view.Display.INVALID_DISPLAY; +import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; + +import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions; +import static com.android.wm.shell.transition.Transitions.TransitionObserver; + +import android.annotation.NonNull; +import android.app.ActivityManager.RunningTaskInfo; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; +import android.view.SurfaceControl; +import android.window.TransitionInfo; + +import com.android.wm.shell.shared.FocusTransitionListener; +import com.android.wm.shell.shared.IFocusTransitionListener; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * The {@link TransitionObserver} that observes for transitions involving focus switch. + * It reports transitions to callers outside of the process via {@link IFocusTransitionListener}, + * and callers within the process via {@link FocusTransitionListener}. + */ +public class FocusTransitionObserver implements TransitionObserver { + private static final String TAG = FocusTransitionObserver.class.getSimpleName(); + + private IFocusTransitionListener mRemoteListener; + private final Map<FocusTransitionListener, Executor> mLocalListeners = + new HashMap<>(); + + private int mFocusedDisplayId = INVALID_DISPLAY; + + public FocusTransitionObserver() {} + + @Override + public void onTransitionReady(@NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction) { + final List<TransitionInfo.Change> changes = info.getChanges(); + for (int i = changes.size() - 1; i >= 0; i--) { + final TransitionInfo.Change change = changes.get(i); + final RunningTaskInfo task = change.getTaskInfo(); + if (task != null && task.isFocused && change.hasFlags(FLAG_MOVED_TO_TOP)) { + if (mFocusedDisplayId != task.displayId) { + mFocusedDisplayId = task.displayId; + notifyFocusedDisplayChanged(); + } + return; + } + } + } + + @Override + public void onTransitionStarting(@NonNull IBinder transition) {} + + @Override + public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {} + + @Override + public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {} + + /** + * Sets the focus transition listener that receives any transitions resulting in focus switch. + * This is for calls from outside the Shell, within the host process. + * + */ + public void setLocalFocusTransitionListener(FocusTransitionListener listener, + Executor executor) { + if (!enableDisplayFocusInShellTransitions()) { + return; + } + mLocalListeners.put(listener, executor); + executor.execute(() -> listener.onFocusedDisplayChanged(mFocusedDisplayId)); + } + + /** + * Sets the focus transition listener that receives any transitions resulting in focus switch. + * This is for calls from outside the Shell, within the host process. + * + */ + public void unsetLocalFocusTransitionListener(FocusTransitionListener listener) { + if (!enableDisplayFocusInShellTransitions()) { + return; + } + mLocalListeners.remove(listener); + } + + /** + * Sets the focus transition listener that receives any transitions resulting in focus switch. + * This is for calls from outside the host process. + */ + public void setRemoteFocusTransitionListener(Transitions transitions, + IFocusTransitionListener listener) { + if (!enableDisplayFocusInShellTransitions()) { + return; + } + mRemoteListener = listener; + notifyFocusedDisplayChangedToRemote(); + } + + /** + * Notifies the listener that display focus has changed. + */ + public void notifyFocusedDisplayChanged() { + notifyFocusedDisplayChangedToRemote(); + mLocalListeners.forEach((listener, executor) -> + executor.execute(() -> listener.onFocusedDisplayChanged(mFocusedDisplayId))); + } + + private void notifyFocusedDisplayChangedToRemote() { + if (mRemoteListener != null) { + try { + mRemoteListener.onFocusedDisplayChanged(mFocusedDisplayId); + } catch (RemoteException e) { + Slog.w(TAG, "Failed call notifyFocusedDisplayChangedToRemote", e); + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java index 5802e2ca8133..1a04997fa384 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java @@ -25,12 +25,9 @@ import static com.android.wm.shell.transition.DefaultTransitionHandler.buildSurf import static com.android.wm.shell.transition.Transitions.TAG; import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.content.Context; -import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.hardware.HardwareBuffer; @@ -38,6 +35,7 @@ import android.util.Slog; import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; +import android.view.animation.AccelerateInterpolator; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.window.ScreenCapture; @@ -74,6 +72,7 @@ import java.util.ArrayList; */ class ScreenRotationAnimation { static final int MAX_ANIMATION_DURATION = 10 * 1000; + static final int FLAG_HAS_WALLPAPER = 1; private final Context mContext; private final TransactionPool mTransactionPool; @@ -98,6 +97,12 @@ class ScreenRotationAnimation { private SurfaceControl mBackColorSurface; /** The leash using to animate screenshot layer. */ private final SurfaceControl mAnimLeash; + /** + * The container with background color for {@link #mSurfaceControl}. It is only created if + * {@link #mSurfaceControl} may be translucent. E.g. visible wallpaper with alpha < 1 (dimmed). + * That prevents flickering of alpha blending. + */ + private SurfaceControl mBackEffectSurface; // The current active animation to move from the old to the new rotated // state. Which animation is run here will depend on the old and new @@ -111,8 +116,8 @@ class ScreenRotationAnimation { /** Intensity of light/whiteness of the layout after rotation occurs. */ private float mEndLuma; - ScreenRotationAnimation(Context context, TransactionPool pool, - Transaction t, TransitionInfo.Change change, SurfaceControl rootLeash, int animHint) { + ScreenRotationAnimation(Context context, TransactionPool pool, Transaction t, + TransitionInfo.Change change, SurfaceControl rootLeash, int animHint, int flags) { mContext = context; mTransactionPool = pool; mAnimHint = animHint; @@ -170,11 +175,20 @@ class ScreenRotationAnimation { } hardwareBuffer.close(); } + if ((flags & FLAG_HAS_WALLPAPER) != 0) { + mBackEffectSurface = new SurfaceControl.Builder() + .setCallsite("ShellRotationAnimation").setParent(rootLeash) + .setEffectLayer().setOpaque(true).setName("BackEffect").build(); + t.reparent(mSurfaceControl, mBackEffectSurface) + .setColor(mBackEffectSurface, + new float[] {mStartLuma, mStartLuma, mStartLuma}) + .show(mBackEffectSurface); + } t.setLayer(mAnimLeash, SCREEN_FREEZE_LAYER_BASE); t.show(mAnimLeash); // Crop the real content in case it contains a larger child layer, e.g. wallpaper. - t.setCrop(mSurfaceControl, new Rect(0, 0, mEndWidth, mEndHeight)); + t.setCrop(getEnterSurface(), new Rect(0, 0, mEndWidth, mEndHeight)); if (!isCustomRotate()) { mBackColorSurface = new SurfaceControl.Builder() @@ -202,6 +216,11 @@ class ScreenRotationAnimation { return mAnimHint == ROTATION_ANIMATION_CROSSFADE || mAnimHint == ROTATION_ANIMATION_JUMPCUT; } + /** Returns the surface which contains the real content to animate enter. */ + private SurfaceControl getEnterSurface() { + return mBackEffectSurface != null ? mBackEffectSurface : mSurfaceControl; + } + private void setScreenshotTransform(SurfaceControl.Transaction t) { if (mScreenshotLayer == null) { return; @@ -314,7 +333,11 @@ class ScreenRotationAnimation { } else { startDisplayRotation(animations, finishCallback, mainExecutor); startScreenshotRotationAnimation(animations, finishCallback, mainExecutor); - //startColorAnimation(mTransaction, animationScale); + if (mBackEffectSurface != null && mStartLuma > 0.1f) { + // Animate from the color of background to black for smooth alpha blending. + buildLumaAnimation(animations, mStartLuma, 0f /* endLuma */, mBackEffectSurface, + animationScale, finishCallback, mainExecutor); + } } return true; @@ -322,7 +345,7 @@ class ScreenRotationAnimation { private void startDisplayRotation(@NonNull ArrayList<Animator> animations, @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { - buildSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback, + buildSurfaceAnimation(animations, mRotateEnterAnimation, getEnterSurface(), finishCallback, mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */, null /* clipRect */, false /* isActivity */); } @@ -341,40 +364,17 @@ class ScreenRotationAnimation { null /* clipRect */, false /* isActivity */); } - private void startColorAnimation(float animationScale, @NonNull ShellExecutor animExecutor) { - int colorTransitionMs = mContext.getResources().getInteger( - R.integer.config_screen_rotation_color_transition); - final float[] rgbTmpFloat = new float[3]; - final int startColor = Color.rgb(mStartLuma, mStartLuma, mStartLuma); - final int endColor = Color.rgb(mEndLuma, mEndLuma, mEndLuma); - final long duration = colorTransitionMs * (long) animationScale; - final Transaction t = mTransactionPool.acquire(); - - final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); - // Animation length is already expected to be scaled. - va.overrideDurationScale(1.0f); - va.setDuration(duration); - va.addUpdateListener(animation -> { - final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime()); - final float fraction = currentPlayTime / va.getDuration(); - applyColor(startColor, endColor, rgbTmpFloat, fraction, mBackColorSurface, t); - }); - va.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationCancel(Animator animation) { - applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface, - t); - mTransactionPool.release(t); - } - - @Override - public void onAnimationEnd(Animator animation) { - applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface, - t); - mTransactionPool.release(t); - } - }); - animExecutor.execute(va::start); + private void buildLumaAnimation(@NonNull ArrayList<Animator> animations, + float startLuma, float endLuma, SurfaceControl surface, float animationScale, + @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { + final long durationMillis = (long) (mContext.getResources().getInteger( + R.integer.config_screen_rotation_color_transition) * animationScale); + final LumaAnimation animation = new LumaAnimation(durationMillis); + // Align the end with the enter animation. + animation.setStartOffset(mRotateEnterAnimation.getDuration() - durationMillis); + final LumaAnimationAdapter adapter = new LumaAnimationAdapter(surface, startLuma, endLuma); + DefaultSurfaceAnimator.buildSurfaceAnimation(animations, animation, finishCallback, + mTransactionPool, mainExecutor, adapter); } public void kill() { @@ -389,21 +389,47 @@ class ScreenRotationAnimation { if (mBackColorSurface != null && mBackColorSurface.isValid()) { t.remove(mBackColorSurface); } + if (mBackEffectSurface != null && mBackEffectSurface.isValid()) { + t.remove(mBackEffectSurface); + } t.apply(); mTransactionPool.release(t); } - private static void applyColor(int startColor, int endColor, float[] rgbFloat, - float fraction, SurfaceControl surface, SurfaceControl.Transaction t) { - final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor, - endColor); - Color middleColor = Color.valueOf(color); - rgbFloat[0] = middleColor.red(); - rgbFloat[1] = middleColor.green(); - rgbFloat[2] = middleColor.blue(); - if (surface.isValid()) { - t.setColor(surface, rgbFloat); + /** A no-op wrapper to provide animation duration. */ + private static class LumaAnimation extends Animation { + LumaAnimation(long durationMillis) { + setDuration(durationMillis); + } + } + + private static class LumaAnimationAdapter extends DefaultSurfaceAnimator.AnimationAdapter { + final float[] mColorArray = new float[3]; + final float mStartLuma; + final float mEndLuma; + final AccelerateInterpolator mInterpolation; + + LumaAnimationAdapter(@NonNull SurfaceControl leash, float startLuma, float endLuma) { + super(leash); + mStartLuma = startLuma; + mEndLuma = endLuma; + // Make the initial progress color lighter if the background is light. That avoids + // darker content when fading into the entering surface. + final float factor = Math.min(3f, (Math.max(0.5f, mStartLuma) - 0.5f) * 10); + Slog.d(TAG, "Luma=" + mStartLuma + " factor=" + factor); + mInterpolation = factor > 0.5f ? new AccelerateInterpolator(factor) : null; + } + + @Override + void applyTransformation(ValueAnimator animator, long currentPlayTime) { + final float fraction = mInterpolation != null + ? mInterpolation.getInterpolation(animator.getAnimatedFraction()) + : animator.getAnimatedFraction(); + final float luma = mStartLuma + fraction * (mEndLuma - mStartLuma); + mColorArray[0] = luma; + mColorArray[1] = luma; + mColorArray[2] = luma; + mTransaction.setColor(mLeash, mColorArray); } - t.apply(); } } 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 d03832d3e85e..d280dcd252b4 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 @@ -87,6 +87,8 @@ import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.FocusTransitionListener; +import com.android.wm.shell.shared.IFocusTransitionListener; import com.android.wm.shell.shared.IHomeTransitionListener; import com.android.wm.shell.shared.IShellTransitions; import com.android.wm.shell.shared.ShellTransitions; @@ -103,6 +105,7 @@ import com.android.wm.shell.transition.tracing.TransitionTracer; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.concurrent.Executor; /** * Plays transition animations. Within this player, each transition has a lifecycle. @@ -224,6 +227,7 @@ public class Transitions implements RemoteCallable<Transitions>, private final ArrayList<TransitionObserver> mObservers = new ArrayList<>(); private HomeTransitionObserver mHomeTransitionObserver; + private FocusTransitionObserver mFocusTransitionObserver; /** List of {@link Runnable} instances to run when the last active transition has finished. */ private final ArrayList<Runnable> mRunWhenIdleQueue = new ArrayList<>(); @@ -309,10 +313,12 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor, - @NonNull HomeTransitionObserver observer) { + @NonNull HomeTransitionObserver homeTransitionObserver, + @NonNull FocusTransitionObserver focusTransitionObserver) { this(context, shellInit, new ShellCommandHandler(), shellController, organizer, pool, displayController, mainExecutor, mainHandler, animExecutor, - new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit), observer); + new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit), + homeTransitionObserver, focusTransitionObserver); } public Transitions(@NonNull Context context, @@ -326,7 +332,8 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor, @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer, - @NonNull HomeTransitionObserver observer) { + @NonNull HomeTransitionObserver homeTransitionObserver, + @NonNull FocusTransitionObserver focusTransitionObserver) { mOrganizer = organizer; mContext = context; mMainExecutor = mainExecutor; @@ -345,7 +352,8 @@ public class Transitions implements RemoteCallable<Transitions>, mHandlers.add(mRemoteTransitionHandler); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote"); shellInit.addInitCallback(this::onInit, this); - mHomeTransitionObserver = observer; + mHomeTransitionObserver = homeTransitionObserver; + mFocusTransitionObserver = focusTransitionObserver; if (android.tracing.Flags.perfettoTransitionTracing()) { mTransitionTracer = new PerfettoTransitionTracer(); @@ -384,6 +392,8 @@ public class Transitions implements RemoteCallable<Transitions>, mShellCommandHandler.addCommandCallback("transitions", this, this); mShellCommandHandler.addDumpCallback(this::dump, this); + + registerObserver(mFocusTransitionObserver); } public boolean isRegistered() { @@ -1573,6 +1583,21 @@ public class Transitions implements RemoteCallable<Transitions>, mMainExecutor.execute( () -> mRemoteTransitionHandler.removeFiltered(remoteTransition)); } + + @Override + public void setFocusTransitionListener(FocusTransitionListener listener, + Executor executor) { + mMainExecutor.execute(() -> + mFocusTransitionObserver.setLocalFocusTransitionListener(listener, executor)); + + } + + @Override + public void unsetFocusTransitionListener(FocusTransitionListener listener) { + mMainExecutor.execute(() -> + mFocusTransitionObserver.unsetLocalFocusTransitionListener(listener)); + + } } /** @@ -1634,6 +1659,15 @@ public class Transitions implements RemoteCallable<Transitions>, } @Override + public void setFocusTransitionListener(IFocusTransitionListener listener) { + executeRemoteCallWithTaskPermission(mTransitions, "setFocusTransitionListener", + (transitions) -> { + transitions.mFocusTransitionObserver.setRemoteFocusTransitionListener( + transitions, listener); + }); + } + + @Override public SurfaceControl getHomeTaskOverlayContainer() { SurfaceControl[] result = new SurfaceControl[1]; executeRemoteCallWithTaskPermission(mTransitions, "getHomeTaskOverlayContainer", 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 226b0fb2e1a1..1be26f080ac8 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 @@ -107,4 +107,27 @@ class AdditionalSystemViewContainer( } windowManagerWrapper.updateViewLayout(view, lp) } + + class Factory { + fun create( + windowManagerWrapper: WindowManagerWrapper, + taskId: Int, + x: Int, + y: Int, + width: Int, + height: Int, + flags: Int, + view: View, + ): AdditionalSystemViewContainer = + AdditionalSystemViewContainer( + windowManagerWrapper = windowManagerWrapper, + taskId = taskId, + x = x, + y = y, + width = width, + height = height, + flags = flags, + view = view + ) + } } 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 new file mode 100644 index 000000000000..98413ee96133 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt @@ -0,0 +1,249 @@ +/* + * 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.DimenRes +import android.annotation.LayoutRes +import android.content.Context +import android.content.res.Resources +import android.graphics.Point +import android.util.Size +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.View.MeasureSpec.UNSPECIFIED +import android.view.WindowManager +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.SpringForce +import com.android.wm.shell.R +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 tooltip, including showing and hiding it. Ensures that + * only one tooltip is displayed at a time. + */ +class DesktopWindowingEducationTooltipController( + private val context: Context, + private val additionalSystemViewContainerFactory: AdditionalSystemViewContainer.Factory, +) { + // TODO: b/369384567 - Set tooltip color scheme to match LT/DT of app theme + private var tooltipView: 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 + + /** + * Shows education tooltip. + * + * @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) { + hideEducationTooltip() + tooltipView = createEducationTooltipView(tooltipViewConfig, taskId) + animator = createAnimator() + animateShowTooltipTransition() + } + + /** Hide the current education view if visible */ + private fun hideEducationTooltip() = animateHideTooltipTransition { cleanUp() } + + /** Create education view by inflating layout provided. */ + private fun createEducationTooltipView( + tooltipViewConfig: EducationViewConfig, + taskId: Int, + ): View { + val tooltipView = + LayoutInflater.from(context) + .inflate( + tooltipViewConfig.tooltipViewLayout, /* root= */ null, /* attachToRoot= */ false) + .apply { + alpha = 0f + scaleX = 0f + scaleY = 0f + + requireViewById<TextView>(R.id.tooltip_text).apply { + text = tooltipViewConfig.tooltipText + } + + setOnTouchListener { _, motionEvent -> + if (motionEvent.action == MotionEvent.ACTION_OUTSIDE) { + hideEducationTooltip() + tooltipViewConfig.onDismissAction() + true + } else { + false + } + } + setOnClickListener { + hideEducationTooltip() + tooltipViewConfig.onEducationClickAction() + } + } + + val tooltipDimens = tooltipDimens(tooltipView = tooltipView, tooltipViewConfig.arrowDirection) + val tooltipViewGlobalCoordinates = + tooltipViewGlobalCoordinates( + tooltipViewGlobalCoordinates = tooltipViewConfig.tooltipViewGlobalCoordinates, + arrowDirection = tooltipViewConfig.arrowDirection, + tooltipDimen = tooltipDimens) + createTooltipPopupWindow( + taskId, tooltipViewGlobalCoordinates, tooltipDimens, tooltipView = tooltipView) + + return tooltipView + } + + /** Create animator for education transitions */ + private fun createAnimator(): PhysicsAnimator<View>? = + tooltipView?.let { + PhysicsAnimator.getInstance(it).apply { setDefaultSpringConfig(springConfig) } + } + + /** Animate show transition for the education view */ + private fun animateShowTooltipTransition() { + 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 animateHideTooltipTransition(endActions: () -> Unit) { + animator + ?.spring(DynamicAnimation.ALPHA, 0f) + ?.spring(DynamicAnimation.SCALE_X, 0f) + ?.spring(DynamicAnimation.SCALE_Y, 0f) + ?.start() + endActions() + } + + /** Remove education tooltip and clean up all relative properties */ + private fun cleanUp() { + tooltipView = null + animator = null + popupWindow?.releaseView() + popupWindow = null + } + + private fun createTooltipPopupWindow( + taskId: Int, + tooltipViewGlobalCoordinates: Point, + tooltipDimen: Size, + tooltipView: View, + ) { + popupWindow = + additionalSystemViewContainerFactory.create( + windowManagerWrapper = + WindowManagerWrapper(context.getSystemService(WindowManager::class.java)), + taskId = taskId, + x = tooltipViewGlobalCoordinates.x, + y = tooltipViewGlobalCoordinates.y, + width = tooltipDimen.width, + height = tooltipDimen.height, + flags = + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, + view = tooltipView) + } + + private fun tooltipViewGlobalCoordinates( + tooltipViewGlobalCoordinates: Point, + arrowDirection: TooltipArrowDirection, + tooltipDimen: Size, + ): Point { + var tooltipX = tooltipViewGlobalCoordinates.x + var tooltipY = tooltipViewGlobalCoordinates.y + + // Current values of [tooltipX]/[tooltipY] are the coordinates of tip of the arrow. + // Parameter x and y passed to [AdditionalSystemViewContainer] is the top left position of + // the window to be created. Hence we will need to move the coordinates left/up in order + // to position the tooltip correctly. + if (arrowDirection == TooltipArrowDirection.UP) { + // Arrow is placed at horizontal center on top edge of the tooltip. Hence decrement + // half of tooltip width from [tooltipX] to horizontally position the tooltip. + tooltipX -= tooltipDimen.width / 2 + } else { + // Arrow is placed at vertical center on the left edge of the tooltip. Hence decrement + // half of tooltip height from [tooltipY] to vertically position the tooltip. + tooltipY -= tooltipDimen.height / 2 + } + return Point(tooltipX, tooltipY) + } + + private fun tooltipDimens(tooltipView: View, arrowDirection: TooltipArrowDirection): Size { + val tooltipBackground = tooltipView.requireViewById<LinearLayout>(R.id.tooltip_container) + val arrowView = tooltipView.requireViewById<ImageView>(R.id.arrow_icon) + tooltipBackground.measure( + /* widthMeasureSpec= */ UNSPECIFIED, /* heightMeasureSpec= */ UNSPECIFIED) + arrowView.measure(/* widthMeasureSpec= */ UNSPECIFIED, /* heightMeasureSpec= */ UNSPECIFIED) + + var desiredWidth = + tooltipBackground.measuredWidth + + 2 * loadDimensionPixelSize(R.dimen.desktop_windowing_education_tooltip_padding) + var desiredHeight = + tooltipBackground.measuredHeight + + 2 * loadDimensionPixelSize(R.dimen.desktop_windowing_education_tooltip_padding) + if (arrowDirection == TooltipArrowDirection.UP) { + // desiredHeight currently does not account for the height of arrow, hence adding it. + desiredHeight += arrowView.height + } else { + // desiredWidth currently does not account for the width of arrow, hence adding it. + desiredWidth += arrowView.width + } + + return Size(desiredWidth, desiredHeight) + } + + 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 tooltipViewLayout Layout resource ID of the view to be used for education tooltip. + * @property tooltipViewGlobalCoordinates Global (screen) coordinates of the tip of the tooltip + * arrow. + * @property tooltipText Text to be added to the TextView of tooltip. + * @property arrowDirection Direction of arrow of the tooltip. + * @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( + @LayoutRes val tooltipViewLayout: Int, + val tooltipViewGlobalCoordinates: Point, + val tooltipText: String, + val arrowDirection: TooltipArrowDirection, + val onEducationClickAction: () -> Unit, + val onDismissAction: () -> Unit, + ) + + /** Direction of arrow of the tooltip */ + enum class TooltipArrowDirection { + UP, + LEFT, + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml index 40dbbac32c7f..c8df15d81345 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml index 85715db3d952..706c63244890 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml index 6c903a2e8c42..7df1675f541c 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml index 6c903a2e8c42..7df1675f541c 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml index f69a90cc793f..d87c1795cf7b 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml index b76d06565700..99969e71238a 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml index 041978c371ff..19c3e4048d69 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml index bf040d2a95f4..7505860709e9 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml @@ -24,6 +24,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="on"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="on"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java index 177e47a342f6..c52d9dd24165 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java @@ -19,7 +19,7 @@ package com.android.wm.shell.common.split; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; import static com.google.common.truth.Truth.assertThat; @@ -136,7 +136,7 @@ public class SplitLayoutTests extends ShellTestCase { @Test public void testSetDivideRatio() { mSplitLayout.setDividerPosition(200, false /* applyLayoutChange */); - mSplitLayout.setDivideRatio(SNAP_TO_50_50); + mSplitLayout.setDivideRatio(SNAP_TO_2_50_50); assertThat(mSplitLayout.getDividerPosition()).isEqualTo( mSplitLayout.mDividerSnapAlgorithm.getMiddleTarget().position); } 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 ee545209904f..94e361659090 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 @@ -40,6 +40,7 @@ import android.graphics.Point import android.graphics.PointF import android.graphics.Rect import android.os.Binder +import android.os.Bundle import android.os.Handler import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags @@ -2086,16 +2087,13 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_withBackNav_removesTask() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,) + fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_removesTask() { val task = setUpFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - assertNotNull(result, "Should handle request").assertRemoveAt(0, task.token) + assertNull(result, "Should not handle request") } @Test @@ -2137,26 +2135,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_backTransition_singleTask_withWallpaper_withBackNav_removesWallpaperAndTask() { - val task = setUpFreeformTask() - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - result.assertRemoveAt(index = 1, task.token) - } - - @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_backTransition_singleTaskWithToken_noBackNav_removesWallpaper() { + fun handleRequest_backTransition_singleTaskWithToken_removesWallpaper() { val task = setUpFreeformTask() val wallpaperToken = MockToken().token() @@ -2183,23 +2163,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_backTransition_multipleTasks_withWallpaper_withBackNav_removesTask() { - val task1 = setUpFreeformTask() - setUpFreeformTask() - - taskRepository.wallpaperActivityToken = MockToken().token() - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) - - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, task1.token) - } - - @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) fun handleRequest_backTransition_multipleTasks_noBackNav_doesNotHandle() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -2226,29 +2190,11 @@ class DesktopTasksControllerTest : ShellTestCase() { // Should create remove wallpaper transaction assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - result.assertRemoveAt(index = 1, task1.token) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_backTransition_multipleTasksSingleNonClosing_noBackNav_removesWallpaper() { - val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) } @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) fun handleRequest_backTransition_multipleTasksSingleNonMinimized_removesWallpaperAndTask() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) @@ -2261,23 +2207,6 @@ class DesktopTasksControllerTest : ShellTestCase() { // Should create remove wallpaper transaction assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - result.assertRemoveAt(index = 1, task1.token) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_backTransition_multipleTasksSingleNonMinimized_noBackNav_removesWallpaper() { - val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) } @Test @@ -2937,6 +2866,108 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun newWindow_fromFullscreenOpensInSplit() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask() + val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + runOpenNewWindow(task) + verify(splitScreenController) + .startIntent(any(), anyInt(), any(), any(), + optionsCaptor.capture(), anyOrNull()) + assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun newWindow_fromSplitOpensInSplit() { + setUpLandscapeDisplay() + val task = setUpSplitScreenTask() + val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + runOpenNewWindow(task) + verify(splitScreenController) + .startIntent( + any(), anyInt(), any(), any(), + optionsCaptor.capture(), anyOrNull() + ) + assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun newWindow_fromFreeformAddsNewWindow() { + setUpLandscapeDisplay() + val task = setUpFreeformTask() + val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + runOpenNewWindow(task) + verify(transitions).startTransition(anyInt(), wctCaptor.capture(), anyOrNull()) + assertThat(ActivityOptions.fromBundle(wctCaptor.value.hierarchyOps[0].launchOptions) + .launchWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) + } + + private fun runOpenNewWindow(task: RunningTaskInfo) { + markTaskVisible(task) + task.baseActivity = mock(ComponentName::class.java) + task.isFocused = true + runningTasks.add(task) + controller.openNewWindow(task) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun openInstance_fromFullscreenOpensInSplit() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask() + val taskToRequest = setUpFreeformTask() + val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + runOpenInstance(task, taskToRequest.taskId) + verify(splitScreenController) + .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) + assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun openInstance_fromSplitOpensInSplit() { + setUpLandscapeDisplay() + val task = setUpSplitScreenTask() + val taskToRequest = setUpFreeformTask() + val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + runOpenInstance(task, taskToRequest.taskId) + verify(splitScreenController) + .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) + assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun openInstance_fromFreeformAddsNewWindow() { + setUpLandscapeDisplay() + val task = setUpFreeformTask() + val taskToRequest = setUpFreeformTask() + val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + runOpenInstance(task, taskToRequest.taskId) + verify(transitions).startTransition(anyInt(), wctCaptor.capture(), anyOrNull()) + assertThat(ActivityOptions.fromBundle(wctCaptor.value.hierarchyOps[0].launchOptions) + .launchWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) + } + + private fun runOpenInstance( + callingTask: RunningTaskInfo, + requestedTaskId: Int + ) { + markTaskVisible(callingTask) + callingTask.baseActivity = mock(ComponentName::class.java) + callingTask.isFocused = true + runningTasks.add(callingTask) + controller.openInstance(callingTask, requestedTaskId) + } + + @Test fun toggleBounds_togglesToStableBounds() { val bounds = Rect(0, 0, 100, 100) val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt new file mode 100644 index 000000000000..c989d1640f80 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.platform.test.annotations.EnableFlags +import android.view.Display.DEFAULT_DISPLAY +import android.view.WindowManager.TRANSIT_TO_BACK +import android.window.IWindowContainerToken +import android.window.TransitionInfo +import android.window.TransitionInfo.Change +import android.window.WindowContainerToken +import com.android.modules.utils.testing.ExtendedMockitoRule +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.Transitions +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class DesktopTasksTransitionObserverTest { + + @JvmField + @Rule + val extendedMockitoRule = + ExtendedMockitoRule.Builder(this) + .mockStatic(DesktopModeStatus::class.java) + .build()!! + + private val testExecutor = mock<ShellExecutor>() + private val mockShellInit = mock<ShellInit>() + private val transitions = mock<Transitions>() + private val context = mock<Context>() + private val shellTaskOrganizer = mock<ShellTaskOrganizer>() + private val taskRepository = mock<DesktopModeTaskRepository>() + + private lateinit var transitionObserver: DesktopTasksTransitionObserver + private lateinit var shellInit: ShellInit + + @Before + fun setup() { + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + shellInit = spy(ShellInit(testExecutor)) + + transitionObserver = + DesktopTasksTransitionObserver( + context, taskRepository, transitions, shellTaskOrganizer, shellInit + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun backNavigation_taskMinimized() { + val task = createTaskInfo(1) + whenever(taskRepository.getVisibleTaskCount(any())).thenReturn(1) + + transitionObserver.onTransitionReady( + transition = mock(), + info = + createBackNavigationTransition(task), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(taskRepository).minimizeTask(task.displayId, task.taskId) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun backNavigation_nullTaskInfo_taskNotMinimized() { + val task = createTaskInfo(1) + whenever(taskRepository.getVisibleTaskCount(any())).thenReturn(1) + + transitionObserver.onTransitionReady( + transition = mock(), + info = + createBackNavigationTransition(null), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(taskRepository, never()).minimizeTask(task.displayId, task.taskId) + } + + private fun createBackNavigationTransition( + task: RunningTaskInfo? + ): TransitionInfo { + return TransitionInfo(TRANSIT_TO_BACK, 0 /* flags */).apply { + addChange( + Change(mock(), mock()).apply { + mode = TRANSIT_TO_BACK + parent = null + taskInfo = task + flags = flags + } + ) + } + } + + private fun createTaskInfo(id: Int) = + RunningTaskInfo().apply { + taskId = id + displayId = DEFAULT_DISPLAY + configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + token = WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)) + baseIntent = Intent().apply { + component = ComponentName("package", "component.name") + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt index 0c3f98a324cd..0c100fca2036 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt @@ -30,7 +30,7 @@ import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_FREEFORM import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SINGLE import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SPLIT import com.android.wm.shell.shared.split.SplitBounds -import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50 +import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50 import com.google.common.truth.Correspondence import com.google.common.truth.Truth.assertThat import org.junit.Assert.assertThrows @@ -136,7 +136,7 @@ class GroupedRecentTaskInfoTest : ShellTestCase() { assertThat(recentTaskInfoParcel.taskInfo2).isNotNull() assertThat(recentTaskInfoParcel.taskInfo2!!.taskId).isEqualTo(2) assertThat(recentTaskInfoParcel.splitBounds).isNotNull() - assertThat(recentTaskInfoParcel.splitBounds!!.snapPosition).isEqualTo(SNAP_TO_50_50) + assertThat(recentTaskInfoParcel.splitBounds!!.snapPosition).isEqualTo(SNAP_TO_2_50_50) } @Test @@ -185,7 +185,7 @@ class GroupedRecentTaskInfoTest : ShellTestCase() { private fun splitTasksGroupInfo(): GroupedRecentTaskInfo { val task1 = createTaskInfo(id = 1) val task2 = createTaskInfo(id = 2) - val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_50_50) + val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_2_50_50) return GroupedRecentTaskInfo.forSplitTasks(task1, task2, splitBounds) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java index 386253c19c82..753d4cd153ee 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java @@ -22,7 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; -import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -211,10 +211,10 @@ public class RecentTasksControllerTest extends ShellTestCase { // Verify only one update if the split info is the same SplitBounds bounds1 = new SplitBounds(new Rect(0, 0, 50, 50), - new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_50_50); + new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_2_50_50); mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, bounds1); SplitBounds bounds2 = new SplitBounds(new Rect(0, 0, 50, 50), - new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_50_50); + new Rect(50, 50, 100, 100), t1.taskId, t2.taskId, SNAP_TO_2_50_50); mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, bounds2); verify(mRecentTasksController, times(1)).notifyRecentTasksChanged(); } @@ -246,9 +246,9 @@ public class RecentTasksControllerTest extends ShellTestCase { // Mark a couple pairs [t2, t4], [t3, t5] SplitBounds pair1Bounds = - new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_50_50); + new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_2_50_50); SplitBounds pair2Bounds = - new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_50_50); + new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_2_50_50); mRecentTasksController.addSplitPair(t2.taskId, t4.taskId, pair1Bounds); mRecentTasksController.addSplitPair(t3.taskId, t5.taskId, pair2Bounds); @@ -277,9 +277,9 @@ public class RecentTasksControllerTest extends ShellTestCase { // Mark a couple pairs [t2, t4], [t3, t5] SplitBounds pair1Bounds = - new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_50_50); + new SplitBounds(new Rect(), new Rect(), 2, 4, SNAP_TO_2_50_50); SplitBounds pair2Bounds = - new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_50_50); + new SplitBounds(new Rect(), new Rect(), 3, 5, SNAP_TO_2_50_50); mRecentTasksController.addSplitPair(t2.taskId, t4.taskId, pair1Bounds); mRecentTasksController.addSplitPair(t3.taskId, t5.taskId, pair2Bounds); @@ -339,7 +339,7 @@ public class RecentTasksControllerTest extends ShellTestCase { setRawList(t1, t2, t3, t4, t5); SplitBounds pair1Bounds = - new SplitBounds(new Rect(), new Rect(), 1, 2, SNAP_TO_50_50); + new SplitBounds(new Rect(), new Rect(), 1, 2, SNAP_TO_2_50_50); mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, pair1Bounds); when(mDesktopModeTaskRepository.isActiveTask(3)).thenReturn(true); @@ -449,7 +449,7 @@ public class RecentTasksControllerTest extends ShellTestCase { // Add a pair SplitBounds pair1Bounds = - new SplitBounds(new Rect(), new Rect(), 2, 3, SNAP_TO_50_50); + new SplitBounds(new Rect(), new Rect(), 2, 3, SNAP_TO_2_50_50); mRecentTasksController.addSplitPair(t2.taskId, t3.taskId, pair1Bounds); reset(mRecentTasksController); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java index 248393cef9ae..be8e6dc3154b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java @@ -1,6 +1,6 @@ package com.android.wm.shell.recents; -import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -46,21 +46,21 @@ public class SplitBoundsTest extends ShellTestCase { @Test public void testVerticalStacked() { SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect, - TASK_ID_1, TASK_ID_2, SNAP_TO_50_50); + TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50); assertTrue(ssb.appsStackedVertically); } @Test public void testHorizontalStacked() { SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect, - TASK_ID_1, TASK_ID_2, SNAP_TO_50_50); + TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50); assertFalse(ssb.appsStackedVertically); } @Test public void testHorizontalDividerBounds() { SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect, - TASK_ID_1, TASK_ID_2, SNAP_TO_50_50); + TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50); Rect dividerBounds = ssb.visualDividerBounds; assertEquals(0, dividerBounds.left); assertEquals(DEVICE_LENGTH / 2 - DIVIDER_SIZE / 2, dividerBounds.top); @@ -71,7 +71,7 @@ public class SplitBoundsTest extends ShellTestCase { @Test public void testVerticalDividerBounds() { SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect, - TASK_ID_1, TASK_ID_2, SNAP_TO_50_50); + TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50); Rect dividerBounds = ssb.visualDividerBounds; assertEquals(DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2, dividerBounds.left); assertEquals(0, dividerBounds.top); @@ -82,7 +82,7 @@ public class SplitBoundsTest extends ShellTestCase { @Test public void testEqualVerticalTaskPercent() { SplitBounds ssb = new SplitBounds(mTopRect, mBottomRect, - TASK_ID_1, TASK_ID_2, SNAP_TO_50_50); + TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50); float topPercentSpaceTaken = (float) (DEVICE_LENGTH / 2 - DIVIDER_SIZE / 2) / DEVICE_LENGTH; assertEquals(topPercentSpaceTaken, ssb.topTaskPercent, 0.01); } @@ -90,7 +90,7 @@ public class SplitBoundsTest extends ShellTestCase { @Test public void testEqualHorizontalTaskPercent() { SplitBounds ssb = new SplitBounds(mLeftRect, mRightRect, - TASK_ID_1, TASK_ID_2, SNAP_TO_50_50); + TASK_ID_1, TASK_ID_2, SNAP_TO_2_50_50); float leftPercentSpaceTaken = (float) (DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2) / DEVICE_WIDTH; assertEquals(leftPercentSpaceTaken, ssb.leftTaskPercent, 0.01); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt index 19c18be44ab1..ac9606350ebd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/split/SplitScreenConstantsTest.kt @@ -42,19 +42,44 @@ class SplitScreenConstantsTest { SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT, ) assertEquals( - "the value of SNAP_TO_30_70 should be 0", + "the value of SNAP_TO_2_33_66 should be 0", 0, - SplitScreenConstants.SNAP_TO_30_70, + SplitScreenConstants.SNAP_TO_2_33_66, ) assertEquals( - "the value of SNAP_TO_50_50 should be 1", + "the value of SNAP_TO_2_50_50 should be 1", 1, - SplitScreenConstants.SNAP_TO_50_50, + SplitScreenConstants.SNAP_TO_2_50_50, ) assertEquals( - "the value of SNAP_TO_70_30 should be 2", + "the value of SNAP_TO_2_66_33 should be 2", 2, - SplitScreenConstants.SNAP_TO_70_30, + SplitScreenConstants.SNAP_TO_2_66_33, + ) + assertEquals( + "the value of SNAP_TO_2_90_10 should be 3", + 3, + SplitScreenConstants.SNAP_TO_2_90_10, + ) + assertEquals( + "the value of SNAP_TO_2_10_90 should be 4", + 4, + SplitScreenConstants.SNAP_TO_2_10_90, + ) + assertEquals( + "the value of SNAP_TO_3_33_33_33 should be 5", + 5, + SplitScreenConstants.SNAP_TO_3_33_33_33, + ) + assertEquals( + "the value of SNAP_TO_3_45_45_10 should be 6", + 6, + SplitScreenConstants.SNAP_TO_3_45_45_10, + ) + assertEquals( + "the value of SNAP_TO_3_10_45_45 should be 7", + 7, + SplitScreenConstants.SNAP_TO_3_10_45_45, ) } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index a6c16c43c8cb..67eda8bfecd1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -74,6 +74,7 @@ import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.DefaultMixedHandler; +import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.HomeTransitionObserver; import com.android.wm.shell.transition.Transitions; @@ -429,7 +430,8 @@ public class StageCoordinatorTests extends ShellTestCase { ShellInit shellInit = new ShellInit(mMainExecutor); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mTaskOrganizer, mTransactionPool, mock(DisplayController.class), mMainExecutor, - mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), + mock(FocusTransitionObserver.class)); shellInit.init(); return t; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java new file mode 100644 index 000000000000..d37b4cf4b4b3 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java @@ -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.wm.shell.transition; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager.RunningTaskInfo; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.TransitionInfo.TransitionMode; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.window.flags.Flags; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.shared.IFocusTransitionListener; +import com.android.wm.shell.shared.TransactionPool; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tests for the focus transition observer. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +@RequiresFlagsEnabled(Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS) +public class FocusTransitionObserverTest extends ShellTestCase { + + static final int SECONDARY_DISPLAY_ID = 1; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private IFocusTransitionListener mListener; + private Transitions mTransition; + private FocusTransitionObserver mFocusTransitionObserver; + + @Before + public void setUp() { + mListener = mock(IFocusTransitionListener.class); + when(mListener.asBinder()).thenReturn(mock(IBinder.class)); + + mFocusTransitionObserver = new FocusTransitionObserver(); + mTransition = + new Transitions(InstrumentationRegistry.getInstrumentation().getTargetContext(), + mock(ShellInit.class), mock(ShellController.class), + mock(ShellTaskOrganizer.class), mock(TransactionPool.class), + mock(DisplayController.class), new TestShellExecutor(), + new Handler(Looper.getMainLooper()), new TestShellExecutor(), + mock(HomeTransitionObserver.class), + mFocusTransitionObserver); + mFocusTransitionObserver.setRemoteFocusTransitionListener(mTransition, mListener); + } + + @Test + public void testTransitionWithMovedToFrontFlagChangesDisplayFocus() throws RemoteException { + final IBinder binder = mock(IBinder.class); + final SurfaceControl.Transaction tx = mock(SurfaceControl.Transaction.class); + + // Open a task on the default display, which doesn't change display focus because the + // default display already has it. + TransitionInfo info = mock(TransitionInfo.class); + final List<TransitionInfo.Change> changes = new ArrayList<>(); + setupChange(changes, 123 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY, + true /* focused */); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx); + verify(mListener, never()).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID); + clearInvocations(mListener); + + // Open a new task on the secondary display and verify display focus changes to the display. + changes.clear(); + setupChange(changes, 456 /* taskId */, TRANSIT_OPEN, SECONDARY_DISPLAY_ID, + true /* focused */); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx); + verify(mListener, times(1)).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID); + clearInvocations(mListener); + + // Open the first task to front and verify display focus goes back to the default display. + changes.clear(); + setupChange(changes, 123 /* taskId */, TRANSIT_TO_FRONT, DEFAULT_DISPLAY, + true /* focused */); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx); + verify(mListener, times(1)).onFocusedDisplayChanged(DEFAULT_DISPLAY); + clearInvocations(mListener); + + // Open another task on the default display and verify no display focus switch as it's + // already on the default display. + changes.clear(); + setupChange(changes, 789 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY, + true /* focused */); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx); + verify(mListener, never()).onFocusedDisplayChanged(DEFAULT_DISPLAY); + } + + private void setupChange(List<TransitionInfo.Change> changes, int taskId, + @TransitionMode int mode, int displayId, boolean focused) { + TransitionInfo.Change change = mock(TransitionInfo.Change.class); + RunningTaskInfo taskInfo = mock(RunningTaskInfo.class); + taskInfo.taskId = taskId; + taskInfo.isFocused = focused; + when(change.hasFlags(FLAG_MOVED_TO_TOP)).thenReturn(focused); + taskInfo.displayId = displayId; + when(change.getTaskInfo()).thenReturn(taskInfo); + when(change.getMode()).thenReturn(mode); + changes.add(change); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java index 8f49de0a98fb..8dfdfb4dcbcf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java @@ -100,7 +100,8 @@ public class HomeTransitionObserverTest extends ShellTestCase { mHomeTransitionObserver = new HomeTransitionObserver(mContext, mMainExecutor); mTransition = new Transitions(mContext, mock(ShellInit.class), mock(ShellController.class), mOrganizer, mTransactionPool, mDisplayController, mMainExecutor, - mMainHandler, mAnimExecutor, mHomeTransitionObserver); + mMainHandler, mAnimExecutor, mHomeTransitionObserver, + mock(FocusTransitionObserver.class)); mHomeTransitionObserver.setHomeTransitionListener(mTransition, mListener); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index aea14b900647..6cde0569796d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -158,7 +158,8 @@ public class ShellTransitionTests extends ShellTestCase { ShellInit shellInit = mock(ShellInit.class); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, - mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), + mock(FocusTransitionObserver.class)); // One from Transitions, one from RootTaskDisplayAreaOrganizer verify(shellInit).addInitCallback(any(), eq(t)); verify(shellInit).addInitCallback(any(), isA(RootTaskDisplayAreaOrganizer.class)); @@ -170,7 +171,8 @@ public class ShellTransitionTests extends ShellTestCase { ShellController shellController = mock(ShellController.class); final Transitions t = new Transitions(mContext, shellInit, shellController, mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, - mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), + mock(FocusTransitionObserver.class)); shellInit.init(); verify(shellController, times(1)).addExternalInterface( eq(ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS), any(), any()); @@ -1238,7 +1240,8 @@ public class ShellTransitionTests extends ShellTestCase { final Transitions transitions = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, - mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), + mock(FocusTransitionObserver.class)); final RecentsTransitionHandler recentsHandler = new RecentsTransitionHandler(shellInit, mock(ShellTaskOrganizer.class), transitions, mock(RecentTasksController.class), mock(HomeTransitionObserver.class)); @@ -1780,7 +1783,8 @@ public class ShellTransitionTests extends ShellTestCase { ShellInit shellInit = new ShellInit(mMainExecutor); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, - mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class), + mock(FocusTransitionObserver.class)); shellInit.init(); return t; } 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 new file mode 100644 index 000000000000..5594981135b1 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor.education + +import android.annotation.LayoutRes +import android.content.Context +import android.graphics.Point +import android.testing.AndroidTestingRunner +import android.testing.TestableContext +import android.testing.TestableLooper +import android.testing.TestableResources +import android.view.MotionEvent +import android.view.View +import android.view.WindowManager +import android.widget.TextView +import androidx.test.filters.SmallTest +import com.android.wm.shell.R +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer +import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipArrowDirection +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify + +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidTestingRunner::class) +class DesktopWindowingEducationTooltipControllerTest : ShellTestCase() { + @Mock private lateinit var mockWindowManager: WindowManager + @Mock private lateinit var mockViewContainerFactory: AdditionalSystemViewContainer.Factory + private lateinit var testableResources: TestableResources + private lateinit var testableContext: TestableContext + private lateinit var tooltipController: DesktopWindowingEducationTooltipController + private val tooltipViewArgumentCaptor = argumentCaptor<View>() + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testableContext = TestableContext(mContext) + testableResources = + testableContext.orCreateTestableResources.apply { + addOverride(R.dimen.desktop_windowing_education_tooltip_padding, 10) + } + testableContext.addMockSystemService( + Context.LAYOUT_INFLATER_SERVICE, context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)) + testableContext.addMockSystemService(WindowManager::class.java, mockWindowManager) + tooltipController = + DesktopWindowingEducationTooltipController(testableContext, mockViewContainerFactory) + } + + @Test + fun showEducationTooltip_createsTooltipWithCorrectText() { + val tooltipText = "This is a tooltip" + val tooltipViewConfig = createTooltipConfig(tooltipText = tooltipText) + + tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123) + + verify(mockViewContainerFactory, times(1)) + .create( + windowManagerWrapper = any(), + taskId = anyInt(), + x = anyInt(), + y = anyInt(), + width = anyInt(), + height = anyInt(), + flags = anyInt(), + view = tooltipViewArgumentCaptor.capture()) + val tooltipTextView = + tooltipViewArgumentCaptor.lastValue.findViewById<TextView>(R.id.tooltip_text) + assertThat(tooltipTextView.text).isEqualTo(tooltipText) + } + + @Test + fun showEducationTooltip_usesCorrectTaskIdForWindow() { + val tooltipViewConfig = createTooltipConfig() + val taskIdArgumentCaptor = argumentCaptor<Int>() + + tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123) + + verify(mockViewContainerFactory, times(1)) + .create( + windowManagerWrapper = any(), + taskId = taskIdArgumentCaptor.capture(), + x = anyInt(), + y = anyInt(), + width = anyInt(), + height = anyInt(), + flags = anyInt(), + view = anyOrNull()) + assertThat(taskIdArgumentCaptor.lastValue).isEqualTo(123) + } + + @Test + fun showEducationTooltip_tooltipPointsUpwards_horizontallyPositionTooltip() { + val initialTooltipX = 0 + val initialTooltipY = 0 + val tooltipViewConfig = + createTooltipConfig( + arrowDirection = TooltipArrowDirection.UP, + tooltipViewGlobalCoordinates = Point(initialTooltipX, initialTooltipY)) + val tooltipXArgumentCaptor = argumentCaptor<Int>() + val tooltipWidthArgumentCaptor = argumentCaptor<Int>() + + tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123) + + verify(mockViewContainerFactory, times(1)) + .create( + windowManagerWrapper = any(), + taskId = anyInt(), + x = tooltipXArgumentCaptor.capture(), + y = anyInt(), + width = tooltipWidthArgumentCaptor.capture(), + height = anyInt(), + flags = anyInt(), + view = tooltipViewArgumentCaptor.capture()) + val expectedTooltipX = initialTooltipX - tooltipWidthArgumentCaptor.lastValue / 2 + assertThat(tooltipXArgumentCaptor.lastValue).isEqualTo(expectedTooltipX) + } + + @Test + fun showEducationTooltip_tooltipPointsLeft_verticallyPositionTooltip() { + val initialTooltipX = 0 + val initialTooltipY = 0 + val tooltipViewConfig = + createTooltipConfig( + arrowDirection = TooltipArrowDirection.LEFT, + tooltipViewGlobalCoordinates = Point(initialTooltipX, initialTooltipY)) + val tooltipYArgumentCaptor = argumentCaptor<Int>() + val tooltipHeightArgumentCaptor = argumentCaptor<Int>() + + tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123) + + verify(mockViewContainerFactory, times(1)) + .create( + windowManagerWrapper = any(), + taskId = anyInt(), + x = anyInt(), + y = tooltipYArgumentCaptor.capture(), + width = anyInt(), + height = tooltipHeightArgumentCaptor.capture(), + flags = anyInt(), + view = tooltipViewArgumentCaptor.capture()) + val expectedTooltipY = initialTooltipY - tooltipHeightArgumentCaptor.lastValue / 2 + assertThat(tooltipYArgumentCaptor.lastValue).isEqualTo(expectedTooltipY) + } + + @Test + fun showEducationTooltip_touchEventActionOutside_dismissActionPerformed() { + val mockLambda: () -> Unit = mock() + val tooltipViewConfig = createTooltipConfig(onDismissAction = mockLambda) + + tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123) + verify(mockViewContainerFactory, times(1)) + .create( + windowManagerWrapper = any(), + taskId = anyInt(), + x = anyInt(), + y = anyInt(), + width = anyInt(), + height = anyInt(), + flags = anyInt(), + view = tooltipViewArgumentCaptor.capture()) + val motionEvent = + MotionEvent.obtain( + /* downTime= */ 0L, + /* eventTime= */ 0L, + MotionEvent.ACTION_OUTSIDE, + /* x= */ 0f, + /* y= */ 0f, + /* metaState= */ 0) + tooltipViewArgumentCaptor.lastValue.dispatchTouchEvent(motionEvent) + + verify(mockLambda).invoke() + } + + @Test + fun showEducationTooltip_tooltipClicked_onClickActionPerformed() { + val mockLambda: () -> Unit = mock() + val tooltipViewConfig = createTooltipConfig(onEducationClickAction = mockLambda) + + tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123) + verify(mockViewContainerFactory, times(1)) + .create( + windowManagerWrapper = any(), + taskId = anyInt(), + x = anyInt(), + y = anyInt(), + width = anyInt(), + height = anyInt(), + flags = anyInt(), + view = tooltipViewArgumentCaptor.capture()) + tooltipViewArgumentCaptor.lastValue.performClick() + + verify(mockLambda).invoke() + } + + private fun createTooltipConfig( + @LayoutRes tooltipViewLayout: Int = R.layout.desktop_windowing_education_top_arrow_tooltip, + tooltipViewGlobalCoordinates: Point = Point(0, 0), + tooltipText: String = "This is a tooltip", + arrowDirection: TooltipArrowDirection = TooltipArrowDirection.UP, + onEducationClickAction: () -> Unit = {}, + onDismissAction: () -> Unit = {} + ) = + DesktopWindowingEducationTooltipController.EducationViewConfig( + tooltipViewLayout = tooltipViewLayout, + tooltipViewGlobalCoordinates = tooltipViewGlobalCoordinates, + tooltipText = tooltipText, + arrowDirection = arrowDirection, + onEducationClickAction = onEducationClickAction, + onDismissAction = onDismissAction, + ) +} diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt index 504e3290b0ae..3ed33db2222e 100644 --- a/libs/appfunctions/api/current.txt +++ b/libs/appfunctions/api/current.txt @@ -3,13 +3,15 @@ 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 java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + 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 @Deprecated public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); } public abstract class AppFunctionService extends android.app.Service { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); - method @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method @Deprecated @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE"; field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; } 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 b1dd4676a35e..815fe05cc3ab 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java @@ -19,12 +19,12 @@ package com.google.android.appfunctions.sidecar; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.content.Context; +import android.os.CancellationSignal; import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; - /** * Provides app functions related functionalities. * @@ -45,7 +45,7 @@ public final class AppFunctionManager { * * @param context A {@link Context}. * @throws java.lang.IllegalStateException if the underlying {@link - * android.app.appfunctions.AppFunctionManager} is not found. + * android.app.appfunctions.AppFunctionManager} is not found. */ public AppFunctionManager(Context context) { mContext = Objects.requireNonNull(context); @@ -66,6 +66,7 @@ public final class AppFunctionManager { public void executeAppFunction( @NonNull ExecuteAppFunctionRequest sidecarRequest, @NonNull @CallbackExecutor Executor executor, + @NonNull CancellationSignal cancellationSignal, @NonNull Consumer<ExecuteAppFunctionResponse> callback) { Objects.requireNonNull(sidecarRequest); Objects.requireNonNull(executor); @@ -74,9 +75,40 @@ public final class AppFunctionManager { android.app.appfunctions.ExecuteAppFunctionRequest platformRequest = SidecarConverter.getPlatformExecuteAppFunctionRequest(sidecarRequest); mManager.executeAppFunction( - platformRequest, executor, (platformResponse) -> { - callback.accept(SidecarConverter.getSidecarExecuteAppFunctionResponse( - platformResponse)); + platformRequest, + executor, + cancellationSignal, + (platformResponse) -> { + callback.accept( + SidecarConverter.getSidecarExecuteAppFunctionResponse( + platformResponse)); }); } + + /** + * Executes the app function. + * + * <p>Proxies request and response to the underlying {@link + * android.app.appfunctions.AppFunctionManager#executeAppFunction}, converting the request and + * response in the appropriate type required by the function. + * + * @deprecated Use {@link #executeAppFunction(ExecuteAppFunctionRequest, Executor, + * CancellationSignal, Consumer)} instead. This method will be removed once usage references + * are updated. + */ + @Deprecated + public void executeAppFunction( + @NonNull ExecuteAppFunctionRequest sidecarRequest, + @NonNull @CallbackExecutor Executor executor, + @NonNull Consumer<ExecuteAppFunctionResponse> callback) { + Objects.requireNonNull(sidecarRequest); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + + executeAppFunction( + sidecarRequest, + executor, + new CancellationSignal(), + callback); + } } diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java index 65959dfdf561..6023c977bd76 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java @@ -25,6 +25,7 @@ import android.app.Service; import android.content.Intent; import android.os.Binder; import android.os.IBinder; +import android.os.CancellationSignal; import java.util.function.Consumer; @@ -69,10 +70,11 @@ public abstract class AppFunctionService extends Service { private final Binder mBinder = android.app.appfunctions.AppFunctionService.createBinder( /* context= */ this, - /* onExecuteFunction= */ (platformRequest, callback) -> { + /* onExecuteFunction= */ (platformRequest, cancellationSignal, callback) -> { AppFunctionService.this.onExecuteFunction( SidecarConverter.getSidecarExecuteAppFunctionRequest( platformRequest), + cancellationSignal, (sidecarResponse) -> { callback.accept( SidecarConverter.getPlatformExecuteAppFunctionResponse( @@ -105,9 +107,42 @@ public abstract class AppFunctionService extends Service { * result using the callback, no matter if the execution was successful or not. * * @param request The function execution request. + * @param cancellationSignal A {@link CancellationSignal} to cancel the request. * @param callback A callback to report back the result. */ @MainThread + public void onExecuteFunction( + @NonNull ExecuteAppFunctionRequest request, + @NonNull CancellationSignal cancellationSignal, + @NonNull Consumer<ExecuteAppFunctionResponse> callback) { + onExecuteFunction(request, callback); + } + + /** + * Called by the system to execute a specific app function. + * + * <p>This method is triggered when the system requests your AppFunctionService to handle a + * particular function you have registered and made available. + * + * <p>To ensure proper routing of function requests, assign a unique identifier to each + * function. This identifier doesn't need to be globally unique, but it must be unique within + * your app. For example, a function to order food could be identified as "orderFood". In most + * cases this identifier should come from the ID automatically generated by the AppFunctions + * SDK. You can determine the specific function to invoke by calling {@link + * ExecuteAppFunctionRequest#getFunctionIdentifier()}. + * + * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker + * thread and dispatch the result with the given callback. You should always report back the + * result using the callback, no matter if the execution was successful or not. + * + * @param request The function execution request. + * @param callback A callback to report back the result. + * + * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal, + * Consumer)} instead. This method will be removed once usage references are updated. + */ + @MainThread + @Deprecated public abstract void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull Consumer<ExecuteAppFunctionResponse> callback); diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index b6476c9d466f..ae46a99f09c8 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -50,6 +50,10 @@ constexpr bool skip_eglmanager_telemetry() { constexpr bool resample_gainmap_regions() { return false; } + +constexpr bool query_global_priority() { + return false; +} } // namespace hwui_flags #endif @@ -110,6 +114,7 @@ bool Properties::clipSurfaceViews = false; bool Properties::hdr10bitPlus = false; bool Properties::skipTelemetry = false; bool Properties::resampleGainmapRegions = false; +bool Properties::queryGlobalPriority = false; int Properties::timeoutMultiplier = 1; @@ -187,6 +192,7 @@ bool Properties::load() { hdr10bitPlus = hwui_flags::hdr_10bit_plus(); resampleGainmapRegions = base::GetBoolProperty("debug.hwui.resample_gainmap_regions", hwui_flags::resample_gainmap_regions()); + queryGlobalPriority = hwui_flags::query_global_priority(); timeoutMultiplier = android::base::GetIntProperty("ro.hw_timeout_multiplier", 1); skipTelemetry = base::GetBoolProperty(PROPERTY_SKIP_EGLMANAGER_TELEMETRY, diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index db471527b861..6f84796fb11e 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -346,6 +346,7 @@ public: static bool hdr10bitPlus; static bool skipTelemetry; static bool resampleGainmapRegions; + static bool queryGlobalPriority; static int timeoutMultiplier; diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index ab052b902e02..93df47853769 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -129,3 +129,13 @@ flag { description: "APIs that expose gainmap metadata corresponding to those defined in ISO 21496-1" bug: "349357636" } + +flag { + name: "query_global_priority" + namespace: "core_graphics" + description: "Attempt to query whether the vulkan driver supports the requested global priority before queue creation." + bug: "343986434" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/libs/hwui/hwui/MinikinSkia.cpp b/libs/hwui/hwui/MinikinSkia.cpp index bbb142014ed8..f0bcfe537998 100644 --- a/libs/hwui/hwui/MinikinSkia.cpp +++ b/libs/hwui/hwui/MinikinSkia.cpp @@ -36,7 +36,7 @@ namespace android { MinikinFontSkia::MinikinFontSkia(sk_sp<SkTypeface> typeface, int sourceId, const void* fontData, size_t fontSize, std::string_view filePath, int ttcIndex, - const std::vector<minikin::FontVariation>& axes) + const minikin::VariationSettings& axes) : mTypeface(std::move(typeface)) , mSourceId(sourceId) , mFontData(fontData) @@ -123,12 +123,12 @@ int MinikinFontSkia::GetFontIndex() const { return mTtcIndex; } -const std::vector<minikin::FontVariation>& MinikinFontSkia::GetAxes() const { +const minikin::VariationSettings& MinikinFontSkia::GetAxes() const { return mAxes; } std::shared_ptr<minikin::MinikinFont> MinikinFontSkia::createFontWithVariation( - const std::vector<minikin::FontVariation>& variations) const { + const minikin::VariationSettings& variations) const { SkFontArguments args; std::vector<SkFontArguments::VariationPosition::Coordinate> skVariation; diff --git a/libs/hwui/hwui/MinikinSkia.h b/libs/hwui/hwui/MinikinSkia.h index de9a5c2af0aa..7fe5978bfda3 100644 --- a/libs/hwui/hwui/MinikinSkia.h +++ b/libs/hwui/hwui/MinikinSkia.h @@ -32,7 +32,7 @@ class ANDROID_API MinikinFontSkia : public minikin::MinikinFont { public: MinikinFontSkia(sk_sp<SkTypeface> typeface, int sourceId, const void* fontData, size_t fontSize, std::string_view filePath, int ttcIndex, - const std::vector<minikin::FontVariation>& axes); + const minikin::VariationSettings& axes); float GetHorizontalAdvance(uint32_t glyph_id, const minikin::MinikinPaint& paint, const minikin::FontFakery& fakery) const override; @@ -59,9 +59,9 @@ public: size_t GetFontSize() const; int GetFontIndex() const; const std::string& getFilePath() const { return mFilePath; } - const std::vector<minikin::FontVariation>& GetAxes() const; + const minikin::VariationSettings& GetAxes() const; std::shared_ptr<minikin::MinikinFont> createFontWithVariation( - const std::vector<minikin::FontVariation>&) const; + const minikin::VariationSettings&) const; int GetSourceId() const override { return mSourceId; } static uint32_t packFontFlags(const SkFont&); @@ -80,7 +80,7 @@ private: const void* mFontData; size_t mFontSize; int mTtcIndex; - std::vector<minikin::FontVariation> mAxes; + minikin::VariationSettings mAxes; std::string mFilePath; }; diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp index a9d1a2aed8cc..2d812d675fdc 100644 --- a/libs/hwui/hwui/Typeface.cpp +++ b/libs/hwui/hwui/Typeface.cpp @@ -92,8 +92,8 @@ Typeface* Typeface::createAbsolute(Typeface* base, int weight, bool italic) { return result; } -Typeface* Typeface::createFromTypefaceWithVariation( - Typeface* src, const std::vector<minikin::FontVariation>& variations) { +Typeface* Typeface::createFromTypefaceWithVariation(Typeface* src, + const minikin::VariationSettings& variations) { const Typeface* resolvedFace = Typeface::resolveDefault(src); Typeface* result = new Typeface(); if (result != nullptr) { @@ -192,9 +192,8 @@ void Typeface::setRobotoTypefaceForTest() { sk_sp<SkTypeface> typeface = fm->makeFromStream(std::move(fontData)); LOG_ALWAYS_FATAL_IF(typeface == nullptr, "Failed to make typeface from %s", kRobotoFont); - std::shared_ptr<minikin::MinikinFont> font = - std::make_shared<MinikinFontSkia>(std::move(typeface), 0, data, st.st_size, kRobotoFont, - 0, std::vector<minikin::FontVariation>()); + std::shared_ptr<minikin::MinikinFont> font = std::make_shared<MinikinFontSkia>( + std::move(typeface), 0, data, st.st_size, kRobotoFont, 0, minikin::VariationSettings()); std::vector<std::shared_ptr<minikin::Font>> fonts; fonts.push_back(minikin::Font::Builder(font).build()); diff --git a/libs/hwui/hwui/Typeface.h b/libs/hwui/hwui/Typeface.h index 565136e53676..2c96c1ad80fe 100644 --- a/libs/hwui/hwui/Typeface.h +++ b/libs/hwui/hwui/Typeface.h @@ -74,8 +74,8 @@ public: static Typeface* createRelative(Typeface* src, Style desiredStyle); static Typeface* createAbsolute(Typeface* base, int weight, bool italic); - static Typeface* createFromTypefaceWithVariation( - Typeface* src, const std::vector<minikin::FontVariation>& variations); + static Typeface* createFromTypefaceWithVariation(Typeface* src, + const minikin::VariationSettings& variations); static Typeface* createFromFamilies( std::vector<std::shared_ptr<minikin::FontFamily>>&& families, int weight, int italic, diff --git a/libs/hwui/jni/FontFamily.cpp b/libs/hwui/jni/FontFamily.cpp index e6d790f56d0f..9922ff393e55 100644 --- a/libs/hwui/jni/FontFamily.cpp +++ b/libs/hwui/jni/FontFamily.cpp @@ -133,9 +133,9 @@ static bool addSkTypeface(NativeFamilyBuilder* builder, sk_sp<SkData>&& data, in builder->axes.clear(); return false; } - std::shared_ptr<minikin::MinikinFont> minikinFont = - std::make_shared<MinikinFontSkia>(std::move(face), fonts::getNewSourceId(), fontPtr, - fontSize, "", ttcIndex, builder->axes); + std::shared_ptr<minikin::MinikinFont> minikinFont = std::make_shared<MinikinFontSkia>( + std::move(face), fonts::getNewSourceId(), fontPtr, fontSize, "", ttcIndex, + minikin::VariationSettings(builder->axes, false)); minikin::Font::Builder fontBuilder(minikinFont); if (weight != RESOLVE_BY_FONT_TABLE) { diff --git a/libs/hwui/jni/PathIterator.cpp b/libs/hwui/jni/PathIterator.cpp index 3884342d8d37..e9de6555935d 100644 --- a/libs/hwui/jni/PathIterator.cpp +++ b/libs/hwui/jni/PathIterator.cpp @@ -20,6 +20,7 @@ #include "GraphicsJNI.h" #include "SkPath.h" #include "SkPoint.h" +#include "graphics_jni_helpers.h" namespace android { @@ -36,6 +37,18 @@ public: return reinterpret_cast<jlong>(new SkPath::RawIter(*path)); } + // A variant of 'next' (below) that is compatible with the host JVM. + static jint nextHost(JNIEnv* env, jclass clazz, jlong iteratorHandle, jfloatArray pointsArray) { + jfloat* points = env->GetFloatArrayElements(pointsArray, 0); +#ifdef __ANDROID__ + jint result = next(iteratorHandle, reinterpret_cast<jlong>(points)); +#else + jint result = next(env, clazz, iteratorHandle, reinterpret_cast<jlong>(points)); +#endif + env->ReleaseFloatArrayElements(pointsArray, points, 0); + return result; + } + // ---------------- @CriticalNative ------------------------- static jint peek(CRITICAL_JNI_PARAMS_COMMA jlong iteratorHandle) { @@ -72,6 +85,7 @@ static const JNINativeMethod methods[] = { {"nPeek", "(J)I", (void*)SkPathIteratorGlue::peek}, {"nNext", "(JJ)I", (void*)SkPathIteratorGlue::next}, + {"nNextHost", "(J[F)I", (void*)SkPathIteratorGlue::nextHost}, }; int register_android_graphics_PathIterator(JNIEnv* env) { diff --git a/libs/hwui/jni/Typeface.cpp b/libs/hwui/jni/Typeface.cpp index 209b35c5537c..0f458dde8b07 100644 --- a/libs/hwui/jni/Typeface.cpp +++ b/libs/hwui/jni/Typeface.cpp @@ -80,7 +80,8 @@ static jlong Typeface_createFromTypefaceWithVariation(JNIEnv* env, jobject, jlon AxisHelper axis(env, axisObject); variations.push_back(minikin::FontVariation(axis.getTag(), axis.getStyleValue())); } - return toJLong(Typeface::createFromTypefaceWithVariation(toTypeface(familyHandle), variations)); + return toJLong(Typeface::createFromTypefaceWithVariation( + toTypeface(familyHandle), minikin::VariationSettings(variations, false /* sorted */))); } static jlong Typeface_createWeightAlias(JNIEnv* env, jobject, jlong familyHandle, jint weight) { @@ -273,7 +274,7 @@ void MinikinFontSkiaFactory::write(minikin::BufferWriter* writer, const std::string& path = typeface->GetFontPath(); writer->writeString(path); writer->write<int>(typeface->GetFontIndex()); - const std::vector<minikin::FontVariation>& axes = typeface->GetAxes(); + const minikin::VariationSettings& axes = typeface->GetAxes(); writer->writeArray<minikin::FontVariation>(axes.data(), axes.size()); bool hasVerity = getVerity(path); writer->write<int8_t>(static_cast<int8_t>(hasVerity)); diff --git a/libs/hwui/jni/fonts/Font.cpp b/libs/hwui/jni/fonts/Font.cpp index f405abaaf5b4..6a05b6c2626c 100644 --- a/libs/hwui/jni/fonts/Font.cpp +++ b/libs/hwui/jni/fonts/Font.cpp @@ -142,7 +142,7 @@ static jlong Font_Builder_clone(JNIEnv* env, jobject clazz, jlong fontPtr, jlong std::shared_ptr<minikin::MinikinFont> newMinikinFont = std::make_shared<MinikinFontSkia>( std::move(newTypeface), minikinSkia->GetSourceId(), minikinSkia->GetFontData(), minikinSkia->GetFontSize(), minikinSkia->getFilePath(), minikinSkia->GetFontIndex(), - builder->axes); + minikin::VariationSettings(builder->axes, false)); std::shared_ptr<minikin::Font> newFont = minikin::Font::Builder(newMinikinFont) .setWeight(weight) .setSlant(static_cast<minikin::FontStyle::Slant>(italic)) @@ -303,7 +303,7 @@ static jlong Font_getAxisInfo(CRITICAL_JNI_PARAMS_COMMA jlong fontPtr, jint inde var = reader.readArray<minikin::FontVariation>().first[index]; } else { const std::shared_ptr<minikin::MinikinFont>& minikinFont = font->font->baseTypeface(); - var = minikinFont->GetAxes().at(index); + var = minikinFont->GetAxes()[index]; } uint32_t floatBinary = *reinterpret_cast<const uint32_t*>(&var.value); return (static_cast<uint64_t>(var.axisTag) << 32) | static_cast<uint64_t>(floatBinary); diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index e3023937964e..6571d92aeafa 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -44,7 +44,7 @@ namespace uirenderer { namespace renderthread { // Not all of these are strictly required, but are all enabled if present. -static std::array<std::string_view, 21> sEnableExtensions{ +static std::array<std::string_view, 23> sEnableExtensions{ VK_KHR_BIND_MEMORY_2_EXTENSION_NAME, VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME, VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME, @@ -65,6 +65,8 @@ static std::array<std::string_view, 21> sEnableExtensions{ VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME, VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME, + VK_EXT_GLOBAL_PRIORITY_QUERY_EXTENSION_NAME, + VK_KHR_GLOBAL_PRIORITY_EXTENSION_NAME, VK_EXT_DEVICE_FAULT_EXTENSION_NAME, }; @@ -206,7 +208,7 @@ void VulkanManager::setupDevice(skgpu::VulkanExtensions& grExtensions, GET_INST_PROC(GetPhysicalDeviceFeatures2); GET_INST_PROC(GetPhysicalDeviceImageFormatProperties2); GET_INST_PROC(GetPhysicalDeviceProperties); - GET_INST_PROC(GetPhysicalDeviceQueueFamilyProperties); + GET_INST_PROC(GetPhysicalDeviceQueueFamilyProperties2); uint32_t gpuCount; LOG_ALWAYS_FATAL_IF(mEnumeratePhysicalDevices(mInstance, &gpuCount, nullptr)); @@ -225,21 +227,30 @@ void VulkanManager::setupDevice(skgpu::VulkanExtensions& grExtensions, // query to get the initial queue props size uint32_t queueCount = 0; - mGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &queueCount, nullptr); + mGetPhysicalDeviceQueueFamilyProperties2(mPhysicalDevice, &queueCount, nullptr); LOG_ALWAYS_FATAL_IF(!queueCount); // now get the actual queue props - std::unique_ptr<VkQueueFamilyProperties[]> queueProps(new VkQueueFamilyProperties[queueCount]); - mGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &queueCount, queueProps.get()); + std::unique_ptr<VkQueueFamilyProperties2[]> + queueProps(new VkQueueFamilyProperties2[queueCount]); + // query the global priority, this ignored if VK_EXT_global_priority isn't supported + std::vector<VkQueueFamilyGlobalPriorityPropertiesEXT> queuePriorityProps(queueCount); + for (uint32_t i = 0; i < queueCount; i++) { + queuePriorityProps[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_GLOBAL_PRIORITY_PROPERTIES_EXT; + queuePriorityProps[i].pNext = nullptr; + queueProps[i].pNext = &queuePriorityProps[i]; + } + mGetPhysicalDeviceQueueFamilyProperties2(mPhysicalDevice, &queueCount, queueProps.get()); constexpr auto kRequestedQueueCount = 2; // iterate to find the graphics queue mGraphicsQueueIndex = queueCount; for (uint32_t i = 0; i < queueCount; i++) { - if (queueProps[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { + if (queueProps[i].queueFamilyProperties.queueFlags & VK_QUEUE_GRAPHICS_BIT) { mGraphicsQueueIndex = i; - LOG_ALWAYS_FATAL_IF(queueProps[i].queueCount < kRequestedQueueCount); + LOG_ALWAYS_FATAL_IF( + queueProps[i].queueFamilyProperties.queueCount < kRequestedQueueCount); break; } } @@ -327,6 +338,15 @@ void VulkanManager::setupDevice(skgpu::VulkanExtensions& grExtensions, tailPNext = &formatFeatures->pNext; } + VkPhysicalDeviceGlobalPriorityQueryFeaturesEXT* globalPriorityQueryFeatures = + new VkPhysicalDeviceGlobalPriorityQueryFeaturesEXT; + globalPriorityQueryFeatures->sType = + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GLOBAL_PRIORITY_QUERY_FEATURES_EXT; + globalPriorityQueryFeatures->pNext = nullptr; + globalPriorityQueryFeatures->globalPriorityQuery = false; + *tailPNext = globalPriorityQueryFeatures; + tailPNext = &globalPriorityQueryFeatures->pNext; + // query to get the physical device features mGetPhysicalDeviceFeatures2(mPhysicalDevice, &features); // this looks like it would slow things down, @@ -341,24 +361,59 @@ void VulkanManager::setupDevice(skgpu::VulkanExtensions& grExtensions, if (Properties::contextPriority != 0 && grExtensions.hasExtension(VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME, 2)) { - memset(&queuePriorityCreateInfo, 0, sizeof(VkDeviceQueueGlobalPriorityCreateInfoEXT)); - queuePriorityCreateInfo.sType = - VK_STRUCTURE_TYPE_DEVICE_QUEUE_GLOBAL_PRIORITY_CREATE_INFO_EXT; - queuePriorityCreateInfo.pNext = nullptr; + VkQueueGlobalPriorityEXT globalPriority; switch (Properties::contextPriority) { case EGL_CONTEXT_PRIORITY_LOW_IMG: - queuePriorityCreateInfo.globalPriority = VK_QUEUE_GLOBAL_PRIORITY_LOW_EXT; + globalPriority = VK_QUEUE_GLOBAL_PRIORITY_LOW_EXT; break; case EGL_CONTEXT_PRIORITY_MEDIUM_IMG: - queuePriorityCreateInfo.globalPriority = VK_QUEUE_GLOBAL_PRIORITY_MEDIUM_EXT; + globalPriority = VK_QUEUE_GLOBAL_PRIORITY_MEDIUM_EXT; break; case EGL_CONTEXT_PRIORITY_HIGH_IMG: - queuePriorityCreateInfo.globalPriority = VK_QUEUE_GLOBAL_PRIORITY_HIGH_EXT; + globalPriority = VK_QUEUE_GLOBAL_PRIORITY_HIGH_EXT; break; default: LOG_ALWAYS_FATAL("Unsupported context priority"); } - queueNextPtr = &queuePriorityCreateInfo; + + // check if the requested priority is reported by the query + bool attachGlobalPriority = false; + if (uirenderer::Properties::queryGlobalPriority && + globalPriorityQueryFeatures->globalPriorityQuery) { + for (uint32_t i = 0; i < queuePriorityProps[mGraphicsQueueIndex].priorityCount; i++) { + if (queuePriorityProps[mGraphicsQueueIndex].priorities[i] == globalPriority) { + attachGlobalPriority = true; + break; + } + } + } else { + // Querying is not supported so attempt queue creation with requested priority anyways + // If the priority turns out not to be supported, the driver *may* fail with + // VK_ERROR_NOT_PERMITTED_KHR + attachGlobalPriority = true; + } + + if (attachGlobalPriority) { + memset(&queuePriorityCreateInfo, 0, sizeof(VkDeviceQueueGlobalPriorityCreateInfoEXT)); + queuePriorityCreateInfo.sType = + VK_STRUCTURE_TYPE_DEVICE_QUEUE_GLOBAL_PRIORITY_CREATE_INFO_EXT; + queuePriorityCreateInfo.pNext = nullptr; + queuePriorityCreateInfo.globalPriority = globalPriority; + queueNextPtr = &queuePriorityCreateInfo; + } else { + // If globalPriorityQuery is enabled, attempting queue creation with an unsupported + // priority will return VK_ERROR_INITIALIZATION_FAILED. + // + // SysUI and Launcher will request HIGH when SF has RT but it is a known issue that + // upstream drm drivers currently lack a way to grant them the granular privileges + // they need for HIGH (but not RT) so they will fail queue creation. + // For now, drop the unsupported global priority request so that queue creation + // succeeds. + // + // Once that is fixed, this should probably be a fatal error indicating an improper + // request or an app needs to get the correct privileges. + ALOGW("Requested context priority is not supported by the queue"); + } } const VkDeviceQueueCreateInfo queueInfo = { diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h index f0425719ea89..a593ec6f8351 100644 --- a/libs/hwui/renderthread/VulkanManager.h +++ b/libs/hwui/renderthread/VulkanManager.h @@ -152,7 +152,7 @@ private: VkPtr<PFN_vkDestroyInstance> mDestroyInstance; VkPtr<PFN_vkEnumeratePhysicalDevices> mEnumeratePhysicalDevices; VkPtr<PFN_vkGetPhysicalDeviceProperties> mGetPhysicalDeviceProperties; - VkPtr<PFN_vkGetPhysicalDeviceQueueFamilyProperties> mGetPhysicalDeviceQueueFamilyProperties; + VkPtr<PFN_vkGetPhysicalDeviceQueueFamilyProperties2> mGetPhysicalDeviceQueueFamilyProperties2; VkPtr<PFN_vkGetPhysicalDeviceFeatures2> mGetPhysicalDeviceFeatures2; VkPtr<PFN_vkGetPhysicalDeviceImageFormatProperties2> mGetPhysicalDeviceImageFormatProperties2; VkPtr<PFN_vkCreateDevice> mCreateDevice; diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index 31f89960836b..ef4c3ef0d321 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -16,6 +16,8 @@ package android.media.projection; +import static android.view.Display.DEFAULT_DISPLAY; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.compat.CompatChanges; @@ -29,6 +31,7 @@ import android.hardware.display.VirtualDisplayConfig; import android.os.Build; import android.os.Handler; import android.os.RemoteException; +import android.os.UserManager; import android.util.ArrayMap; import android.util.Log; import android.util.Slog; @@ -70,6 +73,7 @@ public final class MediaProjection { private final DisplayManager mDisplayManager; @NonNull private final Map<Callback, CallbackRecord> mCallbacks = new ArrayMap<>(); + private final int mDisplayId; /** @hide */ public MediaProjection(Context context, IMediaProjection impl) { @@ -88,6 +92,11 @@ public final class MediaProjection { throw new RuntimeException("Failed to start media projection", e); } mDisplayManager = displayManager; + + final UserManager userManager = context.getSystemService(UserManager.class); + mDisplayId = userManager.isVisibleBackgroundUsersSupported() + ? userManager.getMainDisplayIdAssignedToUser() + : DEFAULT_DISPLAY; } /** @@ -156,6 +165,7 @@ public final class MediaProjection { if (surface != null) { builder.setSurface(surface); } + builder.setDisplayIdToMirror(mDisplayId); return createVirtualDisplay(builder, callback, handler); } @@ -234,6 +244,7 @@ public final class MediaProjection { if (surface != null) { builder.setSurface(surface); } + builder.setDisplayIdToMirror(mDisplayId); return createVirtualDisplay(builder, callback, handler); } diff --git a/media/java/android/media/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig index c814c95e09d9..10423b9c1f0f 100644 --- a/media/java/android/media/tv/flags/media_tv.aconfig +++ b/media/java/android/media/tv/flags/media_tv.aconfig @@ -56,3 +56,11 @@ flag { description: "Enhance HDMI-CEC power state and activeness transitions" bug: "332780751" } + +flag { + name: "media_quality_fw" + is_exported: true + namespace: "media_tv" + description: "Media Quality V1.0 APIs for Android W" + bug: "348412562" +} diff --git a/media/jni/android_media_ImageWriter.cpp b/media/jni/android_media_ImageWriter.cpp index 6776f611559c..33650d91e6a3 100644 --- a/media/jni/android_media_ImageWriter.cpp +++ b/media/jni/android_media_ImageWriter.cpp @@ -735,10 +735,15 @@ static void ImageWriter_queueImage(JNIEnv* env, jobject thiz, jlong nativeCtx, j } static status_t attachAndQeueuGraphicBuffer(JNIEnv* env, JNIImageWriterContext *ctx, - sp<Surface> surface, sp<GraphicBuffer> gb, jlong timestampNs, jint dataSpace, + sp<GraphicBuffer> gb, jlong timestampNs, jint dataSpace, jint left, jint top, jint right, jint bottom, jint transform, jint scalingMode) { status_t res = OK; // Step 1. Attach Image + sp<Surface> surface = ctx->getProducer(); + if (surface == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", + "Producer surface is null, ImageWriter seems already closed"); + } res = surface->attachBuffer(gb.get()); if (res != OK) { ALOGE("Attach image failed: %s (%d)", strerror(-res), res); @@ -835,7 +840,6 @@ static jint ImageWriter_attachAndQueueImage(JNIEnv* env, jobject thiz, jlong nat return -1; } - sp<Surface> surface = ctx->getProducer(); if (isFormatOpaque(ctx->getBufferFormat()) != isFormatOpaque(nativeHalFormat)) { jniThrowException(env, "java/lang/IllegalStateException", "Trying to attach an opaque image into a non-opaque ImageWriter, or vice versa"); @@ -851,7 +855,7 @@ static jint ImageWriter_attachAndQueueImage(JNIEnv* env, jobject thiz, jlong nat return -1; } - return attachAndQeueuGraphicBuffer(env, ctx, surface, buffer->mGraphicBuffer, timestampNs, + return attachAndQeueuGraphicBuffer(env, ctx, buffer->mGraphicBuffer, timestampNs, dataSpace, left, top, right, bottom, transform, scalingMode); } @@ -866,7 +870,6 @@ static jint ImageWriter_attachAndQueueGraphicBuffer(JNIEnv* env, jobject thiz, j return -1; } - sp<Surface> surface = ctx->getProducer(); if (isFormatOpaque(ctx->getBufferFormat()) != isFormatOpaque(nativeHalFormat)) { jniThrowException(env, "java/lang/IllegalStateException", "Trying to attach an opaque image into a non-opaque ImageWriter, or vice versa"); @@ -880,7 +883,8 @@ static jint ImageWriter_attachAndQueueGraphicBuffer(JNIEnv* env, jobject thiz, j "Trying to attach an invalid graphic buffer"); return -1; } - return attachAndQeueuGraphicBuffer(env, ctx, surface, graphicBuffer, timestampNs, + + return attachAndQeueuGraphicBuffer(env, ctx, graphicBuffer, timestampNs, dataSpace, left, top, right, bottom, transform, scalingMode); } diff --git a/native/android/system_fonts.cpp b/native/android/system_fonts.cpp index 91f78ce6f950..0c07b2acbb0c 100644 --- a/native/android/system_fonts.cpp +++ b/native/android/system_fonts.cpp @@ -327,7 +327,7 @@ AFont* _Nonnull AFontMatcher_match( result->mWeight = font->style().weight(); result->mItalic = font->style().slant() == minikin::FontStyle::Slant::ITALIC; result->mCollectionIndex = minikinFontSkia->GetFontIndex(); - const std::vector<minikin::FontVariation>& axes = minikinFontSkia->GetAxes(); + const minikin::VariationSettings& axes = minikinFontSkia->GetAxes(); result->mAxes.reserve(axes.size()); for (auto axis : axes) { result->mAxes.push_back(std::make_pair(axis.axisTag, axis.value)); diff --git a/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml b/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml index fc63c0f9ab93..3e73ebd4880e 100644 --- a/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml +++ b/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml @@ -35,11 +35,13 @@ style="@style/SettingsLibActionButton.Expressive" android:layout_width="@dimen/settingslib_expressive_space_large3" android:layout_height="@dimen/settingslib_expressive_space_medium5" - android:layout_gravity="center_horizontal" /> + android:layout_gravity="center_horizontal" + android:importantForAccessibility="no"/> <TextView android:id="@+id/text1" style="@style/SettingsLibActionButton.Expressive.Label" - android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"/> + android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3" + android:importantForAccessibility="no"/> </LinearLayout> @@ -55,11 +57,13 @@ style="@style/SettingsLibActionButton.Expressive" android:layout_width="@dimen/settingslib_expressive_space_large3" android:layout_height="@dimen/settingslib_expressive_space_medium5" - android:layout_gravity="center_horizontal" /> + android:layout_gravity="center_horizontal" + android:importantForAccessibility="no"/> <TextView android:id="@+id/text2" style="@style/SettingsLibActionButton.Expressive.Label" - android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"/> + android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3" + android:importantForAccessibility="no"/> </LinearLayout> @@ -75,11 +79,13 @@ style="@style/SettingsLibActionButton.Expressive" android:layout_width="@dimen/settingslib_expressive_space_large3" android:layout_height="@dimen/settingslib_expressive_space_medium5" - android:layout_gravity="center_horizontal" /> + android:layout_gravity="center_horizontal" + android:importantForAccessibility="no"/> <TextView android:id="@+id/text3" style="@style/SettingsLibActionButton.Expressive.Label" - android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"/> + android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3" + android:importantForAccessibility="no"/> </LinearLayout> @@ -95,10 +101,12 @@ style="@style/SettingsLibActionButton.Expressive" android:layout_width="@dimen/settingslib_expressive_space_large3" android:layout_height="@dimen/settingslib_expressive_space_medium5" - android:layout_gravity="center_horizontal" /> + android:layout_gravity="center_horizontal" + android:importantForAccessibility="no"/> <TextView android:id="@+id/text4" style="@style/SettingsLibActionButton.Expressive.Label" - android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"/> + android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3" + android:importantForAccessibility="no"/> </LinearLayout> </LinearLayout> diff --git a/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java b/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java index f011039517ce..b2861826a103 100644 --- a/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java +++ b/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java @@ -548,16 +548,18 @@ public class ActionButtonsPreference extends Preference { if (mButton instanceof MaterialButton) { ((MaterialButton) mButton).setIcon(mIcon); } + mButton.setEnabled(mIsEnabled); + mActionLayout.setOnClickListener(mListener); + mActionLayout.setEnabled(mIsEnabled); + mActionLayout.setContentDescription(mText); } else { mButton.setText(mText); mButton.setCompoundDrawablesWithIntrinsicBounds( null /* left */, mIcon /* top */, null /* right */, null /* bottom */); + mButton.setOnClickListener(mListener); + mButton.setEnabled(mIsEnabled); } - mButton.setOnClickListener(mListener); - mButton.setEnabled(mIsEnabled); - - if (shouldBeVisible()) { mButton.setVisibility(View.VISIBLE); if (mIsExpressive) { diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index 0cb85d8638b0..06f471e91d1d 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -32,6 +32,8 @@ android_library { "SettingsLibBannerMessagePreference", "SettingsLibBarChartPreference", "SettingsLibButtonPreference", + "SettingsLibBulletPreference", + "SettingsLibCardPreference", "SettingsLibCollapsingToolbarBaseActivity", "SettingsLibDeviceStateRotationLock", "SettingsLibDisplayUtils", @@ -40,6 +42,7 @@ android_library { "SettingsLibFooterPreference", "SettingsLibHelpUtils", "SettingsLibIllustrationPreference", + "SettingsLibIntroPreference", "SettingsLibLayoutPreference", "SettingsLibMainSwitchPreference", "SettingsLibProfileSelector", @@ -53,6 +56,7 @@ android_library { "SettingsLibTwoTargetPreference", "SettingsLibUsageProgressBarPreference", "SettingsLibUtils", + "SettingsLibZeroStatePreference", "settingslib_media_flags_lib", "settingslib_flags_lib", ], diff --git a/packages/SettingsLib/AppPreference/res/layout-v33/preference_app.xml b/packages/SettingsLib/AppPreference/res/layout-v33/preference_app.xml index 47ce58735048..b06052ad6a00 100644 --- a/packages/SettingsLib/AppPreference/res/layout-v33/preference_app.xml +++ b/packages/SettingsLib/AppPreference/res/layout-v33/preference_app.xml @@ -78,15 +78,6 @@ android:textAppearance="?android:attr/textAppearanceSmall" android:textColor="?android:attr/textColorSecondary" android:visibility="gone"/> - - <ProgressBar - android:id="@android:id/progress" - style="?android:attr/progressBarStyleHorizontal" - android:layout_width="match_parent" - android:layout_height="4dp" - android:layout_marginTop="4dp" - android:max="100" - android:visibility="gone"/> </LinearLayout> <LinearLayout diff --git a/packages/SettingsLib/AppPreference/res/layout/preference_app.xml b/packages/SettingsLib/AppPreference/res/layout/preference_app.xml index e65f7de2466a..ac572280c5ad 100644 --- a/packages/SettingsLib/AppPreference/res/layout/preference_app.xml +++ b/packages/SettingsLib/AppPreference/res/layout/preference_app.xml @@ -74,15 +74,6 @@ android:textAppearance="?android:attr/textAppearanceSmall" android:textColor="?android:attr/textColorSecondary" android:visibility="gone"/> - - <ProgressBar - android:id="@android:id/progress" - style="?android:attr/progressBarStyleHorizontal" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="4dp" - android:max="100" - android:visibility="gone"/> </LinearLayout> <LinearLayout diff --git a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java index f1d162e116b5..3b52df7e5fbb 100644 --- a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java +++ b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java @@ -18,11 +18,8 @@ package com.android.settingslib.widget; import android.content.Context; import android.util.AttributeSet; -import android.view.View; -import android.widget.ProgressBar; import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import com.android.settingslib.widget.preference.app.R; @@ -31,9 +28,6 @@ import com.android.settingslib.widget.preference.app.R; */ public class AppPreference extends Preference { - private int mProgress; - private boolean mProgressVisible; - public AppPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); setLayoutResource(R.layout.preference_app); @@ -53,29 +47,4 @@ public class AppPreference extends Preference { super(context, attrs); setLayoutResource(R.layout.preference_app); } - - /** - * Sets the current progress. - * @param amount the current progress - * - * @see ProgressBar#setProgress(int) - */ - public void setProgress(int amount) { - mProgress = amount; - mProgressVisible = true; - notifyChanged(); - } - - @Override - public void onBindViewHolder(PreferenceViewHolder view) { - super.onBindViewHolder(view); - - final ProgressBar progress = (ProgressBar) view.findViewById(android.R.id.progress); - if (mProgressVisible) { - progress.setProgress(mProgress); - progress.setVisibility(View.VISIBLE); - } else { - progress.setVisibility(View.GONE); - } - } } diff --git a/packages/SettingsLib/BulletPreference/Android.bp b/packages/SettingsLib/BulletPreference/Android.bp new file mode 100644 index 000000000000..3ea0b2b4851e --- /dev/null +++ b/packages/SettingsLib/BulletPreference/Android.bp @@ -0,0 +1,33 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_library { + name: "SettingsLibBulletPreference", + use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], + + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + resource_dirs: ["res"], + + static_libs: [ + "androidx.annotation_annotation", + "androidx.preference_preference", + "SettingsLibSettingsTheme", + ], + sdk_version: "system_current", + min_sdk_version: "21", + apex_available: [ + "//apex_available:platform", + ], +} diff --git a/packages/SettingsLib/BulletPreference/AndroidManifest.xml b/packages/SettingsLib/BulletPreference/AndroidManifest.xml new file mode 100644 index 000000000000..c7495eff14d2 --- /dev/null +++ b/packages/SettingsLib/BulletPreference/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.settingslib.widget.preference.bullet"> + + <uses-sdk android:minSdkVersion="21" /> + +</manifest> diff --git a/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_icon_frame.xml b/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_icon_frame.xml new file mode 100644 index 000000000000..030f02430c02 --- /dev/null +++ b/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_icon_frame.xml @@ -0,0 +1,37 @@ +<?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" + android:id="@+id/icon_frame" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="@dimen/settingslib_expressive_space_medium4" + android:gravity="top|start" + android:layout_marginTop="@dimen/settingslib_expressive_space_small1" + android:paddingStart="@dimen/settingslib_expressive_space_extrasmall4" + android:paddingEnd="@dimen/settingslib_expressive_space_small1"> + + <androidx.preference.internal.PreferenceImageView + android:id="@android:id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:maxWidth="@dimen/settingslib_expressive_space_small4" + app:maxHeight="@dimen/settingslib_expressive_space_small4"/> + +</LinearLayout>
\ No newline at end of file diff --git a/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_preference.xml b/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_preference.xml new file mode 100644 index 000000000000..3f37f6cc00a5 --- /dev/null +++ b/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_preference.xml @@ -0,0 +1,43 @@ +<?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:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:background="?android:attr/selectableItemBackground" + android:clipToPadding="false" + android:baselineAligned="false"> + + <include layout="@layout/settingslib_expressive_bullet_icon_frame"/> + + <include layout="@layout/settingslib_expressive_preference_text_frame"/> + + <!-- Preference should place its actual preference widget here. --> + <LinearLayout + android:id="@android:id/widget_frame" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="end|center_vertical" + android:paddingStart="@dimen/settingslib_expressive_space_small1" + android:paddingEnd="0dp" + android:orientation="vertical"/> + +</LinearLayout>
\ No newline at end of file diff --git a/packages/SettingsLib/BulletPreference/src/com/android/settingslib/widget/BulletPreference.kt b/packages/SettingsLib/BulletPreference/src/com/android/settingslib/widget/BulletPreference.kt new file mode 100644 index 000000000000..45e2e9aaea4b --- /dev/null +++ b/packages/SettingsLib/BulletPreference/src/com/android/settingslib/widget/BulletPreference.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.settingslib.widget + +import android.content.Context +import android.util.AttributeSet +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder + +import com.android.settingslib.widget.preference.bullet.R + +/** + * The BulletPreference shows a text which describe a feature. + */ +class BulletPreference @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : Preference(context, attrs, defStyleAttr, defStyleRes) { + + init { + layoutResource = R.layout.settingslib_expressive_bullet_preference + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + holder.isDividerAllowedAbove = false + holder.isDividerAllowedBelow = false + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml new file mode 100644 index 000000000000..f55b320269a8 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled.xml @@ -0,0 +1,29 @@ +<?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:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Filled" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml new file mode 100644 index 000000000000..b663b6ccc5bf --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_extra.xml @@ -0,0 +1,29 @@ +<?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:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Filled.Extra" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml new file mode 100644 index 000000000000..784e6ad6a9f8 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_filled_large.xml @@ -0,0 +1,29 @@ +<?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:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Filled.Large" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml new file mode 100644 index 000000000000..8b44a6539801 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline.xml @@ -0,0 +1,29 @@ +<?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:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Outline" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml new file mode 100644 index 000000000000..f8a2d8fbd975 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_extra.xml @@ -0,0 +1,29 @@ +<?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:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Outline.Extra" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml new file mode 100644 index 000000000000..781a5a136164 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_outline_large.xml @@ -0,0 +1,29 @@ +<?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:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Outline.Large" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml new file mode 100644 index 000000000000..5b568f870ea4 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal.xml @@ -0,0 +1,29 @@ +<?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:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Tonal" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml new file mode 100644 index 000000000000..1e7a08b714f1 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_extra.xml @@ -0,0 +1,29 @@ +<?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:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Tonal.Extra" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml new file mode 100644 index 000000000000..42116be07041 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/layout-v35/settingslib_expressive_button_tonal_large.xml @@ -0,0 +1,29 @@ +<?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:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/settingslib_button" + style="@style/SettingsLibButtonStyle.Expressive.Tonal.Large" /> + +</LinearLayout> diff --git a/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml b/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml new file mode 100644 index 000000000000..a1761e55f1e0 --- /dev/null +++ b/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.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. +--> + +<resources> + <declare-styleable name="ButtonPreference"> + <attr name="buttonType" format="enum"> + <enum name="filled" value="0"/> + <enum name="tonal" value="1"/> + <enum name="outline" value="2"/> + </attr> + <attr name="buttonSize" format="enum"> + <enum name="normal" value="0"/> + <enum name="large" value="1"/> + <enum name="extra" value="2"/> + </attr> + </declare-styleable> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java b/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java index 16ba96265751..0041eb2c7072 100644 --- a/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java +++ b/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java @@ -32,11 +32,44 @@ import androidx.preference.PreferenceViewHolder; import com.android.settingslib.widget.preference.button.R; +import com.google.android.material.button.MaterialButton; + /** * A preference handled a button */ public class ButtonPreference extends Preference { + enum ButtonStyle { + FILLED_NORMAL(0, 0, R.layout.settingslib_expressive_button_filled), + FILLED_LARGE(0, 1, R.layout.settingslib_expressive_button_filled_large), + FILLED_EXTRA(0, 2, R.layout.settingslib_expressive_button_filled_extra), + TONAL_NORMAL(1, 0, R.layout.settingslib_expressive_button_tonal), + TONAL_LARGE(1, 1, R.layout.settingslib_expressive_button_tonal_large), + TONAL_EXTRA(1, 2, R.layout.settingslib_expressive_button_tonal_extra), + OUTLINE_NORMAL(2, 0, R.layout.settingslib_expressive_button_outline), + OUTLINE_LARGE(2, 1, R.layout.settingslib_expressive_button_outline_large), + OUTLINE_EXTRA(2, 2, R.layout.settingslib_expressive_button_outline_extra); + + private final int mType; + private final int mSize; + private final int mLayoutId; + + ButtonStyle(int type, int size, int layoutId) { + this.mType = type; + this.mSize = size; + this.mLayoutId = layoutId; + } + + static int getLayoutId(int type, int size) { + for (ButtonStyle style : values()) { + if (style.mType == type && style.mSize == size) { + return style.mLayoutId; + } + } + throw new IllegalArgumentException(); + } + } + private static final int ICON_SIZE = 24; private View.OnClickListener mClickListener; @@ -86,7 +119,7 @@ public class ButtonPreference extends Preference { } private void init(Context context, AttributeSet attrs, int defStyleAttr) { - setLayoutResource(R.layout.settingslib_button_layout); + int resId = R.layout.settingslib_button_layout; if (attrs != null) { TypedArray a = context.obtainStyledAttributes(attrs, @@ -102,8 +135,16 @@ public class ButtonPreference extends Preference { R.styleable.ButtonPreference, defStyleAttr, 0 /*defStyleRes*/); mGravity = a.getInt(R.styleable.ButtonPreference_android_gravity, Gravity.START); + + if (SettingsThemeHelper.isExpressiveTheme(context)) { + int type = a.getInt(R.styleable.ButtonPreference_buttonType, 0); + int size = a.getInt(R.styleable.ButtonPreference_buttonSize, 0); + resId = ButtonStyle.getLayoutId(type, size); + } a.recycle(); } + + setLayoutResource(resId); } @Override @@ -144,14 +185,20 @@ public class ButtonPreference extends Preference { if (mButton == null || icon == null) { return; } - //get pixel from dp - int size = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, ICON_SIZE, - getContext().getResources().getDisplayMetrics()); - icon.setBounds(0, 0, size, size); - - //set drawableStart - mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null/* top */, null/* end */, - null/* bottom */); + + if (mButton instanceof MaterialButton) { + ((MaterialButton) mButton).setIcon(icon); + } else { + //get pixel from dp + int size = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, ICON_SIZE, + getContext().getResources().getDisplayMetrics()); + icon.setBounds(0, 0, size, size); + + //set drawableStart + mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null/* top */, + null/* end */, + null/* bottom */); + } } @Override diff --git a/packages/SettingsLib/CardPreference/Android.bp b/packages/SettingsLib/CardPreference/Android.bp new file mode 100644 index 000000000000..1d871d168ee5 --- /dev/null +++ b/packages/SettingsLib/CardPreference/Android.bp @@ -0,0 +1,33 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_library { + name: "SettingsLibCardPreference", + use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], + + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + resource_dirs: ["res"], + + static_libs: [ + "androidx.annotation_annotation", + "androidx.preference_preference", + "SettingsLibSettingsTheme", + ], + sdk_version: "system_current", + min_sdk_version: "21", + apex_available: [ + "//apex_available:platform", + ], +} diff --git a/packages/SettingsLib/CardPreference/AndroidManifest.xml b/packages/SettingsLib/CardPreference/AndroidManifest.xml new file mode 100644 index 000000000000..717f66e0296c --- /dev/null +++ b/packages/SettingsLib/CardPreference/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.settingslib.widget.preference.card"> + + <uses-sdk android:minSdkVersion="21" /> + +</manifest> diff --git a/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml b/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml new file mode 100644 index 000000000000..716ed412eb5c --- /dev/null +++ b/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml @@ -0,0 +1,88 @@ +<?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. +--> +<com.google.android.material.card.MaterialCardView + xmlns:android="http://schemas.android.com/apk/res/android" + style="@style/SettingsLibCardStyle"> + + <LinearLayout + android:id="@+id/card_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:baselineAligned="false" + android:minHeight="@dimen/settingslib_expressive_space_large3" + android:paddingStart="@dimen/settingslib_expressive_space_small1" + android:paddingEnd="@dimen/settingslib_expressive_space_small1" + android:orientation="horizontal" + android:gravity="center_vertical"> + + <LinearLayout + android:id="@+id/icon_frame" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="@dimen/settingslib_expressive_space_medium3" + android:minHeight="@dimen/settingslib_expressive_space_medium3" + android:gravity="center" + android:orientation="horizontal"> + + <ImageView + android:id="@android:id/icon" + android:src="@drawable/settingslib_arrow_drop_down" + android:layout_width="@dimen/settingslib_expressive_space_medium3" + android:layout_height="@dimen/settingslib_expressive_space_medium3" + android:scaleType="centerInside"/> + + </LinearLayout> + + <LinearLayout + android:id="@+id/text_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingHorizontal="@dimen/settingslib_expressive_space_small1" + android:paddingVertical="@dimen/settingslib_expressive_space_small2" + android:orientation="vertical"> + + <TextView + android:id="@android:id/title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hyphenationFrequency="normalFast" + android:lineBreakWordStyle="phrase" + android:textAppearance="@style/TextAppearance.CardTitle.SettingsLib"/> + + <TextView + android:id="@android:id/summary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hyphenationFrequency="normalFast" + android:lineBreakWordStyle="phrase" + android:textAppearance="@style/TextAppearance.CardSummary.SettingsLib"/> + + </LinearLayout> + + <ImageView + android:id="@android:id/closeButton" + android:layout_width="@dimen/settingslib_expressive_space_medium4" + android:layout_height="@dimen/settingslib_expressive_space_medium4" + android:padding="@dimen/settingslib_expressive_space_extrasmall4" + android:layout_gravity="center" + android:src="@drawable/settingslib_expressive_icon_close" + android:background="?android:attr/selectableItemBackground" /> + + </LinearLayout> + +</com.google.android.material.card.MaterialCardView>
\ No newline at end of file diff --git a/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml b/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml new file mode 100644 index 000000000000..4cbdea52d439 --- /dev/null +++ b/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml @@ -0,0 +1,30 @@ +<?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> + <style name="TextAppearance.CardTitle.SettingsLib" + parent="@style/TextAppearance.PreferenceTitle.SettingsLib"> + <item name="android:textColor">@color/settingslib_materialColorOnPrimary</item> + <item name="android:textSize">20sp</item> + </style> + + <style name="TextAppearance.CardSummary.SettingsLib" + parent="@style/TextAppearance.PreferenceSummary.SettingsLib"> + <item name="android:textColor">@color/settingslib_materialColorOnSecondary</item> + <item name="android:textSize">14sp</item> + </style> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt b/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt new file mode 100644 index 000000000000..eb14746a0f22 --- /dev/null +++ b/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settingslib.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import com.android.settingslib.widget.preference.card.R + +/** + * The CardPreference shows a card like suggestion in homepage, which also support dismiss. + */ +class CardPreference @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : Preference(context, attrs, defStyleAttr, defStyleRes) { + + init { + layoutResource = R.layout.settingslib_expressive_preference_card + } + private var dismissible = false + set(value) { + if (field != value) { + field = value + notifyChanged() + } + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + holder.isDividerAllowedBelow = false + holder.isDividerAllowedAbove = false + + holder.findViewById(android.R.id.closeButton)?.let { dismissButton -> + dismissButton.visibility = if (dismissible) View.VISIBLE else View.GONE + dismissButton.setOnClickListener { + isVisible = false + } + } + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/IntroPreference/Android.bp b/packages/SettingsLib/IntroPreference/Android.bp new file mode 100644 index 000000000000..155db186c702 --- /dev/null +++ b/packages/SettingsLib/IntroPreference/Android.bp @@ -0,0 +1,33 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_library { + name: "SettingsLibIntroPreference", + use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], + + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + resource_dirs: ["res"], + + static_libs: [ + "androidx.preference_preference", + "SettingsLibSettingsTheme", + ], + + sdk_version: "system_current", + min_sdk_version: "21", + apex_available: [ + "//apex_available:platform", + ], +} diff --git a/packages/SettingsLib/IntroPreference/AndroidManifest.xml b/packages/SettingsLib/IntroPreference/AndroidManifest.xml new file mode 100644 index 000000000000..f1bfee5524e7 --- /dev/null +++ b/packages/SettingsLib/IntroPreference/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.settingslib.widget.preference.intro"> + + <uses-sdk android:minSdkVersion="21" /> + +</manifest> diff --git a/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml b/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml new file mode 100644 index 000000000000..203a395c3e98 --- /dev/null +++ b/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml @@ -0,0 +1,45 @@ +<?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. + --> + +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/entity_header" + style="@style/SettingsLibEntityHeader"> + + <LinearLayout + android:id="@+id/entity_header_content" + style="@style/SettingsLibEntityHeaderContent"> + + <ImageView + android:id="@android:id/icon" + android:src="@drawable/settingslib_arrow_drop_down" + style="@style/SettingsLibEntityHeaderIcon"/> + + <TextView + android:id="@android:id/title" + android:text="Title" + style="@style/SettingsLibEntityHeaderTitle"/> + + <com.android.settingslib.widget.CollapsableTextView + android:id="@+id/collapsable_summary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center"/> + + </LinearLayout> + +</RelativeLayout> diff --git a/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt new file mode 100644 index 000000000000..c93ec2bd0492 --- /dev/null +++ b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.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.settingslib.widget + +import android.content.Context +import android.os.Build +import android.util.AttributeSet +import androidx.annotation.RequiresApi +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import com.android.settingslib.widget.preference.intro.R + +class IntroPreference @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : Preference(context, attrs, defStyleAttr, defStyleRes) { + + private var isCollapsable: Boolean = false + private var minLines: Int = 2 + + init { + layoutResource = R.layout.settingslib_expressive_preference_intro + isSelectable = false + + initAttributes(context, attrs, defStyleAttr) + } + + private fun initAttributes(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { + context.obtainStyledAttributes( + attrs, + COLLAPSABLE_TEXT_VIEW_ATTRS, defStyleAttr, 0 + ).apply { + isCollapsable = getBoolean(IS_COLLAPSABLE, false) + minLines = getInt( + MIN_LINES, + if (isCollapsable) DEFAULT_MIN_LINES else DEFAULT_MAX_LINES + ).coerceIn(1, DEFAULT_MAX_LINES) + recycle() + } + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + holder.isDividerAllowedBelow = false + holder.isDividerAllowedAbove = false + + (holder.findViewById(R.id.collapsable_summary) as? CollapsableTextView)?.apply { + setCollapsable(isCollapsable) + setMinLines(minLines) + setText(summary.toString()) + } + } + + /** + * Sets whether the summary is collapsable. + * @param collapsable True if the summary should be collapsable, false otherwise. + */ + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + fun setCollapsable(collapsable: Boolean) { + isCollapsable = collapsable + minLines = if (isCollapsable) DEFAULT_MIN_LINES else DEFAULT_MAX_LINES + notifyChanged() + } + + /** + * Sets the minimum number of lines to display when collapsed. + * @param lines The minimum number of lines. + */ + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + fun setMinLines(lines: Int) { + minLines = lines.coerceIn(1, DEFAULT_MAX_LINES) + notifyChanged() + } + + companion object { + private const val DEFAULT_MAX_LINES = 10 + private const val DEFAULT_MIN_LINES = 2 + + private val COLLAPSABLE_TEXT_VIEW_ATTRS = + com.android.settingslib.widget.theme.R.styleable.CollapsableTextView + private val MIN_LINES = + com.android.settingslib.widget.theme.R.styleable.CollapsableTextView_android_minLines + private val IS_COLLAPSABLE = + com.android.settingslib.widget.theme.R.styleable.CollapsableTextView_isCollapsable + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/Preference/Android.bp b/packages/SettingsLib/Preference/Android.bp index 17852e8e7ece..e83e17cc8375 100644 --- a/packages/SettingsLib/Preference/Android.bp +++ b/packages/SettingsLib/Preference/Android.bp @@ -22,3 +22,16 @@ android_library { ], kotlincflags: ["-Xjvm-default=all"], } + +android_library { + name: "SettingsLibPreference-testutils", + srcs: ["testutils/**/*.kt"], + static_libs: [ + "SettingsLibPreference", + "androidx.fragment_fragment-testing", + "androidx.test.core", + "androidx.test.ext.junit", + "flag-junit", + "truth", + ], +} diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt index 5fcf4784f43b..5e6989546cb9 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt @@ -68,10 +68,13 @@ interface PreferenceBinding { preference.icon = null } val context = preference.context + val isPreferenceScreen = preference is PreferenceScreen preference.peekExtras()?.clear() extras(context)?.let { preference.extras.putAll(it) } preference.title = getPreferenceTitle(context) - preference.summary = getPreferenceSummary(context) + if (!isPreferenceScreen) { + preference.summary = getPreferenceSummary(context) + } preference.isEnabled = isEnabled(context) preference.isVisible = (this as? PreferenceAvailabilityProvider)?.isAvailable(context) != false @@ -81,7 +84,7 @@ interface PreferenceBinding { // dependency here. This simplifies dependency management and avoid the // IllegalStateException when call Preference.setDependency preference.dependency = null - if (preference !is PreferenceScreen) { // avoid recursive loop when build graph + if (!isPreferenceScreen) { // avoid recursive loop when build graph preference.fragment = (this as? PreferenceScreenCreator)?.fragmentClass()?.name preference.intent = intent(context) } diff --git a/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt new file mode 100644 index 000000000000..4d5f85fa9020 --- /dev/null +++ b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt @@ -0,0 +1,123 @@ +/* + * 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 android.platform.test.flag.junit.SetFlagsRule +import android.util.Log +import androidx.fragment.app.testing.FragmentScenario +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceGroup +import androidx.preference.PreferenceScreen +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** Test case for catalyst screen. */ +@RunWith(AndroidJUnit4::class) +abstract class CatalystScreenTestCase { + @get:Rule val setFlagsRule = SetFlagsRule() + + protected val context: Context = ApplicationProvider.getApplicationContext() + + /** Catalyst screen. */ + protected abstract val preferenceScreenCreator: PreferenceScreenCreator + + /** Flag to control catalyst screen. */ + protected abstract val flagName: String + + /** + * Test to compare the preference screen hierarchy between legacy screen (flag is disabled) and + * catalyst screen (flag is enabled). + */ + @Test + fun migration() { + enableCatalystScreen() + assertThat(preferenceScreenCreator.isFlagEnabled(context)).isTrue() + val catalystScreen = stringifyPreferenceScreen() + Log.i("Catalyst", catalystScreen) + + disableCatalystScreen() + assertThat(preferenceScreenCreator.isFlagEnabled(context)).isFalse() + val legacyScreen = stringifyPreferenceScreen() + + assertThat(catalystScreen).isEqualTo(legacyScreen) + } + + /** + * Enables the catalyst screen. + * + * By default, enable the [flagName]. Override for more complex situation. + */ + @Suppress("DEPRECATION") + protected open fun enableCatalystScreen() { + setFlagsRule.enableFlags(flagName) + } + + /** + * Disables the catalyst screen (legacy screen is shown). + * + * By default, disable the [flagName]. Override for more complex situation. + */ + @Suppress("DEPRECATION") + protected open fun disableCatalystScreen() { + setFlagsRule.disableFlags(flagName) + } + + private fun stringifyPreferenceScreen(): String { + @Suppress("UNCHECKED_CAST") + val clazz = preferenceScreenCreator.fragmentClass() as Class<PreferenceFragmentCompat> + val builder = StringBuilder() + FragmentScenario.launch(clazz).use { + it.onFragment { fragment -> fragment.preferenceScreen.toString(builder) } + } + return builder.toString() + } + + private fun Preference.toString(builder: StringBuilder, indent: String = "") { + val clazz = javaClass + builder.append(indent).append(clazz).append(" {\n") + val indent2 = "$indent " + if (clazz != PreferenceScreen::class.java) { + key?.let { builder.append(indent2).append("key: \"$it\"\n") } + } + title?.let { builder.append(indent2).append("title: \"$it\"\n") } + summary?.let { builder.append(indent2).append("summary: \"$it\"\n") } + fragment?.let { builder.append(indent2).append("fragment: \"$it\"\n") } + builder.append(indent2).append("order: $order\n") + builder.append(indent2).append("isCopyingEnabled: $isCopyingEnabled\n") + builder.append(indent2).append("isEnabled: $isEnabled\n") + builder.append(indent2).append("isIconSpaceReserved: $isIconSpaceReserved\n") + if (clazz != Preference::class.java && clazz != PreferenceScreen::class.java) { + builder.append(indent2).append("isPersistent: $isPersistent\n") + } + builder.append(indent2).append("isSelectable: $isSelectable\n") + if (this is PreferenceGroup) { + val count = preferenceCount + builder.append(indent2).append("preferenceCount: $count\n") + val indent4 = "$indent2 " + for (index in 0..<count) { + getPreference(index).toString(builder, indent4) + } + } + builder.append(indent).append("}\n") + } +} diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_collapse.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_collapse.xml new file mode 100644 index 000000000000..161ece73f21c --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_collapse.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. + --> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <shape + android:shape="oval"> + <size android:width="24dp" android:height="24dp"/> + <solid android:color="@color/settingslib_materialColorSurfaceDim"/> + </shape> + </item> + <item> + <vector + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960"> + <path + android:fillColor="@color/settingslib_materialColorOnSurface" + android:pathData="M480,432L296,616L240,560L480,320L720,560L664,616L480,432Z"/> + </vector> + </item> +</layer-list>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_expand.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_expand.xml new file mode 100644 index 000000000000..1b5d5182d9b2 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_expressive_icon_expand.xml @@ -0,0 +1,37 @@ +<?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. + --> + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <shape + android:shape="oval"> + <size android:width="24dp" android:height="24dp"/> + <solid android:color="@color/settingslib_materialColorSurfaceDim"/> + </shape> + </item> + <item> + <vector + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960"> + <path + android:fillColor="@color/settingslib_materialColorOnSurface" + android:pathData="M480,616L240,376L296,320L480,504L664,320L720,376L480,616Z"/> + </vector> + </item> +</layer-list>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_icon_bullet_start.xml b/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_icon_bullet_start.xml new file mode 100644 index 000000000000..9216c9615aaa --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_icon_bullet_start.xml @@ -0,0 +1,26 @@ +<?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="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?android:attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M354,673L480,597L606,674L573,530L684,434L538,421L480,285L422,420L276,433L387,530L354,673ZM233,840L298,559L80,370L368,345L480,80L592,345L880,370L662,559L727,840L480,691L233,840ZM480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490Z"/> +</vector>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference.xml b/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference.xml index dda7517cc1c3..952562e3d8ea 100644 --- a/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference.xml +++ b/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference.xml @@ -31,37 +31,7 @@ <include layout="@layout/settingslib_icon_frame"/> - <RelativeLayout - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - android:paddingTop="16dp" - android:paddingBottom="16dp"> - - <TextView - android:id="@android:id/title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="start" - android:textAlignment="viewStart" - android:maxLines="2" - android:textAppearance="?android:attr/textAppearanceListItem" - android:ellipsize="marquee"/> - - <TextView - android:id="@android:id/summary" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_below="@android:id/title" - android:layout_alignLeft="@android:id/title" - android:layout_alignStart="@android:id/title" - android:layout_gravity="start" - android:textAlignment="viewStart" - android:textColor="?android:attr/textColorSecondary" - android:maxLines="10" - style="@style/PreferenceSummaryTextStyle"/> - - </RelativeLayout> + <include layout="@layout/settingslib_preference_frame"/> <!-- Preference should place its actual preference widget here. --> <LinearLayout diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference_frame.xml b/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference_frame.xml new file mode 100644 index 000000000000..433d26445c4d --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference_frame.xml @@ -0,0 +1,51 @@ +<?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. + --> + +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingTop="16dp" + android:paddingBottom="16dp"> + + <TextView + android:id="@android:id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:textAlignment="viewStart" + android:text="Title" + android:maxLines="2" + android:textAppearance="?android:attr/textAppearanceListItem" + android:ellipsize="marquee"/> + + <TextView + android:id="@android:id/summary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@android:id/title" + android:layout_alignLeft="@android:id/title" + android:layout_alignStart="@android:id/title" + android:text="Summary summary summary" + android:layout_gravity="start" + android:textAlignment="viewStart" + android:textColor="?android:attr/textColorSecondary" + android:maxLines="10" + style="@style/PreferenceSummaryTextStyle"/> + +</RelativeLayout>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml index fedcc77ed6b9..4e23b6562e3e 100644 --- a/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml +++ b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml @@ -31,41 +31,7 @@ <include layout="@layout/settingslib_icon_frame"/> - <RelativeLayout - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - android:paddingTop="16dp" - android:paddingBottom="16dp"> - - <TextView - android:id="@android:id/title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="start" - android:textAlignment="viewStart" - android:maxLines="2" - android:hyphenationFrequency="normalFast" - android:lineBreakWordStyle="phrase" - android:textAppearance="?android:attr/textAppearanceListItem" - android:ellipsize="marquee"/> - - <TextView - android:id="@android:id/summary" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_below="@android:id/title" - android:layout_alignLeft="@android:id/title" - android:layout_alignStart="@android:id/title" - android:layout_gravity="start" - android:textAlignment="viewStart" - android:textColor="?android:attr/textColorSecondary" - android:maxLines="10" - android:hyphenationFrequency="normalFast" - android:lineBreakWordStyle="phrase" - style="@style/PreferenceSummaryTextStyle"/> - - </RelativeLayout> + <include layout="@layout/settingslib_preference_frame"/> <!-- Preference should place its actual preference widget here. --> <LinearLayout diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference_frame.xml b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference_frame.xml new file mode 100644 index 000000000000..f93e1b975eb2 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference_frame.xml @@ -0,0 +1,55 @@ +<?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. + --> + +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingTop="16dp" + android:paddingBottom="16dp"> + + <TextView + android:id="@android:id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:textAlignment="viewStart" + android:text="Title" + android:maxLines="2" + android:hyphenationFrequency="normalFast" + android:lineBreakWordStyle="phrase" + android:textAppearance="?android:attr/textAppearanceListItem" + android:ellipsize="marquee"/> + + <TextView + android:id="@android:id/summary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@android:id/title" + android:layout_alignLeft="@android:id/title" + android:layout_alignStart="@android:id/title" + android:layout_gravity="start" + android:textAlignment="viewStart" + android:textColor="?android:attr/textColorSecondary" + android:maxLines="10" + android:text="Summary summary summary" + android:hyphenationFrequency="normalFast" + android:lineBreakWordStyle="phrase" + style="@style/PreferenceSummaryTextStyle"/> + +</RelativeLayout>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_collapsable_textview.xml b/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_collapsable_textview.xml new file mode 100644 index 000000000000..245d3682636b --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_collapsable_textview.xml @@ -0,0 +1,51 @@ +<?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. + --> + +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingBottom="@dimen/settingslib_expressive_space_small1" + android:paddingTop="@dimen/settingslib_expressive_space_extrasmall4" + android:orientation="vertical" + android:animateLayoutChanges="true" + android:background="?android:attr/selectableItemBackground" + android:clipToPadding="false"> + + <TextView + android:id="@android:id/title" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:textAlignment="viewStart" + android:clickable="false" + android:longClickable="false" + android:maxLines="10" + android:ellipsize="end" + android:textAppearance="@style/TextAppearance.TopIntroText"/> + + <com.google.android.material.button.MaterialButton + android:id="@+id/collapse_button" + app:layout_constraintTop_toBottomOf="@android:id/title" + app:layout_constraintStart_toStartOf="parent" + android:text="@string/settingslib_expressive_text_expand" + app:icon="@drawable/settingslib_expressive_icon_expand" + style="@style/SettingslibTextButtonStyle.Expressive"/> +</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/attrs_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/attrs_expressive.xml new file mode 100644 index 000000000000..857dd7953234 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/values-v35/attrs_expressive.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. +--> + +<resources> + <declare-styleable name="CollapsableTextView"> + <attr name="android:gravity"/> + <!-- The minimum number of lines when the textView collapsed. --> + <attr name="android:minLines"/> + <!-- Specifies that the textView is collapsable. --> + <attr name="isCollapsable" format="boolean"/> + </declare-styleable> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/strings.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/strings.xml new file mode 100644 index 000000000000..22734068733a --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/values-v35/strings.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + + <!-- text of button to indicate user the textView is expandable [CHAR LIMIT=NONE] --> + <string name="settingslib_expressive_text_expand">Expand</string> + <!-- text of button to indicate user the textView is collapsable [CHAR LIMIT=NONE] --> + <string name="settingslib_expressive_text_collapse">Collapse</string> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml index 816433c1a18b..9c659050b15e 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml @@ -170,6 +170,52 @@ <item name="thumbIcon">@drawable/settingslib_expressive_switch_thumb_icon</item> </style> + <style name="SettingslibMainSwitchStyle.Expressive" parent="SettingslibSwitchStyle.Expressive"> + <item name="android:layout_gravity">center</item> + <item name="trackTint">@color/settingslib_expressive_color_main_switch_track</item> + </style> + + <style name="SettingsLibCardStyle" parent=""> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_marginHorizontal">?android:attr/listPreferredItemPaddingStart</item> + <item name="android:layout_marginVertical">@dimen/settingslib_expressive_space_extrasmall4</item> + <item name="cardBackgroundColor">@color/settingslib_materialColorPrimary</item> + <item name="cardCornerRadius">@dimen/settingslib_expressive_radius_extralarge3</item> + <item name="cardElevation">0dp</item> + <item name="rippleColor">?android:attr/colorControlHighlight</item> + </style> + + <style name="SettingsLibButtonStyle.Expressive.Filled" + parent="@style/Widget.Material3.Button"> + <item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:gravity">center</item> + <item name="android:minWidth">@dimen/settingslib_expressive_space_medium4</item> + <item name="android:minHeight">@dimen/settingslib_expressive_space_medium4</item> + <item name="android:paddingVertical">@dimen/settingslib_expressive_space_extrasmall5</item> + <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small1</item> + <item name="android:backgroundTint">@color/settingslib_materialColorPrimary</item> + <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item> + <item name="android:textColor">@color/settingslib_materialColorOnPrimary</item> + <item name="android:textSize">14sp</item> + <item name="iconGravity">textStart</item> + <item name="iconTint">@color/settingslib_materialColorOnPrimary</item> + <item name="iconSize">@dimen/settingslib_expressive_space_small4</item> + </style> + + <style name="SettingsLibButtonStyle.Expressive.Filled.Large"> + <item name="android:paddingVertical">@dimen/settingslib_expressive_space_small1</item> + <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small4</item> + <item name="android:textSize">16sp</item> + </style> + + <style name="SettingsLibButtonStyle.Expressive.Filled.Extra" + parent="@style/SettingsLibButtonStyle.Expressive.Filled.Large"> + <item name="android:layout_width">match_parent</item> + </style> + <style name="SettingsLibButtonStyle.Expressive.Tonal" parent="@style/Widget.Material3.Button.TonalButton"> <item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item> @@ -189,8 +235,98 @@ <item name="iconSize">@dimen/settingslib_expressive_space_small4</item> </style> - <style name="SettingslibMainSwitchStyle.Expressive" parent="SettingslibSwitchStyle.Expressive"> - <item name="android:layout_gravity">center</item> - <item name="trackTint">@color/settingslib_expressive_color_main_switch_track</item> + <style name="SettingsLibButtonStyle.Expressive.Tonal.Large"> + <item name="android:paddingVertical">@dimen/settingslib_expressive_space_small1</item> + <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small4</item> + <item name="android:textSize">16sp</item> + </style> + + <style name="SettingsLibButtonStyle.Expressive.Tonal.Extra" + parent="@style/SettingsLibButtonStyle.Expressive.Tonal.Large"> + <item name="android:layout_width">match_parent</item> + </style> + + <style name="SettingsLibButtonStyle.Expressive.Outline" + parent="@style/Widget.Material3.Button.OutlinedButton.Icon"> + <item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:gravity">center</item> + <item name="android:minWidth">@dimen/settingslib_expressive_space_medium4</item> + <item name="android:minHeight">@dimen/settingslib_expressive_space_medium4</item> + <item name="android:paddingVertical">@dimen/settingslib_expressive_space_extrasmall5</item> + <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small1</item> + <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item> + <item name="android:textColor">@color/settingslib_materialColorPrimary</item> + <item name="android:textSize">14sp</item> + <item name="iconTint">@color/settingslib_materialColorPrimary</item> + <item name="iconGravity">textStart</item> + <item name="iconSize">@dimen/settingslib_expressive_space_small4</item> + <item name="iconPadding">@dimen/settingslib_expressive_space_extrasmall4</item> + <item name="strokeColor">@color/settingslib_materialColorOutlineVariant</item> + + </style> + + <style name="SettingsLibButtonStyle.Expressive.Outline.Large"> + <item name="android:paddingVertical">@dimen/settingslib_expressive_space_small1</item> + <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small4</item> + <item name="android:textSize">16sp</item> + </style> + + <style name="SettingsLibButtonStyle.Expressive.Outline.Extra" + parent="@style/SettingsLibButtonStyle.Expressive.Outline.Large"> + <item name="android:layout_width">match_parent</item> + </style> + + <style name="SettingslibTextButtonStyle.Expressive" + parent="@style/Widget.Material3.Button.TextButton.Icon"> + <item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item> + <item name="android:textSize">16sp</item> + <item name="android:textColor">@color/settingslib_materialColorOnSurface</item> + <item name="iconTint">@null</item> + <item name="iconPadding">@dimen/settingslib_expressive_space_extrasmall4</item> + <item name="rippleColor">?android:attr/colorControlHighlight</item> + </style> + + <style name="EntityHeader"> + <item name="android:paddingTop">@dimen/settingslib_expressive_space_small4</item> + <item name="android:paddingBottom">@dimen/settingslib_expressive_space_small1</item> + <item name="android:paddingEnd">@dimen/settingslib_expressive_space_small1</item> + </style> + + <style name="SettingsLibEntityHeader" parent="EntityHeader"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:paddingStart">?android:attr/listPreferredItemPaddingStart</item> + <item name="android:paddingEnd">?android:attr/listPreferredItemPaddingEnd</item> + </style> + + <style name="SettingsLibEntityHeaderContent"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_centerHorizontal">true</item> + <item name="android:orientation">vertical</item> + <item name="android:gravity">center_horizontal</item> + </style> + + <style name="SettingsLibEntityHeaderIcon"> + <item name="android:layout_width">@dimen/settingslib_expressive_space_large3</item> + <item name="android:layout_height">@dimen/settingslib_expressive_space_large3</item> + <item name="android:scaleType">fitCenter</item> + <item name="android:antialias">true</item> + </style> + + <style name="SettingsLibEntityHeaderTitle"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_marginTop">@dimen/settingslib_expressive_space_small1</item> + <item name="android:singleLine">false</item> + <item name="android:gravity">center</item> + <item name="android:ellipsize">marquee</item> + <item name="android:textDirection">locale</item> + <item name="android:textAppearance">@style/TextAppearance.EntityHeaderTitle</item> </style> </resources>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt new file mode 100644 index 000000000000..127f21a540ab --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt @@ -0,0 +1,150 @@ +/* + * 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.widget + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import com.android.settingslib.widget.theme.R +import com.google.android.material.button.MaterialButton + +class CollapsableTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private var isCollapsable: Boolean = false + private var isCollapsed: Boolean = false + private var minLines: Int = DEFAULT_MIN_LINES + + private val titleTextView: TextView + private val collapseButton: MaterialButton + private val collapseButtonResources: CollapseButtonResources + + init { + LayoutInflater.from(context) + .inflate(R.layout.settingslib_expressive_collapsable_textview, this) + titleTextView = findViewById(android.R.id.title) + collapseButton = findViewById(R.id.collapse_button) + + collapseButtonResources = CollapseButtonResources( + context.getDrawable(R.drawable.settingslib_expressive_icon_collapse)!!, + context.getDrawable(R.drawable.settingslib_expressive_icon_expand)!!, + context.getString(R.string.settingslib_expressive_text_collapse), + context.getString(R.string.settingslib_expressive_text_expand) + ) + + collapseButton.setOnClickListener { + isCollapsed = !isCollapsed + updateView() + } + + initAttributes(context, attrs, defStyleAttr) + } + + private fun initAttributes(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { + context.obtainStyledAttributes( + attrs, Attrs, defStyleAttr, 0 + ).apply { + val gravity = getInt(GravityAttr, Gravity.START) + when (gravity) { + Gravity.CENTER_VERTICAL, Gravity.CENTER, Gravity.CENTER_HORIZONTAL -> { + centerHorizontally(titleTextView) + centerHorizontally(collapseButton) + } + } + recycle() + } + } + + private fun centerHorizontally(view: View) { + (view.layoutParams as LayoutParams).apply { + startToStart = LayoutParams.PARENT_ID + endToEnd = LayoutParams.PARENT_ID + horizontalBias = 0.5f + } + } + + /** + * Sets the text content of the CollapsableTextView. + * @param text The text to display. + */ + fun setText(text: String) { + titleTextView.text = text + } + + /** + * Sets whether the text view is collapsable. + * @param collapsable True if the text view should be collapsable, false otherwise. + */ + fun setCollapsable(collapsable: Boolean) { + isCollapsable = collapsable + updateView() + } + + /** + * Sets the minimum number of lines to display when collapsed. + * @param lines The minimum number of lines. + */ + fun setMinLines(line: Int) { + minLines = line.coerceIn(1, DEFAULT_MAX_LINES) + updateView() + } + + private fun updateView() { + when { + isCollapsed -> { + collapseButton.apply { + text = collapseButtonResources.expandText + icon = collapseButtonResources.expandIcon + } + titleTextView.maxLines = minLines + } + + else -> { + collapseButton.apply { + text = collapseButtonResources.collapseText + icon = collapseButtonResources.collapseIcon + } + titleTextView.maxLines = DEFAULT_MAX_LINES + } + } + collapseButton.visibility = if (isCollapsable) VISIBLE else GONE + } + + private data class CollapseButtonResources( + val collapseIcon: Drawable, + val expandIcon: Drawable, + val collapseText: String, + val expandText: String + ) + + companion object { + private const val DEFAULT_MAX_LINES = 10 + private const val DEFAULT_MIN_LINES = 2 + + private val Attrs = R.styleable.CollapsableTextView + private val GravityAttr = R.styleable.CollapsableTextView_android_gravity + } +} + diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt index 7139f5b468ca..2a251a59e1d8 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt @@ -30,9 +30,6 @@ import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider import com.android.settingslib.spa.gallery.editor.SettingsDropdownBoxPageProvider import com.android.settingslib.spa.gallery.editor.SettingsDropdownCheckBoxProvider import com.android.settingslib.spa.gallery.home.HomePageProvider -import com.android.settingslib.spa.gallery.itemList.ItemListPageProvider -import com.android.settingslib.spa.gallery.itemList.ItemOperatePageProvider -import com.android.settingslib.spa.gallery.itemList.OperateListPageProvider import com.android.settingslib.spa.gallery.editor.SettingsOutlinedTextFieldPageProvider import com.android.settingslib.spa.gallery.editor.SettingsTextFieldPasswordPageProvider import com.android.settingslib.spa.gallery.page.ArgumentPageProvider @@ -66,10 +63,6 @@ import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider */ enum class SettingsPageProviderEnum(val displayName: String) { HOME("home"), - PREFERENCE("preference"), - ARGUMENT("argument"), - ITEM_LIST("itemList"), - ITEM_OP_PAGE("itemOp"), // Add your SPPs } @@ -101,9 +94,6 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { ChartPageProvider, DialogMainPageProvider, NavDialogProvider, - ItemListPageProvider, - ItemOperatePageProvider, - OperateListPageProvider, EditorMainPageProvider, SettingsOutlinedTextFieldPageProvider, SettingsDropdownBoxPageProvider, diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/banner/BannerPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/banner/BannerPageProvider.kt index 6edd9173d7e5..c16d8bfde23e 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/banner/BannerPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/banner/BannerPageProvider.kt @@ -39,9 +39,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsTheme @@ -161,14 +159,12 @@ object BannerPageProvider : SettingsPageProvider { } } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = createSettingsPage()) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } private const val TITLE = "Sample Banner" diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonPageProvider.kt index b001caddd000..773d3d121a1d 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonPageProvider.kt @@ -23,9 +23,7 @@ import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.WarningAmber import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.button.ActionButton @@ -55,14 +53,12 @@ object ActionButtonPageProvider : SettingsPageProvider { } } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = createSettingsPage()) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/chart/ChartPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/chart/ChartPageProvider.kt index 7a6ae2cee6ad..6ceb395272c4 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/chart/ChartPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/chart/ChartPageProvider.kt @@ -39,6 +39,7 @@ import java.text.NumberFormat private enum class WeekDay(val num: Int) { Sun(0), Mon(1), Tue(2), Wed(3), Thu(4), Fri(5), Sat(6), } + private const val TITLE = "Sample Chart" object ChartPageProvider : SettingsPageProvider { @@ -103,14 +104,12 @@ object ChartPageProvider : SettingsPageProvider { return entryList } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt index 4e3fcee5383e..c9c81aac01c3 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt @@ -18,6 +18,7 @@ package com.android.settingslib.spa.gallery.dialog import android.os.Bundle import androidx.compose.material3.Text +import androidx.compose.runtime.Composable import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider @@ -55,13 +56,13 @@ object DialogMainPageProvider : SettingsPageProvider { }.build(), ) - fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } override fun getTitle(arguments: Bundle?) = TITLE } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt index c511542f265a..f2b4091c7d85 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt @@ -17,8 +17,8 @@ package com.android.settingslib.spa.gallery.editor import android.os.Bundle +import androidx.compose.runtime.Composable import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator @@ -44,14 +44,12 @@ object EditorMainPageProvider : SettingsPageProvider { ) } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt index b1558cce718a..4d77ea173a85 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt @@ -20,20 +20,16 @@ import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.gallery.R import com.android.settingslib.spa.gallery.SettingsPageProviderEnum -import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider import com.android.settingslib.spa.gallery.banner.BannerPageProvider +import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider import com.android.settingslib.spa.gallery.chart.ChartPageProvider import com.android.settingslib.spa.gallery.dialog.DialogMainPageProvider import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider -import com.android.settingslib.spa.gallery.itemList.OperateListPageProvider -import com.android.settingslib.spa.gallery.page.ArgumentPageModel import com.android.settingslib.spa.gallery.page.ArgumentPageProvider import com.android.settingslib.spa.gallery.page.FooterPageProvider import com.android.settingslib.spa.gallery.page.IllustrationPageProvider @@ -48,35 +44,11 @@ import com.android.settingslib.spa.gallery.ui.CategoryPageProvider import com.android.settingslib.spa.gallery.ui.CopyablePageProvider import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider import com.android.settingslib.spa.widget.scaffold.HomeScaffold +import com.android.settingslib.spa.widget.ui.Category object HomePageProvider : SettingsPageProvider { override val name = SettingsPageProviderEnum.HOME.name override val displayName = SettingsPageProviderEnum.HOME.displayName - private val owner = createSettingsPage() - - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - return listOf( - PreferenceMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - OperateListPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - ArgumentPageProvider.buildInjectEntry("foo")!!.setLink(fromPage = owner).build(), - SearchScaffoldPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - SuwScaffoldPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - SliderPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - SpinnerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - PagerMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - FooterPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - IllustrationPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - CategoryPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - ActionButtonPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - ProgressBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - LoadingBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - ChartPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - DialogMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - EditorMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - BannerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - CopyablePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - ) - } override fun getTitle(arguments: Bundle?): String { return SpaEnvironmentFactory.instance.appContext.getString(R.string.app_name) @@ -85,14 +57,30 @@ object HomePageProvider : SettingsPageProvider { @Composable override fun Page(arguments: Bundle?) { val title = remember { getTitle(arguments) } - val entries = remember { buildEntry(arguments) } HomeScaffold(title) { - for (entry in entries) { - if (entry.owner.isCreateBy(SettingsPageProviderEnum.ARGUMENT.name)) { - entry.UiLayout(ArgumentPageModel.buildArgument(intParam = 0)) - } else { - entry.UiLayout() - } + Category { + PreferenceMainPageProvider.Entry() + } + Category { + SearchScaffoldPageProvider.Entry() + SuwScaffoldPageProvider.Entry() + ArgumentPageProvider.EntryItem(stringParam = "foo", intParam = 0) + } + Category { + SliderPageProvider.Entry() + SpinnerPageProvider.Entry() + PagerMainPageProvider.Entry() + FooterPageProvider.Entry() + IllustrationPageProvider.Entry() + CategoryPageProvider.Entry() + ActionButtonPageProvider.Entry() + ProgressBarPageProvider.Entry() + LoadingBarPageProvider.Entry() + ChartPageProvider.Entry() + DialogMainPageProvider.Entry() + EditorMainPageProvider.Entry() + BannerPageProvider.Entry() + CopyablePageProvider.Entry() } } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemListPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemListPage.kt deleted file mode 100644 index 5f251b1b14dd..000000000000 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemListPage.kt +++ /dev/null @@ -1,99 +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.settingslib.spa.gallery.itemList - -import android.os.Bundle -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.core.os.bundleOf -import androidx.navigation.NavType -import androidx.navigation.navArgument -import com.android.settingslib.spa.framework.common.EntrySearchData -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder -import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage -import com.android.settingslib.spa.framework.compose.navigator -import com.android.settingslib.spa.framework.util.getStringArg -import com.android.settingslib.spa.framework.util.navLink -import com.android.settingslib.spa.gallery.SettingsPageProviderEnum -import com.android.settingslib.spa.widget.preference.Preference -import com.android.settingslib.spa.widget.preference.PreferenceModel -import com.android.settingslib.spa.widget.scaffold.RegularScaffold - -private const val OPERATOR_PARAM_NAME = "opParam" - -object ItemListPageProvider : SettingsPageProvider { - override val name = SettingsPageProviderEnum.ITEM_LIST.name - override val displayName = SettingsPageProviderEnum.ITEM_LIST.displayName - override val parameter = listOf( - navArgument(OPERATOR_PARAM_NAME) { type = NavType.StringType }, - ) - - override fun getTitle(arguments: Bundle?): String { - val operation = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments) ?: "NULL" - return "Operation: $operation" - } - - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - if (!ItemOperatePageProvider.isValidArgs(arguments)) return emptyList() - val operation = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments)!! - val owner = createSettingsPage(arguments) - return listOf( - ItemOperatePageProvider.buildInjectEntry(operation)!!.setLink(fromPage = owner).build(), - ) - } - - fun buildInjectEntry(opParam: String): SettingsEntryBuilder? { - val arguments = bundleOf(OPERATOR_PARAM_NAME to opParam) - if (!ItemOperatePageProvider.isValidArgs(arguments)) return null - - return SettingsEntryBuilder.createInject( - owner = createSettingsPage(arguments), - label = "ItemList_$opParam", - ).setUiLayoutFn { - Preference( - object : PreferenceModel { - override val title = opParam - override val onClick = navigator( - SettingsPageProviderEnum.ITEM_LIST.name + parameter.navLink(it) - ) - } - ) - }.setSearchDataFn { - EntrySearchData(title = "Operation: $opParam") - } - } - - @Composable - override fun Page(arguments: Bundle?) { - val title = remember { getTitle(arguments) } - val entries = remember { buildEntry(arguments) } - val itemList = remember { - // Add logic to get item List during runtime. - listOf("itemFoo", "itemBar", "itemToy") - } - RegularScaffold(title) { - for (item in itemList) { - val rtArgs = ItemOperatePageProvider.genRuntimeArguments(item) - for (entry in entries) { - entry.UiLayout(rtArgs) - } - } - } - } -} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemOperatePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemOperatePage.kt deleted file mode 100644 index 6caec07371f5..000000000000 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/ItemOperatePage.kt +++ /dev/null @@ -1,127 +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.settingslib.spa.gallery.itemList - -import android.os.Bundle -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.core.os.bundleOf -import androidx.navigation.NavType -import androidx.navigation.navArgument -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder -import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage -import com.android.settingslib.spa.framework.compose.navigator -import com.android.settingslib.spa.framework.util.getStringArg -import com.android.settingslib.spa.framework.util.navLink -import com.android.settingslib.spa.gallery.SettingsPageProviderEnum -import com.android.settingslib.spa.widget.preference.Preference -import com.android.settingslib.spa.widget.preference.PreferenceModel -import com.android.settingslib.spa.widget.preference.SwitchPreference -import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel - -private const val OPERATOR_PARAM_NAME = "opParam" -private const val ITEM_NAME_PARAM_NAME = "rt_nameParam" -private val ALLOWED_OPERATOR_LIST = listOf("opDnD", "opPiP", "opInstall", "opConnect") - -object ItemOperatePageProvider : SettingsPageProvider { - override val name = SettingsPageProviderEnum.ITEM_OP_PAGE.name - override val displayName = SettingsPageProviderEnum.ITEM_OP_PAGE.displayName - override val parameter = listOf( - navArgument(OPERATOR_PARAM_NAME) { type = NavType.StringType }, - navArgument(ITEM_NAME_PARAM_NAME) { type = NavType.StringType }, - ) - - override fun getTitle(arguments: Bundle?): String { - // Operation name is not a runtime parameter, which should always available - val operation = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments) ?: "opInValid" - // Item name is a runtime parameter, which could be missing - val itemName = parameter.getStringArg(ITEM_NAME_PARAM_NAME, arguments) ?: "[unset]" - return "$operation on $itemName" - } - - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - if (!isValidArgs(arguments)) return emptyList() - - val owner = createSettingsPage(arguments) - val entryList = mutableListOf<SettingsEntry>() - entryList.add( - SettingsEntryBuilder.create("ItemName", owner) - .setUiLayoutFn { - // Item name is a runtime parameter, which needs to be read inside UiLayoutFn - val itemName = parameter.getStringArg(ITEM_NAME_PARAM_NAME, it) ?: "NULL" - Preference( - object : PreferenceModel { - override val title = "Item $itemName" - } - ) - }.build() - ) - - // Operation name is not a runtime parameter, which can be read outside. - val opName = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments)!! - entryList.add( - SettingsEntryBuilder.create("ItemOp", owner) - .setUiLayoutFn { - var checked by rememberSaveable { mutableStateOf(false) } - SwitchPreference(remember { - object : SwitchPreferenceModel { - override val title = "Item operation: $opName" - override val checked = { checked } - override val onCheckedChange = - { newChecked: Boolean -> checked = newChecked } - } - }) - }.build(), - ) - return entryList - } - - fun buildInjectEntry(opParam: String): SettingsEntryBuilder? { - val arguments = bundleOf(OPERATOR_PARAM_NAME to opParam) - if (!isValidArgs(arguments)) return null - - return SettingsEntryBuilder.createInject( - owner = createSettingsPage(arguments), - label = "ItemOp_$opParam", - ).setUiLayoutFn { - // Item name is a runtime parameter, which needs to be read inside UiLayoutFn - val itemName = parameter.getStringArg(ITEM_NAME_PARAM_NAME, it) ?: "NULL" - Preference( - object : PreferenceModel { - override val title = "item: $itemName" - override val onClick = navigator( - SettingsPageProviderEnum.ITEM_OP_PAGE.name + parameter.navLink(it) - ) - } - ) - } - } - - fun isValidArgs(arguments: Bundle?): Boolean { - val opParam = parameter.getStringArg(OPERATOR_PARAM_NAME, arguments) - return (opParam != null && ALLOWED_OPERATOR_LIST.contains(opParam)) - } - - fun genRuntimeArguments(itemName: String): Bundle { - return bundleOf(ITEM_NAME_PARAM_NAME to itemName) - } -} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/OperateListPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/OperateListPage.kt deleted file mode 100644 index e0baf86119a3..000000000000 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/itemList/OperateListPage.kt +++ /dev/null @@ -1,56 +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.settingslib.spa.gallery.itemList - -import android.os.Bundle -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder -import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage -import com.android.settingslib.spa.framework.compose.navigator -import com.android.settingslib.spa.widget.preference.Preference -import com.android.settingslib.spa.widget.preference.PreferenceModel - -private const val TITLE = "Operate List Main" - -object OperateListPageProvider : SettingsPageProvider { - override val name = "OpList" - private val owner = createSettingsPage() - - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - return listOf( - ItemListPageProvider.buildInjectEntry("opPiP")!!.setLink(fromPage = owner).build(), - ItemListPageProvider.buildInjectEntry("opInstall")!!.setLink(fromPage = owner).build(), - ItemListPageProvider.buildInjectEntry("opDnD")!!.setLink(fromPage = owner).build(), - ItemListPageProvider.buildInjectEntry("opConnect")!!.setLink(fromPage = owner).build(), - ) - } - - override fun getTitle(arguments: Bundle?): String { - return TITLE - } - - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } - } -} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt index f01ff3849701..9ad1c22a4912 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt @@ -18,112 +18,69 @@ package com.android.settingslib.spa.gallery.page import android.os.Bundle import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder -import com.android.settingslib.spa.framework.common.SettingsPage +import androidx.navigation.NavType +import androidx.navigation.navArgument import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory -import com.android.settingslib.spa.framework.common.createSettingsPage +import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme -import com.android.settingslib.spa.gallery.SettingsPageProviderEnum import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.scaffold.RegularScaffold -object ArgumentPageProvider : SettingsPageProvider { - // Defines all entry name in this page. - // Note that entry name would be used in log. DO NOT change it once it is set. - // One can still change the display name for better readability if necessary. - private enum class EntryEnum(val displayName: String) { - STRING_PARAM("string_param"), - INT_PARAM("int_param"), - } - - private fun createEntry(owner: SettingsPage, entry: EntryEnum): SettingsEntryBuilder { - return SettingsEntryBuilder.create(owner, entry.name, entry.displayName) - } - - override val name = SettingsPageProviderEnum.ARGUMENT.name - override val displayName = SettingsPageProviderEnum.ARGUMENT.displayName - override val parameter = ArgumentPageModel.parameter +private const val TITLE = "Sample page with arguments" +private const val STRING_PARAM_NAME = "stringParam" +private const val INT_PARAM_NAME = "intParam" - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - if (!ArgumentPageModel.isValidArgument(arguments)) return emptyList() +object ArgumentPageProvider : SettingsPageProvider { + override val name = "Argument" - val owner = createSettingsPage(arguments) - val entryList = mutableListOf<SettingsEntry>() - entryList.add( - createEntry(owner, EntryEnum.STRING_PARAM) - // Set attributes - .setIsSearchDataDynamic(true) - .setSearchDataFn { ArgumentPageModel.genStringParamSearchData() } - .setUiLayoutFn { - // Set ui rendering - Preference(ArgumentPageModel.create(it).genStringParamPreferenceModel()) - }.build() - ) + override val parameter = listOf( + navArgument(STRING_PARAM_NAME) { type = NavType.StringType }, + navArgument(INT_PARAM_NAME) { type = NavType.IntType }, + ) - entryList.add( - createEntry(owner, EntryEnum.INT_PARAM) - // Set attributes - .setIsSearchDataDynamic(true) - .setSearchDataFn { ArgumentPageModel.genIntParamSearchData() } - .setUiLayoutFn { - // Set ui rendering - Preference(ArgumentPageModel.create(it).genIntParamPreferenceModel()) - }.build() + @Composable + override fun Page(arguments: Bundle?) { + ArgumentPage( + stringParam = arguments!!.getString(STRING_PARAM_NAME, "default"), + intParam = arguments.getInt(INT_PARAM_NAME), ) + } - entryList.add(buildInjectEntry("foo")!!.setLink(fromPage = owner).build()) - entryList.add(buildInjectEntry("bar")!!.setLink(fromPage = owner).build()) - - return entryList + @Composable + fun EntryItem(stringParam: String, intParam: Int) { + Preference(object : PreferenceModel { + override val title = TITLE + override val summary = { "$STRING_PARAM_NAME=$stringParam, $INT_PARAM_NAME=$intParam" } + override val onClick = navigator("$name/$stringParam/$intParam") + }) } +} - fun buildInjectEntry(stringParam: String): SettingsEntryBuilder? { - val arguments = ArgumentPageModel.buildArgument(stringParam) - if (!ArgumentPageModel.isValidArgument(arguments)) return null +@Composable +fun ArgumentPage(stringParam: String, intParam: Int) { + RegularScaffold(title = TITLE) { + Preference(object : PreferenceModel { + override val title = "String param value" + override val summary = { stringParam } + }) - return SettingsEntryBuilder.createInject( - owner = createSettingsPage(arguments), - label = "${name}_$stringParam", - ) - .setSearchDataFn { ArgumentPageModel.genInjectSearchData() } - .setUiLayoutFn { - // Set ui rendering - Preference(ArgumentPageModel.create(it).genInjectPreferenceModel()) - } - } + Preference(object : PreferenceModel { + override val title = "Int param value" + override val summary = { intParam.toString() } + }) - override fun getTitle(arguments: Bundle?): String { - return ArgumentPageModel.genPageTitle() - } + ArgumentPageProvider.EntryItem(stringParam = "foo", intParam = intParam + 1) - @Composable - override fun Page(arguments: Bundle?) { - val title = remember { getTitle(arguments) } - val entries = remember { buildEntry(arguments) } - val rtArgNext = remember { ArgumentPageModel.buildNextArgument(arguments) } - RegularScaffold(title) { - for (entry in entries) { - if (entry.toPage != null) { - entry.UiLayout(rtArgNext) - } else { - entry.UiLayout() - } - } - } + ArgumentPageProvider.EntryItem(stringParam = "bar", intParam = intParam + 1) } } @Preview(showBackground = true) @Composable private fun ArgumentPagePreview() { - SpaEnvironmentFactory.resetForPreview() SettingsTheme { - ArgumentPageProvider.Page( - ArgumentPageModel.buildArgument(stringParam = "foo", intParam = 0) - ) + ArgumentPage(stringParam = "foo", intParam = 0) } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt deleted file mode 100644 index d763f77d2644..000000000000 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt +++ /dev/null @@ -1,138 +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.settingslib.spa.gallery.page - -import android.os.Bundle -import androidx.compose.runtime.Composable -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavType -import androidx.navigation.navArgument -import com.android.settingslib.spa.framework.common.EntrySearchData -import com.android.settingslib.spa.framework.common.PageModel -import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory -import com.android.settingslib.spa.framework.compose.navigator -import com.android.settingslib.spa.framework.util.getIntArg -import com.android.settingslib.spa.framework.util.getStringArg -import com.android.settingslib.spa.framework.util.navLink -import com.android.settingslib.spa.gallery.SettingsPageProviderEnum -import com.android.settingslib.spa.widget.preference.PreferenceModel - -private const val TAG = "ArgumentPageModel" - -// Defines all the resources for this page. -// In real Settings App, resources data is defined in xml, rather than SPP. -private const val PAGE_TITLE = "Sample page with arguments" -private const val STRING_PARAM_TITLE = "String param value" -private const val INT_PARAM_TITLE = "Int param value" -private const val STRING_PARAM_NAME = "stringParam" -private const val INT_PARAM_NAME = "rt_intParam" -private val ARGUMENT_PAGE_KEYWORDS = listOf("argument keyword1", "argument keyword2") - -class ArgumentPageModel : PageModel() { - - companion object { - val parameter = listOf( - navArgument(STRING_PARAM_NAME) { type = NavType.StringType }, - navArgument(INT_PARAM_NAME) { type = NavType.IntType }, - ) - - fun buildArgument(stringParam: String? = null, intParam: Int? = null): Bundle { - val args = Bundle() - if (stringParam != null) args.putString(STRING_PARAM_NAME, stringParam) - if (intParam != null) args.putInt(INT_PARAM_NAME, intParam) - return args - } - - fun buildNextArgument(arguments: Bundle? = null): Bundle { - val intParam = parameter.getIntArg(INT_PARAM_NAME, arguments) - val nextIntParam = if (intParam != null) intParam + 1 else null - return buildArgument(intParam = nextIntParam) - } - - fun isValidArgument(arguments: Bundle?): Boolean { - val stringParam = parameter.getStringArg(STRING_PARAM_NAME, arguments) - return (stringParam != null && listOf("foo", "bar").contains(stringParam)) - } - - fun genStringParamSearchData(): EntrySearchData { - return EntrySearchData(title = STRING_PARAM_TITLE) - } - - fun genIntParamSearchData(): EntrySearchData { - return EntrySearchData(title = INT_PARAM_TITLE) - } - - fun genInjectSearchData(): EntrySearchData { - return EntrySearchData(title = PAGE_TITLE, keyword = ARGUMENT_PAGE_KEYWORDS) - } - - fun genPageTitle(): String { - return PAGE_TITLE - } - - @Composable - fun create(arguments: Bundle?): ArgumentPageModel { - val pageModel: ArgumentPageModel = viewModel(key = arguments.toString()) - pageModel.initOnce(arguments) - return pageModel - } - } - - private var arguments: Bundle? = null - private var stringParam: String? = null - private var intParam: Int? = null - - override fun initialize(arguments: Bundle?) { - SpaEnvironmentFactory.instance.logger.message( - TAG, "Initialize with args " + arguments.toString() - ) - this.arguments = arguments - stringParam = parameter.getStringArg(STRING_PARAM_NAME, arguments) - intParam = parameter.getIntArg(INT_PARAM_NAME, arguments) - } - - @Composable - fun genStringParamPreferenceModel(): PreferenceModel { - return object : PreferenceModel { - override val title = STRING_PARAM_TITLE - override val summary = { stringParam!! } - } - } - - @Composable - fun genIntParamPreferenceModel(): PreferenceModel { - return object : PreferenceModel { - override val title = INT_PARAM_TITLE - override val summary = { intParam!!.toString() } - } - } - - @Composable - fun genInjectPreferenceModel(): PreferenceModel { - val summaryArray = listOf( - "$STRING_PARAM_NAME=" + stringParam!!, - "$INT_PARAM_NAME=" + intParam!! - ) - return object : PreferenceModel { - override val title = PAGE_TITLE - override val summary = { summaryArray.joinToString(", ") } - override val onClick = navigator( - SettingsPageProviderEnum.ARGUMENT.name + parameter.navLink(arguments) - ) - } - } -} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt index 345b47aff791..d31dab31669c 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt @@ -43,7 +43,7 @@ object FooterPageProvider : SettingsPageProvider { override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { val entryList = mutableListOf<SettingsEntry>() entryList.add( - SettingsEntryBuilder.create( "Some Preference", owner) + SettingsEntryBuilder.create("Some Preference", owner) .setSearchDataFn { EntrySearchData(title = "Some Preference") } .setUiLayoutFn { Preference(remember { @@ -58,14 +58,12 @@ object FooterPageProvider : SettingsPageProvider { return entryList } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPageProvider.kt index ee22b96c937f..021e84f81808 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPageProvider.kt @@ -41,7 +41,7 @@ object IllustrationPageProvider : SettingsPageProvider { override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { val entryList = mutableListOf<SettingsEntry>() entryList.add( - SettingsEntryBuilder.create( "Lottie Illustration", owner) + SettingsEntryBuilder.create("Lottie Illustration", owner) .setUiLayoutFn { Preference(object : PreferenceModel { override val title = "Lottie Illustration" @@ -54,7 +54,7 @@ object IllustrationPageProvider : SettingsPageProvider { }.build() ) entryList.add( - SettingsEntryBuilder.create( "Image Illustration", owner) + SettingsEntryBuilder.create("Image Illustration", owner) .setUiLayoutFn { Preference(object : PreferenceModel { override val title = "Image Illustration" @@ -70,14 +70,12 @@ object IllustrationPageProvider : SettingsPageProvider { return entryList } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPageProvider.kt index f1cbc3729a78..4d474816082f 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPageProvider.kt @@ -30,9 +30,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsTheme @@ -47,14 +45,12 @@ private const val TITLE = "Sample LoadingBar" object LoadingBarPageProvider : SettingsPageProvider { override val name = "LoadingBar" - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = createSettingsPage()) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPageProvider.kt index 9026a240a04a..47c49fea3e5a 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPageProvider.kt @@ -27,9 +27,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.preference.Preference @@ -46,14 +44,12 @@ private const val TITLE = "Sample ProgressBar" object ProgressBarPageProvider : SettingsPageProvider { override val name = "ProgressBar" - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = createSettingsPage()) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPageProvider.kt index 89b10ee2cb84..572746b14535 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPageProvider.kt @@ -117,15 +117,14 @@ object SliderPageProvider : SettingsPageProvider { return entryList } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner).setUiLayoutFn { - Preference( - object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - } - ) - } + @Composable + fun Entry() { + Preference( + object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + } + ) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/IntroPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/IntroPreferencePageProvider.kt index 603fceed9900..b83a02637371 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/IntroPreferencePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/IntroPreferencePageProvider.kt @@ -50,15 +50,12 @@ object IntroPreferencePageProvider : SettingsPageProvider { return entryList } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner).setUiLayoutFn { - Preference( - object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - } - ) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ListPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ListPreferencePageProvider.kt index d7de9b4f2045..3bb526ef7996 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ListPreferencePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ListPreferencePageProvider.kt @@ -23,9 +23,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.preference.ListPreference @@ -33,6 +31,8 @@ import com.android.settingslib.spa.widget.preference.ListPreferenceModel import com.android.settingslib.spa.widget.preference.ListPreferenceOption import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.Category import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flow @@ -41,30 +41,24 @@ private const val TITLE = "Sample ListPreference" object ListPreferencePageProvider : SettingsPageProvider { override val name = "ListPreference" - private val owner = createSettingsPage() - override fun buildEntry(arguments: Bundle?) = listOf( - SettingsEntryBuilder.create("ListPreference", owner) - .setUiLayoutFn { + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(TITLE) { + Category { SampleListPreference() - }.build(), - SettingsEntryBuilder.create("ListPreference not changeable", owner) - .setUiLayoutFn { SampleNotChangeableListPreference() - }.build(), - ) - - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) } + } } - override fun getTitle(arguments: Bundle?) = TITLE + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } } @Composable diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePageProvider.kt index 0d85c0e3262f..f548160ef336 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePageProvider.kt @@ -59,14 +59,12 @@ object MainSwitchPreferencePageProvider : SettingsPageProvider { return entryList } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt index 1626b025e2f7..831b43942e98 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt @@ -17,45 +17,44 @@ package com.android.settingslib.spa.gallery.preference import android.os.Bundle -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder +import androidx.compose.runtime.Composable import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.Category private const val TITLE = "Category: Preference" object PreferenceMainPageProvider : SettingsPageProvider { override val name = "PreferenceMain" - private val owner = createSettingsPage() - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - return listOf( - PreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - SwitchPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - MainSwitchPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - ListPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - TwoTargetSwitchPreferencePageProvider.buildInjectEntry() - .setLink(fromPage = owner).build(), - ZeroStatePreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - IntroPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - TopIntroPreferencePageProvider.buildInjectEntry().setLink(fromPage = owner).build(), - ) - } - - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(TITLE) { + Category { + PreferencePageProvider.Entry() + ListPreferencePageProvider.Entry() + } + Category { + SwitchPreferencePageProvider.Entry() + MainSwitchPreferencePageProvider.Entry() + TwoTargetSwitchPreferencePageProvider.Entry() + } + Category { + ZeroStatePreferencePageProvider.Entry() + IntroPreferencePageProvider.Entry() + TopIntroPreferencePageProvider.Entry() } + } } - override fun getTitle(arguments: Bundle?): String { - return TITLE + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt deleted file mode 100644 index fc6f10f79ceb..000000000000 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.gallery.preference - -import android.os.Bundle -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel -import com.android.settingslib.spa.framework.common.PageModel -import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -private const val TAG = "PreferencePageModel" - -class PreferencePageModel : PageModel() { - companion object { - // Defines all the resources for this page. - // In real Settings App, resources data is defined in xml, rather than SPP. - const val PAGE_TITLE = "Sample Preference" - const val SIMPLE_PREFERENCE_TITLE = "Preference" - const val SIMPLE_PREFERENCE_SUMMARY = "Simple summary" - const val DISABLE_PREFERENCE_TITLE = "Disabled" - const val DISABLE_PREFERENCE_SUMMARY = "Disabled summary" - const val ASYNC_PREFERENCE_TITLE = "Async Preference" - const val ASYNC_PREFERENCE_SUMMARY = "Async summary" - const val MANUAL_UPDATE_PREFERENCE_TITLE = "Manual Updater" - const val AUTO_UPDATE_PREFERENCE_TITLE = "Auto Updater" - val SIMPLE_PREFERENCE_KEYWORDS = listOf("simple keyword1", "simple keyword2") - - @Composable - fun create(): PreferencePageModel { - val pageModel: PreferencePageModel = viewModel() - pageModel.initOnce() - return pageModel - } - } - - private val spaLogger = SpaEnvironmentFactory.instance.logger - - val asyncSummary = mutableStateOf("(loading)") - val asyncEnable = mutableStateOf(false) - - private val manualUpdater = mutableStateOf(0) - - private val autoUpdater = object : MutableLiveData<String>(" ") { - private var tick = 0 - private var updateJob: Job? = null - override fun onActive() { - spaLogger.message(TAG, "autoUpdater.active") - updateJob = viewModelScope.launch(Dispatchers.IO) { - while (true) { - delay(1000L) - tick++ - spaLogger.message(TAG, "autoUpdater.value $tick") - postValue(tick.toString()) - } - } - } - - override fun onInactive() { - spaLogger.message(TAG, "autoUpdater.inactive") - updateJob?.cancel() - } - } - - override fun initialize(arguments: Bundle?) { - spaLogger.message(TAG, "initialize with args " + arguments.toString()) - viewModelScope.launch(Dispatchers.IO) { - // Loading your data here. - delay(2000L) - asyncSummary.value = ASYNC_PREFERENCE_SUMMARY - asyncEnable.value = true - } - } - - fun getManualUpdaterSummary(): State<String> { - spaLogger.message(TAG, "getManualUpdaterSummary") - return derivedStateOf { manualUpdater.value.toString() } - } - - fun manualUpdaterOnClick() { - spaLogger.message(TAG, "manualUpdaterOnClick") - manualUpdater.value = manualUpdater.value + 1 - } - - fun getAutoUpdaterSummary(): LiveData<String> { - spaLogger.message(TAG, "getAutoUpdaterSummary") - return autoUpdater - } -} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt index 6d1d34628efa..f7649b91f558 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt @@ -18,187 +18,100 @@ package com.android.settingslib.spa.gallery.preference import android.os.Bundle import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Autorenew import androidx.compose.material.icons.outlined.DisabledByDefault -import androidx.compose.material.icons.outlined.TouchApp import androidx.compose.runtime.Composable -import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.EntrySearchData -import com.android.settingslib.spa.framework.common.EntryStatusData -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory -import com.android.settingslib.spa.framework.common.createSettingsPage +import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.gallery.R -import com.android.settingslib.spa.gallery.SettingsPageProviderEnum -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.ASYNC_PREFERENCE_TITLE -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.AUTO_UPDATE_PREFERENCE_TITLE -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.DISABLE_PREFERENCE_SUMMARY -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.DISABLE_PREFERENCE_TITLE -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.MANUAL_UPDATE_PREFERENCE_TITLE -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.PAGE_TITLE -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_KEYWORDS -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_SUMMARY -import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_TITLE import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel -import com.android.settingslib.spa.widget.preference.SimplePreferenceMacro +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.Category import com.android.settingslib.spa.widget.ui.SettingsIcon - -private const val TAG = "PreferencePage" +import kotlinx.coroutines.delay object PreferencePageProvider : SettingsPageProvider { - // Defines all entry name in this page. - // Note that entry name would be used in log. DO NOT change it once it is set. - // One can still change the display name for better readability if necessary. - enum class EntryEnum(val displayName: String) { - SIMPLE_PREFERENCE("preference"), - SUMMARY_PREFERENCE("preference_with_summary"), - SINGLE_LINE_SUMMARY_PREFERENCE("preference_with_single_line_summary"), - DISABLED_PREFERENCE("preference_disable"), - ASYNC_SUMMARY_PREFERENCE("preference_with_async_summary"), - MANUAL_UPDATE_PREFERENCE("preference_actionable"), - AUTO_UPDATE_PREFERENCE("preference_auto_update"), - } - - override val name = SettingsPageProviderEnum.PREFERENCE.name - override val displayName = SettingsPageProviderEnum.PREFERENCE.displayName - private val spaLogger = SpaEnvironmentFactory.instance.logger - private val owner = createSettingsPage() - - private fun createEntry(entry: EntryEnum): SettingsEntryBuilder { - return SettingsEntryBuilder.create(owner, entry.name, entry.displayName) - } - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - val entryList = mutableListOf<SettingsEntry>() - entryList.add( - createEntry(EntryEnum.SIMPLE_PREFERENCE) - .setMacro { - spaLogger.message(TAG, "create macro for ${EntryEnum.SIMPLE_PREFERENCE}") - SimplePreferenceMacro(title = SIMPLE_PREFERENCE_TITLE) - } - .setStatusDataFn { EntryStatusData(isDisabled = false) } - .build() - ) - entryList.add( - createEntry(EntryEnum.SUMMARY_PREFERENCE) - .setMacro { - spaLogger.message(TAG, "create macro for ${EntryEnum.SUMMARY_PREFERENCE}") - SimplePreferenceMacro( - title = SIMPLE_PREFERENCE_TITLE, - summary = SIMPLE_PREFERENCE_SUMMARY, - searchKeywords = SIMPLE_PREFERENCE_KEYWORDS, - ) - } - .setStatusDataFn { EntryStatusData(isDisabled = true) } - .build() - ) - entryList.add(singleLineSummaryEntry()) - entryList.add( - createEntry(EntryEnum.DISABLED_PREFERENCE) - .setHasMutableStatus(true) - .setMacro { - spaLogger.message(TAG, "create macro for ${EntryEnum.DISABLED_PREFERENCE}") - SimplePreferenceMacro( - title = DISABLE_PREFERENCE_TITLE, - summary = DISABLE_PREFERENCE_SUMMARY, - disabled = true, - icon = Icons.Outlined.DisabledByDefault, - ) - } - .setStatusDataFn { EntryStatusData(isDisabled = true) } - .build() - ) - entryList.add( - createEntry(EntryEnum.ASYNC_SUMMARY_PREFERENCE) - .setHasMutableStatus(true) - .setSearchDataFn { - EntrySearchData(title = ASYNC_PREFERENCE_TITLE) - } - .setStatusDataFn { EntryStatusData(isDisabled = false) } - .setUiLayoutFn { - val model = PreferencePageModel.create() - Preference( - object : PreferenceModel { - override val title = ASYNC_PREFERENCE_TITLE - override val summary = { model.asyncSummary.value } - override val enabled = { model.asyncEnable.value } - } - ) - }.build() - ) - entryList.add( - createEntry(EntryEnum.MANUAL_UPDATE_PREFERENCE) - .setUiLayoutFn { - val model = PreferencePageModel.create() - val manualUpdaterSummary = remember { model.getManualUpdaterSummary() } - Preference( - object : PreferenceModel { - override val title = MANUAL_UPDATE_PREFERENCE_TITLE - override val summary = { manualUpdaterSummary.value } - override val onClick = { model.manualUpdaterOnClick() } - override val icon = @Composable { - SettingsIcon(imageVector = Icons.Outlined.TouchApp) - } - } - ) - }.build() - ) - entryList.add( - createEntry(EntryEnum.AUTO_UPDATE_PREFERENCE) - .setUiLayoutFn { - val model = PreferencePageModel.create() - val autoUpdaterSummary = remember { - model.getAutoUpdaterSummary() - }.observeAsState(" ") - Preference( - object : PreferenceModel { - override val title = AUTO_UPDATE_PREFERENCE_TITLE - override val summary = { autoUpdaterSummary.value } - override val icon = @Composable { - SettingsIcon(imageVector = Icons.Outlined.Autorenew) - } - } - ) - }.build() - ) + override val name = "Preference" + private const val PAGE_TITLE = "Sample Preference" - return entryList - } + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(PAGE_TITLE) { + Category { + Preference(object : PreferenceModel { + override val title = "Preference" + }) + Preference(object : PreferenceModel { + override val title = "Preference" + override val summary = { "Simple summary" } + }) + val summary = stringResource(R.string.single_line_summary_preference_summary) + Preference( + model = object : PreferenceModel { + override val title = + stringResource(R.string.single_line_summary_preference_title) + override val summary = { summary } + }, + singleLineSummary = true, + ) + } + Category { + Preference(object : PreferenceModel { + override val title = "Disabled" + override val summary = { "Disabled summary" } + override val enabled = { false } + override val icon = @Composable { + SettingsIcon(imageVector = Icons.Outlined.DisabledByDefault) + } + }) + } + Category { + Preference(object : PreferenceModel { + override val title = "Preference" + val asyncSummary by produceState(initialValue = " ") { + delay(1000L) + value = "Async summary" + } + override val summary = { asyncSummary } + }) - private fun singleLineSummaryEntry() = createEntry(EntryEnum.SINGLE_LINE_SUMMARY_PREFERENCE) - .setUiLayoutFn { - val summary = stringResource(R.string.single_line_summary_preference_summary) - Preference( - model = object : PreferenceModel { - override val title: String = - stringResource(R.string.single_line_summary_preference_title) - override val summary = { summary } - }, - singleLineSummary = true, - ) - } - .build() + var count by remember { mutableIntStateOf(0) } + Preference(object : PreferenceModel { + override val title = "Click me" + override val summary = { count.toString() } + override val onClick: (() -> Unit) = { count++ } + }) - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = owner) - .setMacro { - spaLogger.message(TAG, "create macro for INJECT entry") - SimplePreferenceMacro( - title = PAGE_TITLE, - clickRoute = SettingsPageProviderEnum.PREFERENCE.name - ) + var ticks by remember { mutableIntStateOf(0) } + LaunchedEffect(ticks) { + delay(1000L) + ticks++ + } + Preference(object : PreferenceModel { + override val title = "Ticker" + override val summary = { ticks.toString() } + }) } + } } - override fun getTitle(arguments: Bundle?): String { - return PAGE_TITLE + @Composable + fun Entry() { + Preference(model = object : PreferenceModel { + override val title = PAGE_TITLE + override val onClick = navigator(name) + }) } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePageProvider.kt index f2225fa86136..9508d504a5d8 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePageProvider.kt @@ -27,16 +27,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SwitchPreference import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.Category import com.android.settingslib.spa.widget.ui.SettingsIcon import kotlinx.coroutines.delay @@ -44,56 +43,26 @@ private const val TITLE = "Sample SwitchPreference" object SwitchPreferencePageProvider : SettingsPageProvider { override val name = "SwitchPreference" - private val owner = createSettingsPage() - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - val entryList = mutableListOf<SettingsEntry>() - entryList.add( - SettingsEntryBuilder.create( "SwitchPreference", owner) - .setUiLayoutFn { - SampleSwitchPreference() - }.build() - ) - entryList.add( - SettingsEntryBuilder.create( "SwitchPreference with summary", owner) - .setUiLayoutFn { - SampleSwitchPreferenceWithSummary() - }.build() - ) - entryList.add( - SettingsEntryBuilder.create( "SwitchPreference with async summary", owner) - .setUiLayoutFn { - SampleSwitchPreferenceWithAsyncSummary() - }.build() - ) - entryList.add( - SettingsEntryBuilder.create( "SwitchPreference not changeable", owner) - .setUiLayoutFn { - SampleNotChangeableSwitchPreference() - }.build() - ) - entryList.add( - SettingsEntryBuilder.create( "SwitchPreference with icon", owner) - .setUiLayoutFn { - SampleSwitchPreferenceWithIcon() - }.build() - ) - - return entryList - } - - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(TITLE) { + Category { + SampleSwitchPreference() + SampleSwitchPreferenceWithSummary() + SampleSwitchPreferenceWithAsyncSummary() + SampleNotChangeableSwitchPreference() + SampleSwitchPreferenceWithIcon() } + } } - override fun getTitle(arguments: Bundle?): String { - return TITLE + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TopIntroPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TopIntroPreferencePageProvider.kt index b251266e0574..ee08e30ab4ae 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TopIntroPreferencePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TopIntroPreferencePageProvider.kt @@ -50,15 +50,12 @@ object TopIntroPreferencePageProvider : SettingsPageProvider { return entryList } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner).setUiLayoutFn { - Preference( - object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - } - ) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePageProvider.kt index 19de31dab046..1a89bb2dc4f4 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePageProvider.kt @@ -25,66 +25,40 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.SettingsEntry -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.Category import kotlinx.coroutines.delay private const val TITLE = "Sample TwoTargetSwitchPreference" object TwoTargetSwitchPreferencePageProvider : SettingsPageProvider { override val name = "TwoTargetSwitchPreference" - private val owner = createSettingsPage() - override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { - val entryList = mutableListOf<SettingsEntry>() - entryList.add( - SettingsEntryBuilder.create( "TwoTargetSwitchPreference", owner) - .setUiLayoutFn { - SampleTwoTargetSwitchPreference() - }.build() - ) - entryList.add( - SettingsEntryBuilder.create( "TwoTargetSwitchPreference with summary", owner) - .setUiLayoutFn { - SampleTwoTargetSwitchPreferenceWithSummary() - }.build() - ) - entryList.add( - SettingsEntryBuilder.create( "TwoTargetSwitchPreference with async summary", owner) - .setUiLayoutFn { - SampleTwoTargetSwitchPreferenceWithAsyncSummary() - }.build() - ) - entryList.add( - SettingsEntryBuilder.create( "TwoTargetSwitchPreference not changeable", owner) - .setUiLayoutFn { - SampleNotChangeableTwoTargetSwitchPreference() - }.build() - ) - - return entryList - } - - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(TITLE) { + Category { + SampleTwoTargetSwitchPreference() + SampleTwoTargetSwitchPreferenceWithSummary() + SampleTwoTargetSwitchPreferenceWithAsyncSummary() + SampleNotChangeableTwoTargetSwitchPreference() } + } } - override fun getTitle(arguments: Bundle?): String { - return TITLE + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ZeroStatePreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ZeroStatePreferencePageProvider.kt index 4a9c5c8fad4f..04b5ceb796e7 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ZeroStatePreferencePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ZeroStatePreferencePageProvider.kt @@ -53,14 +53,12 @@ object ZeroStatePreferencePageProvider : SettingsPageProvider { return entryList } - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/PagerMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/PagerMainPageProvider.kt index 66cc38f74b07..c9a6557d60ef 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/PagerMainPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/PagerMainPageProvider.kt @@ -17,7 +17,7 @@ package com.android.settingslib.spa.gallery.scaffold import android.os.Bundle -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder +import androidx.compose.runtime.Composable import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator @@ -34,13 +34,13 @@ object PagerMainPageProvider : SettingsPageProvider { ScrollablePagerPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), ) - fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } override fun getTitle(arguments: Bundle?) = TITLE } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SearchScaffoldPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SearchScaffoldPageProvider.kt index eac06e3eb52b..0d7cad108b7d 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SearchScaffoldPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SearchScaffoldPageProvider.kt @@ -18,9 +18,7 @@ package com.android.settingslib.spa.gallery.scaffold import android.os.Bundle import androidx.compose.runtime.Composable -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel @@ -32,15 +30,13 @@ private const val TITLE = "Sample SearchScaffold" object SearchScaffoldPageProvider : SettingsPageProvider { override val name = "SearchScaffold" - private val owner = createSettingsPage() - - fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } @Composable override fun Page(arguments: Bundle?) { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SuwScaffoldPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SuwScaffoldPageProvider.kt index a0ab2ce6945d..7b02fcb59cd8 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SuwScaffoldPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/scaffold/SuwScaffoldPageProvider.kt @@ -27,9 +27,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.gallery.R @@ -49,15 +47,13 @@ private const val TITLE = "Sample SuwScaffold" object SuwScaffoldPageProvider : SettingsPageProvider { override val name = "SuwScaffold" - private val owner = createSettingsPage() - - fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } @Composable override fun Page(arguments: Bundle?) { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPageProvider.kt index 7a1fad016d0e..4d3a78a583fc 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPageProvider.kt @@ -19,7 +19,6 @@ package com.android.settingslib.spa.gallery.ui import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.EntrySearchData import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider @@ -31,7 +30,6 @@ import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SimplePreferenceMacro import com.android.settingslib.spa.widget.scaffold.RegularScaffold import com.android.settingslib.spa.widget.ui.Category -import com.android.settingslib.spa.widget.ui.CategoryTitle private const val TITLE = "Sample Category" @@ -39,15 +37,14 @@ object CategoryPageProvider : SettingsPageProvider { override val name = "Category" private val owner = createSettingsPage() - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) + @Composable + fun Entry() { + Preference( + object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) } - .setSearchDataFn { EntrySearchData(title = TITLE) } + ) } override fun getTitle(arguments: Bundle?): String { @@ -70,7 +67,6 @@ object CategoryPageProvider : SettingsPageProvider { SettingsEntryBuilder.create("Preference 3", owner) .setMacro { SimplePreferenceMacro(title = "Preference 2", summary = "Summary 3") } .build() - ) entryList.add( SettingsEntryBuilder.create("Preference 4", owner) @@ -84,11 +80,11 @@ object CategoryPageProvider : SettingsPageProvider { override fun Page(arguments: Bundle?) { val entries = buildEntry(arguments) RegularScaffold(title = getTitle(arguments)) { - CategoryTitle("Category A") - entries[0].UiLayout() - entries[1].UiLayout() - - Category("Category B") { + Category("Category A") { + entries[0].UiLayout() + entries[1].UiLayout() + } + Category { entries[2].UiLayout() entries[3].UiLayout() } @@ -99,7 +95,5 @@ object CategoryPageProvider : SettingsPageProvider { @Preview(showBackground = true) @Composable private fun SpinnerPagePreview() { - SettingsTheme { - SpinnerPageProvider.Page(null) - } + SettingsTheme { CategoryPageProvider.Page(null) } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt index f897d8c58030..e919129e9dac 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt @@ -21,10 +21,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.android.settingslib.spa.framework.common.EntrySearchData -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.widget.preference.Preference @@ -37,17 +34,12 @@ private const val TITLE = "Sample Copyable" object CopyablePageProvider : SettingsPageProvider { override val name = "Copyable" - private val owner = createSettingsPage() - - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } - .setSearchDataFn { EntrySearchData(title = TITLE) } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } @Composable diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPageProvider.kt index 5c5c504a4310..7a4b63291375 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPageProvider.kt @@ -23,9 +23,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider -import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.preference.Preference @@ -39,14 +37,12 @@ private const val TITLE = "Sample Spinner" object SpinnerPageProvider : SettingsPageProvider { override val name = "Spinner" - fun buildInjectEntry(): SettingsEntryBuilder { - return SettingsEntryBuilder.createInject(owner = createSettingsPage()) - .setUiLayoutFn { - Preference(object : PreferenceModel { - override val title = TITLE - override val onClick = navigator(name) - }) - } + @Composable + fun Entry() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) } override fun getTitle(arguments: Bundle?): String { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt index f8c791aab0d0..ab95162fb142 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt @@ -24,6 +24,7 @@ object SettingsDimension { val paddingExtraSmall = 4.dp val paddingSmall = if (isSpaExpressiveEnabled) 8.dp else 4.dp val paddingExtraSmall5 = 10.dp + val paddingExtraSmall6 = 12.dp val paddingLarge = 16.dp val paddingExtraLarge = 24.dp @@ -36,9 +37,9 @@ object SettingsDimension { val itemIconSize = 24.dp val itemIconContainerSize = 72.dp - val itemPaddingStart = paddingExtraLarge + val itemPaddingStart = if (isSpaExpressiveEnabled) paddingLarge else paddingExtraLarge val itemPaddingEnd = paddingLarge - val itemPaddingVertical = paddingLarge + val itemPaddingVertical = if (isSpaExpressiveEnabled) paddingExtraSmall6 else paddingLarge val itemPadding = PaddingValues( start = itemPaddingStart, top = itemPaddingVertical, diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt index f7c5414a420c..c78771566f64 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt @@ -24,5 +24,7 @@ object SettingsShape { val CornerMedium = RoundedCornerShape(12.dp) + val categoryCorner = RoundedCornerShape(20.dp) + val CornerExtraLarge = RoundedCornerShape(28.dp) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt index 460bf9993b41..965c97124329 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt @@ -162,3 +162,6 @@ internal fun rememberSettingsTypography(): Typography { /** Creates a new [TextStyle] which font weight set to medium. */ internal fun TextStyle.toMediumWeight() = copy(fontWeight = FontWeight.Medium, letterSpacing = 0.01.em) + +internal fun TextStyle.toSemiBoldWeight() = + copy(fontWeight = FontWeight.SemiBold, letterSpacing = 0.01.em) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/banner/SettingsBanner.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/banner/SettingsBanner.kt index 185fd2974fb1..38707b0378bc 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/banner/SettingsBanner.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/banner/SettingsBanner.kt @@ -57,6 +57,7 @@ import com.android.settingslib.spa.framework.theme.SettingsShape.CornerExtraLarg import com.android.settingslib.spa.framework.theme.SettingsShape.CornerExtraSmall import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled +import com.android.settingslib.spa.framework.theme.toSemiBoldWeight import com.android.settingslib.spa.widget.ui.SettingsBody import com.android.settingslib.spa.widget.ui.SettingsTitle @@ -159,7 +160,9 @@ fun BannerHeader(imageVector: ImageVector?, iconColor: Color, onDismiss: (() -> @Composable fun BannerTitleHeader(title: String, onDismiss: (() -> Unit)? = null) { Row(Modifier.fillMaxWidth()) { - Box(modifier = Modifier.weight(1f)) { SettingsTitle(title) } + Box(modifier = Modifier.weight(1f)) { + Text(text = title, style = MaterialTheme.typography.titleMedium.toSemiBoldWeight()) + } Spacer(modifier = Modifier.padding(SettingsDimension.paddingSmall)) DismissButton(onDismiss) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt index 7e1df1694b10..203a8bd39fae 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt @@ -56,6 +56,7 @@ import com.android.settingslib.spa.framework.theme.SettingsShape import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.framework.theme.divider import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled +import com.android.settingslib.spa.framework.theme.toSemiBoldWeight data class ActionButton( val text: String, @@ -110,7 +111,7 @@ private fun RowScope.ActionButton(actionButton: ActionButton) { shape = RectangleShape, colors = ButtonDefaults.filledTonalButtonColors( containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimary, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, disabledContainerColor = MaterialTheme.colorScheme.surface, ), contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp), @@ -129,7 +130,7 @@ private fun RowScope.ActionButton(actionButton: ActionButton) { Text( text = actionButton.text, textAlign = TextAlign.Center, - style = MaterialTheme.typography.labelMedium, + style = MaterialTheme.typography.labelLarge.toSemiBoldWeight(), ) } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt index 265864e1b3fd..490936fa7a47 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -99,7 +100,16 @@ private fun AlertDialogPresenter.SettingsAlertDialog( dismissButton?.let { { if (isSpaExpressiveEnabled) DismissButton(it) else Button(it) } }, - title = title?.let { { CenterRow { Text(it) } } }, + title = + title?.let { + { + CenterRow { + if (isSpaExpressiveEnabled) + Text(it, style = MaterialTheme.typography.bodyLarge) + else Text(it) + } + } + }, text = text?.let { { CenterRow { Column(Modifier.verticalScroll(rememberScrollState())) { text() } } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt index 23a8e78e6c4a..c68ec78b1ba6 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt @@ -16,6 +16,7 @@ package com.android.settingslib.spa.widget.preference +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -25,16 +26,20 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsOpacity.alphaForEnabled +import com.android.settingslib.spa.framework.theme.SettingsShape import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled import com.android.settingslib.spa.widget.ui.SettingsTitle @Composable @@ -51,10 +56,17 @@ internal fun BaseLayout( widget: @Composable () -> Unit = {}, ) { Row( - modifier = modifier - .fillMaxWidth() - .semantics(mergeDescendants = true) {} - .padding(end = paddingEnd), + modifier = + modifier + .fillMaxWidth() + .semantics(mergeDescendants = true) {} + .then( + if (isSpaExpressiveEnabled) + Modifier.clip(SettingsShape.CornerExtraSmall) + .background(MaterialTheme.colorScheme.surfaceBright) + else Modifier + ) + .padding(end = paddingEnd), verticalAlignment = Alignment.CenterVertically, ) { val alphaModifier = Modifier.alphaForEnabled(enabled()) @@ -63,20 +75,14 @@ internal fun BaseLayout( title = title, titleContentDescription = titleContentDescription, subTitle = subTitle, - modifier = alphaModifier - .weight(1f) - .padding(vertical = paddingVertical), + modifier = alphaModifier.weight(1f).padding(vertical = paddingVertical), ) widget() } } @Composable -internal fun BaseIcon( - icon: @Composable (() -> Unit)?, - modifier: Modifier, - paddingStart: Dp, -) { +internal fun BaseIcon(icon: @Composable (() -> Unit)?, modifier: Modifier, paddingStart: Dp) { if (icon != null) { Box( modifier = modifier.size(SettingsDimension.itemIconContainerSize), @@ -107,11 +113,6 @@ private fun Titles( @Composable private fun BaseLayoutPreview() { SettingsTheme { - BaseLayout( - title = "Title", - subTitle = { - HorizontalDivider(thickness = 10.dp) - } - ) + BaseLayout(title = "Title", subTitle = { HorizontalDivider(thickness = 10.dp) }) } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/IntroPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/IntroPreference.kt index 22a57554eeaf..77073765d5a9 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/IntroPreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/IntroPreference.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.toSemiBoldWeight @Composable fun IntroPreference( @@ -112,7 +113,7 @@ private fun IntroTitle(title: String) { Text( text = title, textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.titleLarge.toSemiBoldWeight(), color = MaterialTheme.colorScheme.onSurface, ) } @@ -126,7 +127,7 @@ private fun IntroDescription(descriptions: List<String>?) { Text( text = description, textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = SettingsDimension.paddingExtraSmall), ) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt index 3f2e7723c585..b771f367e697 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt @@ -47,6 +47,7 @@ import androidx.graphics.shapes.CornerRounding import androidx.graphics.shapes.RoundedPolygon import androidx.graphics.shapes.star import androidx.graphics.shapes.toPath +import com.android.settingslib.spa.framework.theme.toSemiBoldWeight @Composable fun ZeroStatePreference(icon: ImageVector, text: String? = null, description: String? = null) { @@ -80,7 +81,7 @@ fun ZeroStatePreference(icon: ImageVector, text: String? = null, description: St Text( text = text, textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleMedium.toSemiBoldWeight(), color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 24.dp), ) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt index 94d2c210daab..f99d20669183 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt @@ -41,9 +41,7 @@ import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled internal fun NavigateBack() { val navController = LocalNavController.current val contentDescription = stringResource(R.string.abc_action_bar_up_description) - BackAction(contentDescription) { - navController.navigateBack() - } + BackAction(contentDescription) { navController.navigateBack() } } /** Action that collapses the search bar. */ @@ -55,15 +53,35 @@ internal fun CollapseAction(onClick: () -> Unit) { @Composable private fun BackAction(contentDescription: String, onClick: () -> Unit) { - IconButton(onClick) { + IconButton( + onClick = onClick, + modifier = + if (isSpaExpressiveEnabled) + Modifier + .padding( + start = SettingsDimension.paddingLarge, + end = SettingsDimension.paddingSmall, + top = SettingsDimension.paddingExtraSmall, + bottom = SettingsDimension.paddingExtraSmall, + ) + .size(SettingsDimension.actionIconWidth, SettingsDimension.actionIconHeight) + .clip(SettingsShape.CornerExtraLarge) + else Modifier, + ) { Icon( imageVector = Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = contentDescription, - modifier = if (isSpaExpressiveEnabled) Modifier - .size(SettingsDimension.actionIconWidth, SettingsDimension.actionIconHeight) - .clip(SettingsShape.CornerExtraLarge) - .background(MaterialTheme.colorScheme.surfaceContainerHigh) - .padding(SettingsDimension.actionIconPadding) else Modifier + modifier = + if (isSpaExpressiveEnabled) + Modifier + .size( + SettingsDimension.actionIconWidth, + SettingsDimension.actionIconHeight, + ) + .clip(SettingsShape.CornerExtraLarge) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + .padding(SettingsDimension.actionIconPadding) + else Modifier, ) } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt index 2ae3b569bc70..2c55779c9a01 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt @@ -78,7 +78,9 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled import com.android.settingslib.spa.framework.theme.settingsBackground +import com.android.settingslib.spa.framework.theme.toSemiBoldWeight import kotlin.math.abs import kotlin.math.max import kotlin.math.roundToInt @@ -116,8 +118,12 @@ internal fun CustomizedLargeTopAppBar( ) { TwoRowsTopAppBar( title = { Title(title = title, maxLines = 3) }, - titleTextStyle = MaterialTheme.typography.displaySmall, - smallTitleTextStyle = MaterialTheme.typography.titleMedium, + titleTextStyle = + if (isSpaExpressiveEnabled) MaterialTheme.typography.displaySmall.toSemiBoldWeight() + else MaterialTheme.typography.displaySmall, + smallTitleTextStyle = + if (isSpaExpressiveEnabled) MaterialTheme.typography.titleLarge.toSemiBoldWeight() + else MaterialTheme.typography.titleLarge, titleBottomPadding = LargeTitleBottomPadding, smallTitle = { Title(title = title, maxLines = 1) }, modifier = modifier, @@ -136,7 +142,9 @@ private fun Title(title: String, maxLines: Int = Int.MAX_VALUE) { text = title, modifier = Modifier.padding( - start = SettingsDimension.itemPaddingAround, + start = + if (isSpaExpressiveEnabled) SettingsDimension.paddingExtraSmall + else SettingsDimension.itemPaddingAround, end = SettingsDimension.itemPaddingEnd, ) .semantics { heading() }, @@ -194,7 +202,7 @@ private class TopAppBarColors( return lerp( containerColor, scrolledContainerColor, - FastOutLinearInEasing.transform(colorTransitionFraction) + FastOutLinearInEasing.transform(colorTransitionFraction), ) } @@ -241,7 +249,7 @@ private fun SingleRowTopAppBar( Row( horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, - content = actions + content = actions, ) } @@ -296,7 +304,7 @@ private fun TwoRowsTopAppBar( windowInsets: WindowInsets, colors: TopAppBarColors, pinnedHeight: Dp, - scrollBehavior: TopAppBarScrollBehavior? + scrollBehavior: TopAppBarScrollBehavior?, ) { if (MaxHeightWithoutTitle <= pinnedHeight) { throw IllegalArgumentException( @@ -333,7 +341,7 @@ private fun TwoRowsTopAppBar( Row( horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, - content = actions + content = actions, ) } val topTitleAlpha = { TopTitleAlphaEasing.transform(colorTransitionFraction()) } @@ -356,9 +364,9 @@ private fun TwoRowsTopAppBar( scrollBehavior.state, velocity, scrollBehavior.flingAnimationSpec, - scrollBehavior.snapAnimationSpec + scrollBehavior.snapAnimationSpec, ) - } + }, ) } else { Modifier @@ -412,7 +420,8 @@ private fun TwoRowsTopAppBar( val measuredMaxHeightPx = density.run { MaxHeightWithoutTitle.toPx() + - coordinates.size.height.toFloat() + coordinates.size.height.toFloat() + + titleBaselineHeight.toPx() } // Allow larger max height for multi-line title, but do not reduce // max height to prevent flaky. @@ -430,7 +439,7 @@ private fun TwoRowsTopAppBar( titleBottomPadding = titleBottomPaddingPx, hideTitleSemantics = hideBottomRowSemantics, navigationIcon = {}, - actions = {} + actions = {}, ) } } @@ -485,7 +494,7 @@ private fun TopAppBarLayout( Box(Modifier.layoutId("navigationIcon").padding(start = TopAppBarHorizontalPadding)) { CompositionLocalProvider( LocalContentColor provides navigationIconContentColor, - content = navigationIcon + content = navigationIcon, ) } Box( @@ -504,18 +513,18 @@ private fun TopAppBarLayout( fontScale = if (titleScaleDisabled) 1f else fontScale, ) }, - content = title + content = title, ) } } Box(Modifier.layoutId("actionIcons").padding(end = TopAppBarHorizontalPadding)) { CompositionLocalProvider( LocalContentColor provides actionIconContentColor, - content = actions + content = actions, ) } }, - modifier = modifier + modifier = modifier, ) { measurables, constraints -> val navigationIconPlaceable = measurables @@ -552,7 +561,7 @@ private fun TopAppBarLayout( // Navigation icon navigationIconPlaceable.placeRelative( x = 0, - y = (layoutHeight - navigationIconPlaceable.height) / 2 + y = (layoutHeight - navigationIconPlaceable.height) / 2, ) // Title @@ -570,17 +579,17 @@ private fun TopAppBarLayout( titlePlaceable.height - max( 0, - titleBottomPadding - titlePlaceable.height + titleBaseline + titleBottomPadding - titlePlaceable.height + titleBaseline, ) // Arrangement.Top else -> 0 - } + }, ) // Action icons actionIconsPlaceable.placeRelative( x = constraints.maxWidth - actionIconsPlaceable.width, - y = (layoutHeight - actionIconsPlaceable.height) / 2 + y = (layoutHeight - actionIconsPlaceable.height) / 2, ) } } @@ -595,7 +604,7 @@ private suspend fun settleAppBar( state: TopAppBarState, velocity: Float, flingAnimationSpec: DecayAnimationSpec<Float>?, - snapAnimationSpec: AnimationSpec<Float>? + snapAnimationSpec: AnimationSpec<Float>?, ): Velocity { // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar, // and just return Zero Velocity. @@ -609,20 +618,18 @@ private suspend fun settleAppBar( // continue the motion to expand or collapse the app bar. if (flingAnimationSpec != null && abs(velocity) > 1f) { var lastValue = 0f - AnimationState( - initialValue = 0f, - initialVelocity = velocity, - ) - .animateDecay(flingAnimationSpec) { - val delta = value - lastValue - val initialHeightOffset = state.heightOffset - state.heightOffset = initialHeightOffset + delta - val consumed = abs(initialHeightOffset - state.heightOffset) - lastValue = value - remainingVelocity = this.velocity - // avoid rounding errors and stop if anything is unconsumed - if (abs(delta - consumed) > 0.5f) this.cancelAnimation() - } + AnimationState(initialValue = 0f, initialVelocity = velocity).animateDecay( + flingAnimationSpec + ) { + val delta = value - lastValue + val initialHeightOffset = state.heightOffset + state.heightOffset = initialHeightOffset + delta + val consumed = abs(initialHeightOffset - state.heightOffset) + lastValue = value + remainingVelocity = this.velocity + // avoid rounding errors and stop if anything is unconsumed + if (abs(delta - consumed) > 0.5f) this.cancelAnimation() + } } // Snap if animation specs were provided. if (snapAnimationSpec != null) { @@ -633,7 +640,7 @@ private suspend fun settleAppBar( } else { state.heightOffsetLimit }, - animationSpec = snapAnimationSpec + animationSpec = snapAnimationSpec, ) { state.heightOffset = value } @@ -647,9 +654,10 @@ private suspend fun settleAppBar( // Medium or Large app bar. private val TopTitleAlphaEasing = CubicBezierEasing(.8f, 0f, .8f, .15f) -internal val MaxHeightWithoutTitle = 124.dp +internal val MaxHeightWithoutTitle = if (isSpaExpressiveEnabled) 84.dp else 124.dp internal val DefaultTitleHeight = 52.dp internal val ContainerHeight = 56.dp +private val titleBaselineHeight = if (isSpaExpressiveEnabled) 8.dp else 0.dp private val LargeTitleBottomPadding = 28.dp private val TopAppBarHorizontalPadding = 4.dp diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt index 48cd145da124..6c5581fb4b50 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt @@ -16,9 +16,13 @@ package com.android.settingslib.spa.widget.ui +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.TouchApp import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -27,25 +31,31 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.SettingsShape import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel -/** - * A category title that is placed before a group of similar items. - */ +/** A category title that is placed before a group of similar items. */ @Composable fun CategoryTitle(title: String) { Text( text = title, - modifier = Modifier.padding( - start = SettingsDimension.itemPaddingStart, - top = 20.dp, - end = SettingsDimension.itemPaddingEnd, - bottom = 8.dp, - ), + modifier = + Modifier.padding( + start = SettingsDimension.itemPaddingStart, + top = 20.dp, + end = + if (isSpaExpressiveEnabled) SettingsDimension.paddingSmall + else SettingsDimension.itemPaddingEnd, + bottom = 8.dp, + ), color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.labelMedium, ) @@ -56,14 +66,31 @@ fun CategoryTitle(title: String) { * visually separates groups of items. */ @Composable -fun Category(title: String, content: @Composable ColumnScope.() -> Unit) { - Column { +fun Category(title: String? = null, content: @Composable ColumnScope.() -> Unit) { + Column( + modifier = + if (isSpaExpressiveEnabled) + Modifier.padding( + horizontal = SettingsDimension.paddingLarge, + vertical = SettingsDimension.paddingSmall, + ) + else Modifier + ) { var displayTitle by remember { mutableStateOf(false) } - if (displayTitle) CategoryTitle(title = title) + if (title != null && displayTitle) CategoryTitle(title = title) Column( - modifier = Modifier.onGloballyPositioned { coordinates -> - displayTitle = coordinates.size.height > 0 - }, + modifier = + Modifier.onGloballyPositioned { coordinates -> + displayTitle = coordinates.size.height > 0 + } + .then( + if (isSpaExpressiveEnabled) + Modifier.fillMaxWidth().clip(SettingsShape.categoryCorner) + else Modifier + ), + verticalArrangement = + if (isSpaExpressiveEnabled) Arrangement.spacedBy(SettingsDimension.paddingTiny) + else Arrangement.Top, content = content, ) } @@ -73,6 +100,21 @@ fun Category(title: String, content: @Composable ColumnScope.() -> Unit) { @Composable private fun CategoryPreview() { SettingsTheme { - CategoryTitle("Appearance") + Category("Appearance") { + Preference( + object : PreferenceModel { + override val title = "Title" + override val summary = { "Summary" } + } + ) + Preference( + object : PreferenceModel { + override val title = "Title" + override val summary = { "Summary" } + override val icon = + @Composable { SettingsIcon(imageVector = Icons.Outlined.TouchApp) } + } + ) + } } } diff --git a/packages/SettingsLib/TopIntroPreference/Android.bp b/packages/SettingsLib/TopIntroPreference/Android.bp index e70201b0feb7..76e36dc5ff7d 100644 --- a/packages/SettingsLib/TopIntroPreference/Android.bp +++ b/packages/SettingsLib/TopIntroPreference/Android.bp @@ -14,7 +14,10 @@ android_library { "SettingsLintDefaults", ], - srcs: ["src/**/*.java"], + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], resource_dirs: ["res"], static_libs: [ diff --git a/packages/SettingsLib/TopIntroPreference/res/layout-v35/settingslib_expressive_top_intro.xml b/packages/SettingsLib/TopIntroPreference/res/layout-v35/settingslib_expressive_top_intro.xml new file mode 100644 index 000000000000..fb13ef79cc3b --- /dev/null +++ b/packages/SettingsLib/TopIntroPreference/res/layout-v35/settingslib_expressive_top_intro.xml @@ -0,0 +1,27 @@ +<?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:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:paddingStart="?android:attr/listPreferredItemPaddingStart"> + + <com.android.settingslib.widget.CollapsableTextView + android:id="@+id/collapsable_text_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.java b/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.java deleted file mode 100644 index 1bbd76d86b7f..000000000000 --- a/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.widget; - -import android.content.Context; -import android.util.AttributeSet; - -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; - -import com.android.settingslib.widget.preference.topintro.R; - -/** - * The TopIntroPreference shows a text which describe a feature. Gernerally, we expect this - * preference always shows on the top of screen. - */ -public class TopIntroPreference extends Preference { - - public TopIntroPreference(Context context) { - super(context); - setLayoutResource(R.layout.top_intro_preference); - setSelectable(false); - } - - public TopIntroPreference(Context context, AttributeSet attrs) { - super(context, attrs); - setLayoutResource(R.layout.top_intro_preference); - setSelectable(false); - } - - @Override - public void onBindViewHolder(PreferenceViewHolder holder) { - super.onBindViewHolder(holder); - holder.setDividerAllowedAbove(false); - holder.setDividerAllowedBelow(false); - } -} diff --git a/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.kt b/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.kt new file mode 100644 index 000000000000..afced0c8d638 --- /dev/null +++ b/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.kt @@ -0,0 +1,108 @@ +/* + * 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.widget + +import android.content.Context +import android.os.Build +import android.util.AttributeSet +import androidx.annotation.RequiresApi +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import com.android.settingslib.widget.preference.topintro.R + +open class TopIntroPreference @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : Preference(context, attrs, defStyleAttr, defStyleRes) { + + private var isCollapsable: Boolean = false + private var minLines: Int = 2 + + init { + if (SettingsThemeHelper.isExpressiveTheme(context)) { + layoutResource = R.layout.settingslib_expressive_top_intro + initAttributes(context, attrs, defStyleAttr) + } else { + layoutResource = R.layout.top_intro_preference + } + isSelectable = false + } + + private fun initAttributes(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { + context.obtainStyledAttributes( + attrs, + COLLAPSABLE_TEXT_VIEW_ATTRS, defStyleAttr, 0 + ).apply { + isCollapsable = getBoolean(IS_COLLAPSABLE, false) + minLines = getInt( + MIN_LINES, + if (isCollapsable) DEFAULT_MIN_LINES else DEFAULT_MAX_LINES + ).coerceIn(1, DEFAULT_MAX_LINES) + recycle() + } + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + holder.isDividerAllowedAbove = false + holder.isDividerAllowedBelow = false + + if (!SettingsThemeHelper.isExpressiveTheme(context)) { + return + } + + (holder.findViewById(R.id.collapsable_text_view) as? CollapsableTextView)?.apply { + setCollapsable(isCollapsable) + setMinLines(minLines) + setText(title.toString()) + } + } + + /** + * Sets whether the text view is collapsable. + * @param collapsable True if the text view should be collapsable, false otherwise. + */ + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + fun setCollapsable(collapsable: Boolean) { + isCollapsable = collapsable + notifyChanged() + } + + /** + * Sets the minimum number of lines to display when collapsed. + * @param lines The minimum number of lines. + */ + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + fun setMinLines(lines: Int) { + minLines = lines.coerceIn(1, DEFAULT_MAX_LINES) + notifyChanged() + } + + companion object { + private const val DEFAULT_MAX_LINES = 10 + private const val DEFAULT_MIN_LINES = 2 + + private val COLLAPSABLE_TEXT_VIEW_ATTRS = + com.android.settingslib.widget.theme.R.styleable.CollapsableTextView + private val MIN_LINES = + com.android.settingslib.widget.theme.R.styleable.CollapsableTextView_android_minLines + private val IS_COLLAPSABLE = + com.android.settingslib.widget.theme.R.styleable.CollapsableTextView_isCollapsable + } +} diff --git a/packages/SettingsLib/ZeroStatePreference/Android.bp b/packages/SettingsLib/ZeroStatePreference/Android.bp new file mode 100644 index 000000000000..4fc00bdbfee0 --- /dev/null +++ b/packages/SettingsLib/ZeroStatePreference/Android.bp @@ -0,0 +1,33 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_library { + name: "SettingsLibZeroStatePreference", + use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], + + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + resource_dirs: ["res"], + + static_libs: [ + "androidx.annotation_annotation", + "androidx.preference_preference", + "SettingsLibSettingsTheme", + ], + sdk_version: "system_current", + min_sdk_version: "28", + apex_available: [ + "//apex_available:platform", + ], +} diff --git a/packages/SettingsLib/ZeroStatePreference/AndroidManifest.xml b/packages/SettingsLib/ZeroStatePreference/AndroidManifest.xml new file mode 100644 index 000000000000..51b0ab86c835 --- /dev/null +++ b/packages/SettingsLib/ZeroStatePreference/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.settingslib.widget.preference.zerostate"> + + <uses-sdk android:minSdkVersion="28" /> + +</manifest> diff --git a/packages/SettingsLib/ZeroStatePreference/res/drawable/settingslib_expressive_zerostate_background.xml b/packages/SettingsLib/ZeroStatePreference/res/drawable/settingslib_expressive_zerostate_background.xml new file mode 100644 index 000000000000..f42b4415c39e --- /dev/null +++ b/packages/SettingsLib/ZeroStatePreference/res/drawable/settingslib_expressive_zerostate_background.xml @@ -0,0 +1,26 @@ +<?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="160dp" + android:height="160dp" + android:viewportWidth="161" + android:viewportHeight="161"> + <path + android:pathData="M67.2,4.43C74.79,-1.41 85.35,-1.41 92.94,4.43L112.01,19.1C113.48,20.23 115.09,21.16 116.8,21.87L139.02,31.08C147.86,34.74 153.14,43.9 151.89,53.4L148.74,77.28C148.5,79.12 148.5,80.98 148.74,82.82L151.89,106.71C153.14,116.2 147.86,125.36 139.02,129.03L116.8,138.23C115.09,138.95 113.48,139.87 112.01,141L92.94,155.67C85.35,161.51 74.79,161.51 67.2,155.67L48.13,141C46.66,139.87 45.05,138.95 43.34,138.23L21.12,129.03C12.28,125.36 7,116.2 8.25,106.71L11.4,82.82C11.64,80.98 11.64,79.12 11.4,77.28L8.25,53.4C7,43.9 12.28,34.74 21.12,31.08L43.34,21.87C45.05,21.16 46.66,20.23 48.13,19.1L67.2,4.43Z" + android:fillColor="@color/settingslib_materialColorSurfaceContainerHigh"/> +</vector> diff --git a/packages/SettingsLib/ZeroStatePreference/res/layout/settingslib_expressive_preference_zerostate.xml b/packages/SettingsLib/ZeroStatePreference/res/layout/settingslib_expressive_preference_zerostate.xml new file mode 100644 index 000000000000..c0b195cc1f74 --- /dev/null +++ b/packages/SettingsLib/ZeroStatePreference/res/layout/settingslib_expressive_preference_zerostate.xml @@ -0,0 +1,57 @@ +<?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:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:gravity="center" + android:orientation="vertical"> + + <FrameLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + tools:ignore="ContentDescription"> + + <ImageView + android:layout_width="@dimen/settingslib_expressive_zero_state_background_size" + android:layout_height="@dimen/settingslib_expressive_zero_state_background_size" + android:src="@drawable/settingslib_expressive_zerostate_background"/> + + <ImageView + android:id="@android:id/icon" + android:layout_width="@dimen/settingslib_expressive_space_large3" + android:layout_height="@dimen/settingslib_expressive_space_large3" + android:layout_gravity="center"/> + + </FrameLayout> + <TextView + android:id="@android:id/title" + android:layout_width="@dimen/settingslib_expressive_zero_state_title_width" + android:layout_height="wrap_content" + android:gravity="center" + android:textAppearance="?android:attr/textAppearanceMedium" + android:layout_marginTop="@dimen/settingslib_expressive_space_small4"/> + <TextView + android:id="@android:id/summary" + android:layout_width="@dimen/settingslib_expressive_zero_state_title_width" + android:layout_height="wrap_content" + android:gravity="center" + android:layout_marginTop="@dimen/settingslib_expressive_space_small4" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:attr/textColorSecondary"/> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SettingsLib/ZeroStatePreference/res/values/dimens.xml b/packages/SettingsLib/ZeroStatePreference/res/values/dimens.xml new file mode 100644 index 000000000000..e981eccf2be2 --- /dev/null +++ b/packages/SettingsLib/ZeroStatePreference/res/values/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> + <dimen name="settingslib_expressive_zero_state_background_size">160dp</dimen> + <dimen name="settingslib_expressive_zero_state_title_width">316dp</dimen> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/ZeroStatePreference/src/com/android/settingslib/widget/ZeroStatePreference.kt b/packages/SettingsLib/ZeroStatePreference/src/com/android/settingslib/widget/ZeroStatePreference.kt new file mode 100644 index 000000000000..9b1ccef9dadf --- /dev/null +++ b/packages/SettingsLib/ZeroStatePreference/src/com/android/settingslib/widget/ZeroStatePreference.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.widget + +import android.content.Context +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.widget.ImageView +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder + +import com.android.settingslib.widget.preference.zerostate.R + +class ZeroStatePreference @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : Preference(context, attrs, defStyleAttr, defStyleRes) { + + private val iconTint: Int = context.getColor( + com.android.settingslib.widget.theme.R.color.settingslib_materialColorOnSecondaryContainer + ) + private var tintedIcon: Drawable? = null + + init { + isSelectable = false + layoutResource = R.layout.settingslib_expressive_preference_zerostate + icon?.let { originalIcon -> + tintedIcon = originalIcon.mutate().apply { + colorFilter = PorterDuffColorFilter(iconTint, PorterDuff.Mode.SRC_IN) + } + } + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + + holder.itemView.isFocusable = false + holder.itemView.isClickable = false + + (holder.findViewById(android.R.id.icon) as? ImageView)?.apply { + setImageDrawable(tintedIcon ?: icon) + } + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 34e33c0df8f5..efc98dbf7102 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1645,13 +1645,13 @@ <string name="media_transfer_headphone_name">Headphone</string> <!-- Name of the usb audio device speaker, used in desktop devices. [CHAR LIMIT=50] --> - <string name="media_transfer_usb_speaker_name">USB speaker</string> + <string name="media_transfer_usb_audio_name">USB audio</string> <!-- Name of the 3.5mm audio device mic. [CHAR LIMIT=50] --> <string name="media_transfer_wired_device_mic_name">Mic jack</string> <!-- Name of the usb audio device mic. [CHAR LIMIT=50] --> - <string name="media_transfer_usb_device_mic_name">USB mic</string> + <string name="media_transfer_usb_device_mic_name">USB microphone</string> <!-- Label for Wifi hotspot switch on. Toggles hotspot on [CHAR LIMIT=30] --> <string name="wifi_hotspot_switch_on_text">On</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index 616ab072ae20..612c193da9c3 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -709,6 +709,9 @@ public class BluetoothUtils { @WorkerThread public static boolean hasConnectedBroadcastSourceForBtDevice( @Nullable BluetoothDevice device, @Nullable LocalBluetoothManager localBtManager) { + if (Flags.audioSharingHysteresisModeFix()) { + return hasActiveLocalBroadcastSourceForBtDevice(device, localBtManager); + } LocalBluetoothLeBroadcastAssistant assistant = localBtManager == null ? null diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index a3f9e515a0bc..364e95c61ca8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -52,6 +52,7 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.android.settingslib.R; +import com.android.settingslib.flags.Flags; import com.google.common.collect.ImmutableList; @@ -1134,20 +1135,8 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to assistant profile is null"); return; } - List<BluetoothDevice> connectedDevices = mServiceBroadcastAssistant.getConnectedDevices(); - List<BluetoothDevice> devicesInSharing = - connectedDevices.stream() - .filter( - bluetoothDevice -> { - List<BluetoothLeBroadcastReceiveState> sourceList = - mServiceBroadcastAssistant.getAllSources( - bluetoothDevice); - return !sourceList.isEmpty() - && sourceList.stream() - .anyMatch(BluetoothUtils::isConnected); - }) - .collect(Collectors.toList()); - if (devicesInSharing.isEmpty()) { + List<BluetoothDevice> devicesInBroadcast = getDevicesInBroadcast(); + if (devicesInBroadcast.isEmpty()) { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to no sinks in broadcast"); return; } @@ -1156,7 +1145,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { BluetoothDevice targetDevice = null; // Find the earliest connected device in sharing session. int targetDeviceIdx = -1; - for (BluetoothDevice device : devicesInSharing) { + for (BluetoothDevice device : devicesInBroadcast) { if (devices.contains(device)) { int idx = devices.indexOf(device); if (idx > targetDeviceIdx) { @@ -1169,10 +1158,6 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, target is null"); return; } - Log.d( - TAG, - "updateFallbackActiveDeviceIfNeeded, set active device: " - + targetDevice.getAnonymizedAddress()); CachedBluetoothDevice targetCachedDevice = mDeviceManager.findDevice(targetDevice); if (targetCachedDevice == null) { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, fail to find cached bt device"); @@ -1180,16 +1165,37 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } int fallbackActiveGroupId = getFallbackActiveGroupId(); if (fallbackActiveGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID - && getGroupId(targetCachedDevice) == fallbackActiveGroupId) { + && BluetoothUtils.getGroupId(targetCachedDevice) == fallbackActiveGroupId) { Log.d( TAG, "Skip updateFallbackActiveDeviceIfNeeded, already is fallback: " + fallbackActiveGroupId); return; } + Log.d( + TAG, + "updateFallbackActiveDeviceIfNeeded, set active device: " + + targetDevice.getAnonymizedAddress()); targetCachedDevice.setActive(); } + private List<BluetoothDevice> getDevicesInBroadcast() { + boolean hysteresisModeFixEnabled = Flags.audioSharingHysteresisModeFix(); + List<BluetoothDevice> connectedDevices = mServiceBroadcastAssistant.getConnectedDevices(); + return connectedDevices.stream() + .filter( + bluetoothDevice -> { + List<BluetoothLeBroadcastReceiveState> sourceList = + mServiceBroadcastAssistant.getAllSources( + bluetoothDevice); + return !sourceList.isEmpty() && sourceList.stream().anyMatch( + source -> hysteresisModeFixEnabled + ? BluetoothUtils.isSourceMatched(source, mBroadcastId) + : BluetoothUtils.isConnected(source)); + }) + .collect(Collectors.toList()); + } + private int getFallbackActiveGroupId() { return Settings.Secure.getInt( mContext.getContentResolver(), @@ -1197,23 +1203,6 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { BluetoothCsipSetCoordinator.GROUP_ID_INVALID); } - private int getGroupId(CachedBluetoothDevice cachedDevice) { - int groupId = cachedDevice.getGroupId(); - String anonymizedAddress = cachedDevice.getDevice().getAnonymizedAddress(); - if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { - Log.d(TAG, "getGroupId by CSIP profile for device: " + anonymizedAddress); - return groupId; - } - for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) { - if (profile instanceof LeAudioProfile) { - Log.d(TAG, "getGroupId by LEA profile for device: " + anonymizedAddress); - return ((LeAudioProfile) profile).getGroupId(cachedDevice.getDevice()); - } - } - Log.d(TAG, "getGroupId return invalid id for device: " + anonymizedAddress); - return BluetoothCsipSetCoordinator.GROUP_ID_INVALID; - } - private void notifyBroadcastStateChange(@BroadcastState int state) { if (!mContext.getPackageName().equals(SETTINGS_PKG)) { Log.d(TAG, "Skip notifyBroadcastStateChange, not triggered by Settings."); 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 58dc8c7aad6c..e7c7476d4797 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java @@ -45,6 +45,7 @@ import java.lang.annotation.RetentionPolicy; DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS, DeviceSettingId.DEVICE_SETTING_ID_DEVICE_DETAILS_FOOTER, DeviceSettingId.DEVICE_SETTING_ID_ANC, + DeviceSettingId.DEVICE_SETTING_ID_GENERAL_BLUETOOTH_DEVICE_HEADER, }, open = true) public @interface DeviceSettingId { @@ -114,6 +115,9 @@ public @interface DeviceSettingId { /** Device setting ID for "More Settings" page. */ int DEVICE_SETTING_ID_MORE_SETTINGS = 21; + /** Device setting ID for general bluetooth device header. */ + int DEVICE_SETTING_ID_GENERAL_BLUETOOTH_DEVICE_HEADER = 22; + /** Device setting ID for ANC. */ int DEVICE_SETTING_ID_ANC = 1001; } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt index 38183d5a01fd..da01b3bcaafb 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt @@ -32,9 +32,9 @@ import android.os.Parcelable */ data class DeviceSettingItem( @DeviceSettingId val settingId: Int, - val packageName: String, - val className: String, - val intentAction: String, + val packageName: String? = null, + val className: String? = null, + val intentAction: String? = null, val preferenceKey: String? = null, val highlighted: Boolean = false, val extras: Bundle = Bundle.EMPTY, @@ -62,11 +62,11 @@ data class DeviceSettingItem( parcel.run { DeviceSettingItem( settingId = readInt(), - packageName = readString() ?: "", - className = readString() ?: "", - intentAction = readString() ?: "", + packageName = readString(), + className = readString(), + intentAction = readString(), highlighted = readBoolean(), - preferenceKey = readString() ?: "", + preferenceKey = readString(), extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY, ) } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt index 5656f38a0a11..36276696e3ec 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt @@ -63,7 +63,8 @@ data class DeviceSettingsConfig( }, moreSettingsHelpItem = readParcelable( DeviceSettingItem::class.java.classLoader - ) + ), + extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY, ) } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.aidl new file mode 100644 index 000000000000..d8378067b115 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.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.settingslib.bluetooth.devicesettings; + +parcelable DeviceSettingsConfigServiceStatus;
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.kt new file mode 100644 index 000000000000..ae867713c831 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatus.kt @@ -0,0 +1,60 @@ +/* + * 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.bluetooth.devicesettings + +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable + +/** + * A data class representing a device settings config service status. + * + * @property success Whether the status is succeed. + * @property extras Extra bundle + */ +data class DeviceSettingsConfigServiceStatus( + val success: Boolean, + val extras: Bundle = Bundle.EMPTY, +) : Parcelable { + + override fun describeContents(): Int = 0 + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.run { + writeBoolean(success) + writeBundle(extras) + } + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator<DeviceSettingsConfigServiceStatus> = + object : Parcelable.Creator<DeviceSettingsConfigServiceStatus> { + override fun createFromParcel(parcel: Parcel) = + parcel.run { + DeviceSettingsConfigServiceStatus( + success = readBoolean(), + extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY, + ) + } + + override fun newArray(size: Int): Array<DeviceSettingsConfigServiceStatus?> { + return arrayOfNulls(size) + } + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt index 977849e75556..77d790e7f773 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt @@ -21,7 +21,7 @@ import android.os.Parcel import android.os.Parcelable /** - * A data class representing a device settings item in bluetooth device details config. + * A data class representing a device settings provider service status. * * @property enabled Whether the service is enabled. * @property extras Extra bundle diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl index 647611ed8ef4..9cf49070a62c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl @@ -17,8 +17,8 @@ package com.android.settingslib.bluetooth.devicesettings; import com.android.settingslib.bluetooth.devicesettings.DeviceInfo; -import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig; +import com.android.settingslib.bluetooth.devicesettings.IGetDeviceSettingsConfigCallback; interface IDeviceSettingsConfigProviderService { - DeviceSettingsConfig getDeviceSettingsConfig(in DeviceInfo device); + oneway void getDeviceSettingsConfig(in DeviceInfo device, in IGetDeviceSettingsConfigCallback callback); }
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IGetDeviceSettingsConfigCallback.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IGetDeviceSettingsConfigCallback.aidl new file mode 100644 index 000000000000..403cdd9e4d70 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IGetDeviceSettingsConfigCallback.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 com.android.settingslib.bluetooth.devicesettings; + +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig; +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfigServiceStatus; + +interface IGetDeviceSettingsConfigCallback { + oneway void onResult(in DeviceSettingsConfigServiceStatus status, in DeviceSettingsConfig config) = 0; +}
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt index 3d8ff86c9377..4af0504bd73a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt @@ -33,12 +33,15 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId import com.android.settingslib.bluetooth.devicesettings.DeviceSettingPreferenceState import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfigServiceStatus import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService +import com.android.settingslib.bluetooth.devicesettings.IGetDeviceSettingsConfigCallback import com.android.settingslib.bluetooth.devicesettings.data.model.ServiceConnectionStatus import java.util.concurrent.ConcurrentHashMap import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.resume import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -63,6 +66,7 @@ import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.plus +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext @OptIn(ExperimentalCoroutinesApi::class) @@ -74,22 +78,22 @@ class DeviceSettingServiceConnection( private val backgroundCoroutineContext: CoroutineContext, ) { data class EndPoint( - private val packageName: String, + private val packageName: String?, private val className: String?, - private val intentAction: String, + private val intentAction: String?, ) { - fun toIntent(): Intent = - Intent().apply { + fun toIntent(): Intent? { + if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(intentAction)) { + return null + } + return Intent().apply { if (className.isNullOrBlank()) { setPackage(packageName) } else { - setClassName(packageName, className) + setClassName(packageName!!, className) } setAction(intentAction) } - - fun isValid(): Boolean { - return !TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(intentAction) } } @@ -126,8 +130,9 @@ class DeviceSettingServiceConnection( when (it) { is ServiceConnectionStatus.Connected -> flowOf( - it.service.getDeviceSettingsConfig( - deviceInfo { setBluetoothAddress(cachedDevice.address) } + getDeviceSettingsConfigFromService( + deviceInfo { setBluetoothAddress(cachedDevice.address) }, + it.service, ) ) ServiceConnectionStatus.Connecting -> flowOf() @@ -137,6 +142,27 @@ class DeviceSettingServiceConnection( .first() } + private suspend fun getDeviceSettingsConfigFromService( + deviceInfo: DeviceInfo, + service: IDeviceSettingsConfigProviderService, + ): DeviceSettingsConfig? = suspendCancellableCoroutine { continuation -> + service.getDeviceSettingsConfig( + deviceInfo, + object : IGetDeviceSettingsConfigCallback.Stub() { + override fun onResult( + status: DeviceSettingsConfigServiceStatus, + config: DeviceSettingsConfig?, + ) { + if (!status.success) { + continuation.resume(null) + } else { + continuation.resume(config) + } + } + }, + ) + } + private val settingIdToItemMapping = flow { if (!isServiceEnabled.await()) { @@ -160,6 +186,12 @@ class DeviceSettingServiceConnection( } .shareIn(scope = coroutineScope, started = SharingStarted.WhileSubscribed(), replay = 1) + private val services = + ConcurrentHashMap< + EndPoint, + StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>, + >() + /** Gets [DeviceSettingsConfig] for the device, return null when failed. */ suspend fun getDeviceSettingsConfig(): DeviceSettingsConfig? { if (!isServiceEnabled.await()) { @@ -222,24 +254,23 @@ class DeviceSettingServiceConnection( ) } } - ?.filter { it.isValid() } ?.distinct() - ?.associateBy( - { it }, - { endpoint -> - services.computeIfAbsent(endpoint) { - getService( - endpoint.toIntent(), - IDeviceSettingsProviderService.Stub::asInterface, - ) - .stateIn( - coroutineScope.plus(backgroundCoroutineContext), - SharingStarted.WhileSubscribed(), - ServiceConnectionStatus.Connecting, - ) - } - }, - ) + ?.mapNotNull { endpoint -> + endpoint.toIntent()?.let { intent -> + Pair( + endpoint, + services.computeIfAbsent(endpoint) { + getService(intent, IDeviceSettingsProviderService.Stub::asInterface) + .stateIn( + coroutineScope.plus(backgroundCoroutineContext), + SharingStarted.WhileSubscribed(), + ServiceConnectionStatus.Connecting, + ) + }, + ) + } + } + ?.toMap() private fun getDeviceSettingsFromService( cachedDevice: CachedBluetoothDevice, @@ -320,11 +351,5 @@ class DeviceSettingServiceConnection( const val CONFIG_SERVICE_PACKAGE_NAME = "DEVICE_SETTINGS_CONFIG_PACKAGE_NAME" const val CONFIG_SERVICE_CLASS_NAME = "DEVICE_SETTINGS_CONFIG_CLASS" const val CONFIG_SERVICE_INTENT_ACTION = "DEVICE_SETTINGS_CONFIG_ACTION" - - val services = - ConcurrentHashMap< - EndPoint, - StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>, - >() } } diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java index ef0f6cbc6ed9..13a06017abbc 100644 --- a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java +++ b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java @@ -42,6 +42,8 @@ import androidx.annotation.Nullable; import com.android.settingslib.R; import com.android.settingslib.Utils; +import java.util.Objects; + /** * Drawable displaying a mobile cell signal indicator. */ @@ -90,6 +92,10 @@ public class SignalDrawable extends DrawableWrapper { private int mCurrentDot; public SignalDrawable(Context context) { + this(context, new Handler()); + } + + public SignalDrawable(@NonNull Context context, @NonNull Handler handler) { super(context.getDrawable(ICON_RES)); final String attributionPathString = context.getString( com.android.internal.R.string.config_signalAttributionPath); @@ -106,7 +112,7 @@ public class SignalDrawable extends DrawableWrapper { mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size); mTransparentPaint.setColor(context.getColor(android.R.color.transparent)); mTransparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); - mHandler = new Handler(); + mHandler = handler; setDarkIntensity(0); } @@ -304,6 +310,17 @@ public class SignalDrawable extends DrawableWrapper { | level; } + @Override + public boolean equals(@Nullable Object other) { + return other instanceof SignalDrawable + && ((SignalDrawable) other).getLevel() == this.getLevel(); + } + + @Override + public int hashCode() { + return Objects.hash(getLevel()); + } + /** Returns the state representing empty mobile signal with the given number of levels. */ public static int getEmptyState(int numLevels) { return getState(0, numLevels, true); diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java index 0b8fb22cef3a..feaf7fbc4b64 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java @@ -97,7 +97,7 @@ public class PhoneMediaDevice extends MediaDevice { case TYPE_USB_ACCESSORY: name = inputRoutingEnabledAndIsDesktop() - ? context.getString(R.string.media_transfer_usb_speaker_name) + ? context.getString(R.string.media_transfer_usb_audio_name) : context.getString(R.string.media_transfer_wired_headphone_name); break; case TYPE_DOCK: diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java index 712ddc8aea4b..5eeb49a0b398 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java @@ -167,6 +167,13 @@ public class TestModeBuilder { return this; } + public TestModeBuilder setVisualEffect(int effect, boolean allowed) { + ZenPolicy newPolicy = new ZenPolicy.Builder(mRule.getZenPolicy()) + .showVisualEffect(effect, allowed).build(); + setZenPolicy(newPolicy); + return this; + } + public TestModeBuilder setEnabled(boolean enabled) { return setEnabled(enabled, /* byUser= */ false); } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java index 8eedb35a3181..0e060dfdd447 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java @@ -47,6 +47,7 @@ import android.provider.Settings; import android.util.Pair; import com.android.internal.R; +import com.android.settingslib.flags.Flags; import com.android.settingslib.widget.AdaptiveIcon; import com.google.common.collect.ImmutableList; @@ -605,6 +606,7 @@ public class BluetoothUtilsTest { @Test public void testHasConnectedBroadcastSource_leadDeviceConnectedToBroadcastSource() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); CachedBluetoothDevice memberCachedDevice = mock(CachedBluetoothDevice.class); BluetoothDevice memberDevice = mock(BluetoothDevice.class); @@ -630,6 +632,7 @@ public class BluetoothUtilsTest { @Test public void testHasConnectedBroadcastSource_memberDeviceConnectedToBroadcastSource() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); CachedBluetoothDevice memberCachedDevice = mock(CachedBluetoothDevice.class); BluetoothDevice memberDevice = mock(BluetoothDevice.class); @@ -655,6 +658,7 @@ public class BluetoothUtilsTest { @Test public void testHasConnectedBroadcastSource_deviceNotConnectedToBroadcastSource() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); List<Long> bisSyncState = new ArrayList<>(); @@ -672,6 +676,7 @@ public class BluetoothUtilsTest { @Test public void testHasConnectedBroadcastSourceForBtDevice_deviceConnectedToBroadcastSource() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); List<Long> bisSyncState = new ArrayList<>(); bisSyncState.add(1L); when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState); @@ -688,6 +693,7 @@ public class BluetoothUtilsTest { @Test public void testHasConnectedBroadcastSourceForBtDevice_deviceNotConnectedToBroadcastSource() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); List<Long> bisSyncState = new ArrayList<>(); when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState); @@ -702,6 +708,106 @@ public class BluetoothUtilsTest { } @Test + public void hasConnectedBroadcastSource_hysteresisFix_leadDeviceHasActiveLocalSource() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + CachedBluetoothDevice memberCachedDevice = mock(CachedBluetoothDevice.class); + BluetoothDevice memberDevice = mock(BluetoothDevice.class); + when(memberCachedDevice.getDevice()).thenReturn(memberDevice); + Set<CachedBluetoothDevice> memberCachedDevices = new HashSet<>(); + memberCachedDevices.add(memberCachedDevice); + when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(memberCachedDevices); + + + when(mBroadcast.getLatestBroadcastId()).thenReturn(TEST_BROADCAST_ID); + when(mLeBroadcastReceiveState.getBroadcastId()).thenReturn(TEST_BROADCAST_ID); + + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList); + when(mAssistant.getAllSources(memberDevice)).thenReturn(Collections.emptyList()); + + assertThat( + BluetoothUtils.hasConnectedBroadcastSource( + mCachedBluetoothDevice, mLocalBluetoothManager)) + .isTrue(); + } + + @Test + public void hasConnectedBroadcastSource_hysteresisFix_memberDeviceHasActiveLocalSource() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + CachedBluetoothDevice memberCachedDevice = mock(CachedBluetoothDevice.class); + BluetoothDevice memberDevice = mock(BluetoothDevice.class); + when(memberCachedDevice.getDevice()).thenReturn(memberDevice); + Set<CachedBluetoothDevice> memberCachedDevices = new HashSet<>(); + memberCachedDevices.add(memberCachedDevice); + when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(memberCachedDevices); + + when(mBroadcast.getLatestBroadcastId()).thenReturn(TEST_BROADCAST_ID); + when(mLeBroadcastReceiveState.getBroadcastId()).thenReturn(TEST_BROADCAST_ID); + + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(memberDevice)).thenReturn(sourceList); + when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(Collections.emptyList()); + + assertThat( + BluetoothUtils.hasConnectedBroadcastSource( + mCachedBluetoothDevice, mLocalBluetoothManager)) + .isTrue(); + } + + @Test + public void hasConnectedBroadcastSource_hysteresisFix_deviceNoActiveLocalSource() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + + when(mBroadcast.getLatestBroadcastId()).thenReturn(UNKNOWN_VALUE_PLACEHOLDER); + + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList); + + assertThat( + BluetoothUtils.hasConnectedBroadcastSource( + mCachedBluetoothDevice, mLocalBluetoothManager)) + .isFalse(); + } + + @Test + public void hasConnectedBroadcastSourceForBtDevice_hysteresisFix_deviceHasActiveLocalSource() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + when(mBroadcast.getLatestBroadcastId()).thenReturn(TEST_BROADCAST_ID); + when(mLeBroadcastReceiveState.getBroadcastId()).thenReturn(TEST_BROADCAST_ID); + + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList); + + assertThat( + BluetoothUtils.hasConnectedBroadcastSourceForBtDevice( + mBluetoothDevice, mLocalBluetoothManager)) + .isTrue(); + } + + @Test + public void hasConnectedBroadcastSourceForBtDevice_hysteresisFix_deviceNoActiveLocalSource() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); + when(mBroadcast.getLatestBroadcastId()).thenReturn(TEST_BROADCAST_ID); + when(mLeBroadcastReceiveState.getBroadcastId()).thenReturn(UNKNOWN_VALUE_PLACEHOLDER); + + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList); + + assertThat( + BluetoothUtils.hasConnectedBroadcastSourceForBtDevice( + mBluetoothDevice, mLocalBluetoothManager)) + .isFalse(); + } + + @Test public void testHasActiveLocalBroadcastSourceForBtDevice_hasActiveLocalSource() { when(mBroadcast.getLatestBroadcastId()).thenReturn(TEST_BROADCAST_ID); when(mLeBroadcastReceiveState.getBroadcastId()).thenReturn(TEST_BROADCAST_ID); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatusTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatusTest.kt new file mode 100644 index 000000000000..3149acf6fc3c --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigServiceStatusTest.kt @@ -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 com.android.settingslib.bluetooth.devicesettings + +import android.os.Bundle +import android.os.Parcel +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DeviceSettingsConfigServiceStatusTest { + + @Test + fun parcelOperation() { + val item = + DeviceSettingsConfigServiceStatus( + success = true, + extras = Bundle().apply { putString("key1", "value1") }, + ) + + val fromParcel = writeAndRead(item) + + assertThat(fromParcel.success).isEqualTo(item.success) + assertThat(fromParcel.extras.getString("key1")).isEqualTo(item.extras.getString("key1")) + } + + private fun writeAndRead( + item: DeviceSettingsConfigServiceStatus + ): DeviceSettingsConfigServiceStatus { + val parcel = Parcel.obtain() + item.writeToParcel(parcel, 0) + parcel.setDataPosition(0) + return DeviceSettingsConfigServiceStatus.CREATOR.createFromParcel(parcel) + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt index 7f1729387a22..ebaad342f9f2 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt @@ -86,6 +86,7 @@ class DeviceSettingsConfigTest { assertThat(fromParcel.moreSettingsHelpItem?.packageName).isEqualTo("package_name_2") assertThat(fromParcel.moreSettingsHelpItem?.className).isEqualTo("class_name_2") assertThat(fromParcel.moreSettingsHelpItem?.intentAction).isEqualTo("intent_action_2") + assertThat(fromParcel.extras.getString("key1")).isEqualTo("value1") } private fun writeAndRead(item: DeviceSettingsConfig): DeviceSettingsConfig { 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 0cb6bc1b1261..4e62fd3b27c5 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 @@ -33,10 +33,12 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId import com.android.settingslib.bluetooth.devicesettings.DeviceSettingItem import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfigServiceStatus import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsProviderServiceStatus import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService +import com.android.settingslib.bluetooth.devicesettings.IGetDeviceSettingsConfigCallback import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreference import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreferenceState import com.android.settingslib.bluetooth.devicesettings.ToggleInfo @@ -53,7 +55,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest -import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test @@ -140,15 +141,10 @@ class DeviceSettingRepositoryTest { ) } - @After - fun clean() { - DeviceSettingServiceConnection.services.clear() - } - @Test fun getDeviceSettingsConfig_withMetadata_success() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) `when`(settingProviderService1.serviceStatus) .thenReturn(DeviceSettingsProviderServiceStatus(true)) `when`(settingProviderService2.serviceStatus) @@ -179,7 +175,7 @@ class DeviceSettingRepositoryTest { ) ) .thenReturn("".toByteArray()) - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) `when`(settingProviderService1.serviceStatus) .thenReturn(DeviceSettingsProviderServiceStatus(true)) `when`(settingProviderService2.serviceStatus) @@ -194,7 +190,7 @@ class DeviceSettingRepositoryTest { @Test fun getDeviceSettingsConfig_providerServiceNotEnabled_returnNull() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) `when`(settingProviderService1.serviceStatus) .thenReturn(DeviceSettingsProviderServiceStatus(false)) `when`(settingProviderService2.serviceStatus) @@ -209,7 +205,7 @@ class DeviceSettingRepositoryTest { @Test fun getDeviceSettingsConfig_bindingServiceFail_returnNull() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) doReturn(false).`when`(context).bindService(any(), anyInt(), any(), any()) val config = underTest.getDeviceSettingsConfig(cachedDevice) @@ -221,7 +217,7 @@ class DeviceSettingRepositoryTest { @Test fun getDeviceSetting_actionSwitchPreference_success() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { input -> input @@ -247,7 +243,7 @@ class DeviceSettingRepositoryTest { @Test fun getDeviceSetting_multiTogglePreference_success() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then { input -> input @@ -273,7 +269,7 @@ class DeviceSettingRepositoryTest { @Test fun getDeviceSetting_helpPreference_success() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then { input -> input @@ -299,6 +295,7 @@ class DeviceSettingRepositoryTest { @Test fun getDeviceSetting_noConfig_returnNull() { testScope.runTest { + setUpConfigService(false, null) `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { input -> input @@ -320,7 +317,7 @@ class DeviceSettingRepositoryTest { @Test fun updateDeviceSettingState_switchState_success() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { input -> input @@ -358,7 +355,7 @@ class DeviceSettingRepositoryTest { @Test fun updateDeviceSettingState_multiToggleState_success() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + setUpConfigService(true, DEVICE_SETTING_CONFIG) `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then { input -> input @@ -459,6 +456,17 @@ class DeviceSettingRepositoryTest { assertThat(actual.settingId).isEqualTo(serviceResponse.settingId) } + private fun setUpConfigService(success: Boolean, config: DeviceSettingsConfig?) { + `when`(configService.getDeviceSettingsConfig(any(), any())).then { input -> + input + .getArgument<IGetDeviceSettingsConfigCallback>(1) + .onResult( + DeviceSettingsConfigServiceStatus(success = success), + config + ) + } + } + private companion object { const val BLUETOOTH_ADDRESS = "12:34:56:78" const val CONFIG_SERVICE_PACKAGE_NAME = "com.android.fake.configservice" diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java index da5f428ce23b..1739c0e5e2bf 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java @@ -136,7 +136,7 @@ public class PhoneMediaDeviceTest { when(mInfo.getType()).thenReturn(TYPE_USB_DEVICE); assertThat(mPhoneMediaDevice.getName()) - .isEqualTo(mContext.getString(R.string.media_transfer_usb_speaker_name)); + .isEqualTo(mContext.getString(R.string.media_transfer_usb_audio_name)); when(mInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppPreferenceTest.java deleted file mode 100644 index 6c8fd50d1896..000000000000 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppPreferenceTest.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS 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.widget; - -import static com.google.common.truth.Truth.assertThat; - -import android.content.Context; -import android.view.View; - -import androidx.preference.PreferenceViewHolder; - -import com.android.settingslib.widget.preference.app.R; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; - -@RunWith(RobolectricTestRunner.class) -public class AppPreferenceTest { - - private Context mContext; - private View mRootView; - private AppPreference mPref; - private PreferenceViewHolder mHolder; - - @Before - public void setUp() { - mContext = RuntimeEnvironment.application; - mRootView = View.inflate(mContext, R.layout.preference_app, null /* parent */); - mHolder = PreferenceViewHolder.createInstanceForTests(mRootView); - mPref = new AppPreference(mContext); - } - - @Test - public void setProgress_showProgress() { - mPref.setProgress(1); - mPref.onBindViewHolder(mHolder); - - assertThat(mHolder.findViewById(android.R.id.progress).getVisibility()) - .isEqualTo(View.VISIBLE); - } - - @Test - public void foobar_testName() { - float iconSize = mContext.getResources().getDimension(com.android.settingslib.widget.theme.R.dimen.secondary_app_icon_size); - assertThat(Float.floatToIntBits(iconSize)).isEqualTo(Float.floatToIntBits(32)); - } -} diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index d7109398b956..5e31da411e49 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -164,6 +164,7 @@ public class SecureSettings { Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM, Settings.Secure.SHOW_NOTIFICATION_SNOOZE, Settings.Secure.NOTIFICATION_HISTORY_ENABLED, Settings.Secure.ZEN_DURATION, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index fa16a44f4592..b3f73749f393 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -243,6 +243,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SHOW_NOTIFICATION_SNOOZE, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.NOTIFICATION_HISTORY_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ZEN_DURATION, ANY_INTEGER_VALIDATOR); diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index f8383d94b1ab..cb1411bdd91e 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -40,16 +40,6 @@ flag { } flag { - name: "notification_minimalism_prototype" - namespace: "systemui" - description: "Prototype of notification minimalism; the new 'Intermediate' lockscreen customization proposal." - bug: "330387368" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "notification_view_flipper_pausing_v2" namespace: "systemui" description: "Pause ViewFlippers inside Notification custom layouts when the shade is closed." @@ -569,6 +559,13 @@ flag { } flag { + name: "volume_redesign" + namespace: "systemui" + description: "Enables Volume BC25 visuals update" + bug: "368308908" +} + +flag { name: "clipboard_shared_transitions" namespace: "systemui" description: "Show shared transitions from clipboard" @@ -1128,6 +1125,16 @@ flag { } flag { + name: "media_controls_umo_inflation_in_background" + namespace: "systemui" + description: "Inflate UMO in background thread" + bug: "368514198" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { namespace: "systemui" name: "enable_view_capture_tracing" description: "Enables view capture tracing in System UI." @@ -1431,4 +1438,4 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java new file mode 100644 index 000000000000..08db95e5a795 --- /dev/null +++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java @@ -0,0 +1,374 @@ +/* + * 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.animation; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.ValueAnimator; +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.SurfaceControl; +import android.window.IRemoteTransition; +import android.window.IRemoteTransitionFinishedCallback; +import android.window.TransitionInfo; +import android.window.TransitionInfo.Change; +import android.window.WindowAnimationState; + +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.wm.shell.shared.TransitionUtil; + +import java.util.ArrayList; +import java.util.List; + +/** + * An implementation of {@link IRemoteTransition} that accepts a {@link UIComponent} as the origin + * and automatically attaches it to the transition leash before the transition starts. + */ +public class OriginRemoteTransition extends IRemoteTransition.Stub { + private static final String TAG = "OriginRemoteTransition"; + + private final Context mContext; + private final boolean mIsEntry; + private final UIComponent mOrigin; + private final TransitionPlayer mPlayer; + private final long mDuration; + private final Handler mHandler; + + @Nullable private SurfaceControl.Transaction mStartTransaction; + @Nullable private IRemoteTransitionFinishedCallback mFinishCallback; + @Nullable private UIComponent.Transaction mOriginTransaction; + @Nullable private ValueAnimator mAnimator; + @Nullable private SurfaceControl mOriginLeash; + private boolean mCancelled; + + OriginRemoteTransition( + Context context, + boolean isEntry, + UIComponent origin, + TransitionPlayer player, + long duration, + Handler handler) { + mContext = context; + mIsEntry = isEntry; + mOrigin = origin; + mPlayer = player; + mDuration = duration; + mHandler = handler; + } + + @Override + public void startAnimation( + IBinder token, + TransitionInfo info, + SurfaceControl.Transaction t, + IRemoteTransitionFinishedCallback finishCallback) { + logD("startAnimation - " + info); + mHandler.post( + () -> { + mStartTransaction = t; + mFinishCallback = finishCallback; + startAnimationInternal(info); + }); + } + + @Override + public void mergeAnimation( + IBinder transition, + TransitionInfo info, + SurfaceControl.Transaction t, + IBinder mergeTarget, + IRemoteTransitionFinishedCallback finishCallback) { + logD("mergeAnimation - " + info); + mHandler.post(this::cancel); + } + + @Override + public void takeOverAnimation( + IBinder transition, + TransitionInfo info, + SurfaceControl.Transaction t, + IRemoteTransitionFinishedCallback finishCallback, + WindowAnimationState[] states) { + logD("takeOverAnimation - " + info); + } + + @Override + public void onTransitionConsumed(IBinder transition, boolean aborted) { + logD("onTransitionConsumed - aborted: " + aborted); + mHandler.post(this::cancel); + } + + private void startAnimationInternal(TransitionInfo info) { + if (!prepareUIs(info)) { + logE("Unable to prepare UI!"); + finishAnimation(/* finished= */ false); + return; + } + // Notify player that we are starting. + mPlayer.onStart(info, mStartTransaction, mOrigin, mOriginTransaction); + + // Start the animator. + mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); + mAnimator.setDuration(mDuration); + mAnimator.addListener( + new AnimatorListener() { + @Override + public void onAnimationStart(Animator a) {} + + @Override + public void onAnimationEnd(Animator a) { + finishAnimation(/* finished= */ !mCancelled); + } + + @Override + public void onAnimationCancel(Animator a) { + mCancelled = true; + } + + @Override + public void onAnimationRepeat(Animator a) {} + }); + mAnimator.addUpdateListener( + a -> { + mPlayer.onProgress((float) a.getAnimatedValue()); + }); + mAnimator.start(); + } + + private boolean prepareUIs(TransitionInfo info) { + if (info.getRootCount() == 0) { + logE("prepareUIs: no root leash!"); + return false; + } + if (info.getRootCount() > 1) { + logE("prepareUIs: multi-display transition is not supported yet!"); + return false; + } + if (info.getChanges().isEmpty()) { + logE("prepareUIs: no changes!"); + return false; + } + + SurfaceControl rootLeash = info.getRoot(0).getLeash(); + int displayId = info.getChanges().get(0).getEndDisplayId(); + Rect displayBounds = getDisplayBounds(displayId); + float windowRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext); + logD("prepareUIs: windowRadius=" + windowRadius + ", displayBounds=" + displayBounds); + + // Create the origin leash and add to the transition root leash. + mOriginLeash = + new SurfaceControl.Builder().setName("OriginTransition-origin-leash").build(); + mStartTransaction + .reparent(mOriginLeash, rootLeash) + .show(mOriginLeash) + .setCornerRadius(mOriginLeash, windowRadius) + .setWindowCrop(mOriginLeash, displayBounds.width(), displayBounds.height()); + + // Process surfaces + List<SurfaceControl> openingSurfaces = new ArrayList<>(); + List<SurfaceControl> closingSurfaces = new ArrayList<>(); + for (Change change : info.getChanges()) { + int mode = change.getMode(); + SurfaceControl leash = change.getLeash(); + // Reparent leash to the transition root. + mStartTransaction.reparent(leash, rootLeash); + if (TransitionUtil.isOpeningMode(mode)) { + openingSurfaces.add(change.getLeash()); + // For opening surfaces, ending bounds are base bound. Apply corner radius if + // it's full screen. + Rect bounds = change.getEndAbsBounds(); + if (displayBounds.equals(bounds)) { + mStartTransaction + .setCornerRadius(leash, windowRadius) + .setWindowCrop(leash, bounds.width(), bounds.height()); + } + } else if (TransitionUtil.isClosingMode(mode)) { + closingSurfaces.add(change.getLeash()); + // For closing surfaces, starting bounds are base bounds. Apply corner radius if + // it's full screen. + Rect bounds = change.getStartAbsBounds(); + if (displayBounds.equals(bounds)) { + mStartTransaction + .setCornerRadius(leash, windowRadius) + .setWindowCrop(leash, bounds.width(), bounds.height()); + } + } + } + + // Set relative order: + // ---- App1 ---- + // ---- origin ---- + // ---- App2 ---- + if (mIsEntry) { + mStartTransaction + .setRelativeLayer(mOriginLeash, closingSurfaces.get(0), 1) + .setRelativeLayer( + openingSurfaces.get(openingSurfaces.size() - 1), mOriginLeash, 1); + } else { + mStartTransaction + .setRelativeLayer(mOriginLeash, openingSurfaces.get(0), 1) + .setRelativeLayer( + closingSurfaces.get(closingSurfaces.size() - 1), mOriginLeash, 1); + } + + // Attach origin UIComponent to origin leash. + mOriginTransaction = mOrigin.newTransaction(); + mOriginTransaction + .attachToTransitionLeash( + mOrigin, mOriginLeash, displayBounds.width(), displayBounds.height()) + .commit(); + + // Apply all surface changes. + mStartTransaction.apply(); + return true; + } + + private Rect getDisplayBounds(int displayId) { + DisplayManager dm = mContext.getSystemService(DisplayManager.class); + DisplayMetrics metrics = new DisplayMetrics(); + dm.getDisplay(displayId).getMetrics(metrics); + return new Rect(0, 0, metrics.widthPixels, metrics.heightPixels); + } + + private void finishAnimation(boolean finished) { + logD("finishAnimation: finished=" + finished); + if (mAnimator == null) { + // The transition didn't start. Ensure we apply the start transaction and report + // finish afterwards. + mStartTransaction + .addTransactionCommittedListener( + mContext.getMainExecutor(), this::finishInternal) + .apply(); + return; + } + mAnimator = null; + // Notify client that we have ended. + mPlayer.onEnd(finished); + // Detach the origin from the transition leash and report finish after it's done. + mOriginTransaction + .detachFromTransitionLeash( + mOrigin, mContext.getMainExecutor(), this::finishInternal) + .commit(); + } + + private void finishInternal() { + logD("finishInternal"); + if (mOriginLeash != null) { + // Release origin leash. + mOriginLeash.release(); + mOriginLeash = null; + } + try { + mFinishCallback.onTransitionFinished(null, null); + } catch (RemoteException e) { + logE("Unable to report transition finish!", e); + } + mStartTransaction = null; + mOriginTransaction = null; + mFinishCallback = null; + } + + private void cancel() { + if (mAnimator != null) { + mAnimator.cancel(); + } + } + + private static void logD(String msg) { + if (OriginTransitionSession.DEBUG) { + Log.d(TAG, msg); + } + } + + private static void logE(String msg) { + Log.e(TAG, msg); + } + + private static void logE(String msg, Throwable e) { + Log.e(TAG, msg, e); + } + + private static UIComponent wrapSurfaces(TransitionInfo info, boolean isOpening) { + List<SurfaceControl> surfaces = new ArrayList<>(); + Rect maxBounds = new Rect(); + for (Change change : info.getChanges()) { + int mode = change.getMode(); + if (TransitionUtil.isOpeningMode(mode) == isOpening) { + surfaces.add(change.getLeash()); + Rect bounds = isOpening ? change.getEndAbsBounds() : change.getStartAbsBounds(); + maxBounds.union(bounds); + } + } + return new SurfaceUIComponent( + surfaces, + /* alpha= */ 1.0f, + /* visible= */ true, + /* bounds= */ maxBounds, + /* baseBounds= */ maxBounds); + } + + /** An interface that represents an origin transitions. */ + public interface TransitionPlayer { + + /** + * Called when an origin transition starts. This method exposes the raw {@link + * TransitionInfo} so that clients can extract more information from it. + */ + default void onStart( + TransitionInfo transitionInfo, + SurfaceControl.Transaction sfTransaction, + UIComponent origin, + UIComponent.Transaction uiTransaction) { + // Wrap transactions. + Transactions transactions = + new Transactions() + .registerTransactionForClass(origin.getClass(), uiTransaction) + .registerTransactionForClass( + SurfaceUIComponent.class, + new SurfaceUIComponent.Transaction(sfTransaction)); + // Wrap surfaces and start. + onStart( + transactions, + origin, + wrapSurfaces(transitionInfo, /* isOpening= */ false), + wrapSurfaces(transitionInfo, /* isOpening= */ true)); + } + + /** + * Called when an origin transition starts. This method exposes the opening and closing + * windows as wrapped {@link UIComponent} to provide simplified interface to clients. + */ + void onStart( + UIComponent.Transaction transaction, + UIComponent origin, + UIComponent closingApp, + UIComponent openingApp); + + /** Called to update the transition frame. */ + void onProgress(float progress); + + /** Called when the transition ended. */ + void onEnd(boolean finished); + } +} diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java index 64bedd347d7a..23693b68a920 100644 --- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java +++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java @@ -24,11 +24,14 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Build; +import android.os.Handler; +import android.os.Looper; import android.os.RemoteException; import android.util.Log; import android.window.IRemoteTransition; import android.window.RemoteTransition; +import com.android.systemui.animation.OriginRemoteTransition.TransitionPlayer; import com.android.systemui.animation.shared.IOriginTransitions; import java.lang.annotation.Retention; @@ -182,6 +185,7 @@ public class OriginTransitionSession { @Nullable private final IOriginTransitions mOriginTransitions; @Nullable private Supplier<IRemoteTransition> mEntryTransitionSupplier; @Nullable private Supplier<IRemoteTransition> mExitTransitionSupplier; + private Handler mHandler = new Handler(Looper.getMainLooper()); private String mName; @Nullable private Predicate<RemoteTransition> mIntentStarter; @@ -259,12 +263,48 @@ public class OriginTransitionSession { return this; } + /** Add an origin entry transition to the builder. */ + public Builder withEntryTransition( + UIComponent entryOrigin, TransitionPlayer entryPlayer, long entryDuration) { + mEntryTransitionSupplier = + () -> + new OriginRemoteTransition( + mContext, + /* isEntry= */ true, + entryOrigin, + entryPlayer, + entryDuration, + mHandler); + return this; + } + /** Add an exit transition to the builder. */ public Builder withExitTransition(IRemoteTransition transition) { mExitTransitionSupplier = () -> transition; return this; } + /** Add an origin exit transition to the builder. */ + public Builder withExitTransition( + UIComponent exitTarget, TransitionPlayer exitPlayer, long exitDuration) { + mExitTransitionSupplier = + () -> + new OriginRemoteTransition( + mContext, + /* isEntry= */ false, + exitTarget, + exitPlayer, + exitDuration, + mHandler); + return this; + } + + /** Supply a handler where transition callbacks will run. */ + public Builder withHandler(Handler handler) { + mHandler = handler; + return this; + } + /** Build an {@link OriginTransitionSession}. */ public OriginTransitionSession build() { if (mIntentStarter == null) { diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/SurfaceUIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/SurfaceUIComponent.java new file mode 100644 index 000000000000..24387360936b --- /dev/null +++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/SurfaceUIComponent.java @@ -0,0 +1,169 @@ +/* + * 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.animation; + +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.view.SurfaceControl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.Executor; + +/** A {@link UIComponent} representing a {@link SurfaceControl}. */ +public class SurfaceUIComponent implements UIComponent { + private final Collection<SurfaceControl> mSurfaces; + private final Rect mBaseBounds; + private final float[] mFloat9 = new float[9]; + + private float mAlpha; + private boolean mVisible; + private Rect mBounds; + + public SurfaceUIComponent( + SurfaceControl sc, float alpha, boolean visible, Rect bounds, Rect baseBounds) { + this(Arrays.asList(sc), alpha, visible, bounds, baseBounds); + } + + public SurfaceUIComponent( + Collection<SurfaceControl> surfaces, + float alpha, + boolean visible, + Rect bounds, + Rect baseBounds) { + mSurfaces = surfaces; + mAlpha = alpha; + mVisible = visible; + mBounds = bounds; + mBaseBounds = baseBounds; + } + + @Override + public float getAlpha() { + return mAlpha; + } + + @Override + public boolean isVisible() { + return mVisible; + } + + @Override + public Rect getBounds() { + return mBounds; + } + + @Override + public Transaction newTransaction() { + return new Transaction(new SurfaceControl.Transaction()); + } + + @Override + public String toString() { + return "SurfaceUIComponent{mSurfaces=" + + mSurfaces + + ", mAlpha=" + + mAlpha + + ", mVisible=" + + mVisible + + ", mBounds=" + + mBounds + + ", mBaseBounds=" + + mBaseBounds + + "}"; + } + + /** A {@link Transaction} wrapping a {@link SurfaceControl.Transaction}. */ + public static class Transaction implements UIComponent.Transaction<SurfaceUIComponent> { + private final SurfaceControl.Transaction mTransaction; + private final ArrayList<Runnable> mChanges = new ArrayList<>(); + + public Transaction(SurfaceControl.Transaction transaction) { + mTransaction = transaction; + } + + @Override + public Transaction setAlpha(SurfaceUIComponent ui, float alpha) { + mChanges.add( + () -> { + ui.mAlpha = alpha; + ui.mSurfaces.forEach(s -> mTransaction.setAlpha(s, alpha)); + }); + return this; + } + + @Override + public Transaction setVisible(SurfaceUIComponent ui, boolean visible) { + mChanges.add( + () -> { + ui.mVisible = visible; + if (visible) { + ui.mSurfaces.forEach(s -> mTransaction.show(s)); + } else { + ui.mSurfaces.forEach(s -> mTransaction.hide(s)); + } + }); + return this; + } + + @Override + public Transaction setBounds(SurfaceUIComponent ui, Rect bounds) { + mChanges.add( + () -> { + if (ui.mBounds.equals(bounds)) { + return; + } + ui.mBounds = bounds; + Matrix matrix = new Matrix(); + matrix.setRectToRect( + new RectF(ui.mBaseBounds), + new RectF(ui.mBounds), + Matrix.ScaleToFit.FILL); + ui.mSurfaces.forEach(s -> mTransaction.setMatrix(s, matrix, ui.mFloat9)); + }); + return this; + } + + @Override + public Transaction attachToTransitionLeash( + SurfaceUIComponent ui, SurfaceControl transitionLeash, int w, int h) { + mChanges.add( + () -> ui.mSurfaces.forEach(s -> mTransaction.reparent(s, transitionLeash))); + return this; + } + + @Override + public Transaction detachFromTransitionLeash( + SurfaceUIComponent ui, Executor executor, Runnable onDone) { + mChanges.add( + () -> { + ui.mSurfaces.forEach(s -> mTransaction.reparent(s, null)); + mTransaction.addTransactionCommittedListener(executor, onDone::run); + }); + return this; + } + + @Override + public void commit() { + mChanges.forEach(Runnable::run); + mChanges.clear(); + mTransaction.apply(); + } + } +} diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/Transactions.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/Transactions.java new file mode 100644 index 000000000000..5240d99a9217 --- /dev/null +++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/Transactions.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.animation; + +import android.annotation.FloatRange; +import android.graphics.Rect; +import android.util.ArrayMap; +import android.view.SurfaceControl; + +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * A composite {@link UIComponent.Transaction} that combines multiple other transactions for each ui + * type. + */ +public class Transactions implements UIComponent.Transaction<UIComponent> { + private final Map<Class, UIComponent.Transaction> mTransactions = new ArrayMap<>(); + + /** Register a transaction object for updating a certain {@link UIComponent} type. */ + public <T extends UIComponent> Transactions registerTransactionForClass( + Class<T> clazz, UIComponent.Transaction transaction) { + mTransactions.put(clazz, transaction); + return this; + } + + private UIComponent.Transaction getTransactionFor(UIComponent ui) { + UIComponent.Transaction transaction = mTransactions.get(ui.getClass()); + if (transaction == null) { + transaction = ui.newTransaction(); + mTransactions.put(ui.getClass(), transaction); + } + return transaction; + } + + @Override + public Transactions setAlpha(UIComponent ui, @FloatRange(from = 0.0, to = 1.0) float alpha) { + getTransactionFor(ui).setAlpha(ui, alpha); + return this; + } + + @Override + public Transactions setVisible(UIComponent ui, boolean visible) { + getTransactionFor(ui).setVisible(ui, visible); + return this; + } + + @Override + public Transactions setBounds(UIComponent ui, Rect bounds) { + getTransactionFor(ui).setBounds(ui, bounds); + return this; + } + + @Override + public Transactions attachToTransitionLeash( + UIComponent ui, SurfaceControl transitionLeash, int w, int h) { + getTransactionFor(ui).attachToTransitionLeash(ui, transitionLeash, w, h); + return this; + } + + @Override + public Transactions detachFromTransitionLeash( + UIComponent ui, Executor executor, Runnable onDone) { + getTransactionFor(ui).detachFromTransitionLeash(ui, executor, onDone); + return this; + } + + @Override + public void commit() { + mTransactions.values().forEach(UIComponent.Transaction::commit); + } +} diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/UIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/UIComponent.java new file mode 100644 index 000000000000..747e4d1eb278 --- /dev/null +++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/UIComponent.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.animation; + +import android.annotation.FloatRange; +import android.graphics.Rect; +import android.view.SurfaceControl; + +import java.util.concurrent.Executor; + +/** An interface representing an UI component on the display. */ +public interface UIComponent { + + /** Get the current alpha of this UI. */ + float getAlpha(); + + /** Check if this UI is visible. */ + boolean isVisible(); + + /** Get the bounds of this UI in its display. */ + Rect getBounds(); + + /** Create a new {@link Transaction} that can update this UI. */ + Transaction newTransaction(); + + /** + * A transaction class for updating {@link UIComponent}. + * + * @param <T> the subtype of {@link UIComponent} that this {@link Transaction} can handle. + */ + interface Transaction<T extends UIComponent> { + /** Update alpha of an UI. Execution will be delayed until {@link #commit()} is called. */ + Transaction setAlpha(T ui, @FloatRange(from = 0.0, to = 1.0) float alpha); + + /** + * Update visibility of an UI. Execution will be delayed until {@link #commit()} is called. + */ + Transaction setVisible(T ui, boolean visible); + + /** Update bounds of an UI. Execution will be delayed until {@link #commit()} is called. */ + Transaction setBounds(T ui, Rect bounds); + + /** + * Attach a ui to the transition leash. Execution will be delayed until {@link #commit()} is + * called. + */ + Transaction attachToTransitionLeash(T ui, SurfaceControl transitionLeash, int w, int h); + + /** + * Detach a ui from the transition leash. Execution will be delayed until {@link #commit} is + * called. + */ + Transaction detachFromTransitionLeash(T ui, Executor executor, Runnable onDone); + + /** Commit any pending changes added to this transaction. */ + void commit(); + } +} diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java new file mode 100644 index 000000000000..313789c4ca7e --- /dev/null +++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java @@ -0,0 +1,278 @@ +/* + * 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.animation; + +import android.annotation.Nullable; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.os.Build; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnDrawListener; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * A {@link UIComponent} wrapping a {@link View}. After being attached to the transition leash, this + * class will draw the content of the {@link View} directly into the leash, and the actual View will + * be changed to INVISIBLE in its view tree. This allows the {@link View} to transform in the + * full-screen size leash without being constrained by the view tree's boundary or inheriting its + * parent's alpha and transformation. + */ +public class ViewUIComponent implements UIComponent { + private static final String TAG = "ViewUIComponent"; + private static final boolean DEBUG = Build.IS_USERDEBUG || Log.isLoggable(TAG, Log.DEBUG); + private final OnDrawListener mOnDrawListener = this::postDraw; + private final View mView; + + @Nullable private SurfaceControl mSurfaceControl; + @Nullable private Surface mSurface; + @Nullable private Rect mViewBoundsOverride; + private boolean mVisibleOverride; + private boolean mDirty; + + public ViewUIComponent(View view) { + mView = view; + } + + @Override + public float getAlpha() { + return mView.getAlpha(); + } + + @Override + public boolean isVisible() { + return isAttachedToLeash() ? mVisibleOverride : mView.getVisibility() == View.VISIBLE; + } + + @Override + public Rect getBounds() { + if (isAttachedToLeash() && mViewBoundsOverride != null) { + return mViewBoundsOverride; + } + return getRealBounds(); + } + + @Override + public Transaction newTransaction() { + return new Transaction(); + } + + private void attachToTransitionLeash(SurfaceControl transitionLeash, int w, int h) { + logD("attachToTransitionLeash"); + // Remember current visibility. + mVisibleOverride = mView.getVisibility() == View.VISIBLE; + + // Create the surface + mSurfaceControl = + new SurfaceControl.Builder().setName("ViewUIComponent").setBufferSize(w, h).build(); + mSurface = new Surface(mSurfaceControl); + forceDraw(); + + // Attach surface to transition leash + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.reparent(mSurfaceControl, transitionLeash).show(mSurfaceControl); + + // Make sure view draw triggers surface draw. + mView.getViewTreeObserver().addOnDrawListener(mOnDrawListener); + + // Make the view invisible AFTER the surface is shown. + t.addTransactionCommittedListener( + mView.getContext().getMainExecutor(), + () -> mView.setVisibility(View.INVISIBLE)) + .apply(); + } + + private void detachFromTransitionLeash(Executor executor, Runnable onDone) { + logD("detachFromTransitionLeash"); + Surface s = mSurface; + SurfaceControl sc = mSurfaceControl; + mSurface = null; + mSurfaceControl = null; + mView.getViewTreeObserver().removeOnDrawListener(mOnDrawListener); + // Restore view visibility + mView.setVisibility(mVisibleOverride ? View.VISIBLE : View.INVISIBLE); + mView.invalidate(); + // Clean up surfaces. + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.reparent(sc, null) + .addTransactionCommittedListener( + mView.getContext().getMainExecutor(), + () -> { + s.release(); + sc.release(); + executor.execute(onDone); + }); + // Apply transaction AFTER the view is drawn. + mView.getRootSurfaceControl().applyTransactionOnDraw(t); + } + + @Override + public String toString() { + return "ViewUIComponent{" + + "alpha=" + + getAlpha() + + ", visible=" + + isVisible() + + ", bounds=" + + getBounds() + + ", attached=" + + isAttachedToLeash() + + "}"; + } + + private void draw() { + if (!mDirty) { + // No need to draw. This is probably a duplicate call. + logD("draw: skipped - clean"); + return; + } + mDirty = false; + if (!isAttachedToLeash()) { + // Not attached. + logD("draw: skipped - not attached"); + return; + } + ViewGroup.LayoutParams params = mView.getLayoutParams(); + if (params == null || params.width == 0 || params.height == 0) { + // layout pass didn't happen. + logD("draw: skipped - no layout"); + return; + } + Canvas canvas = mSurface.lockHardwareCanvas(); + // Clear the canvas first. + canvas.drawColor(0, PorterDuff.Mode.CLEAR); + if (mVisibleOverride) { + Rect realBounds = getRealBounds(); + Rect renderBounds = getBounds(); + canvas.translate(renderBounds.left, renderBounds.top); + canvas.scale( + (float) renderBounds.width() / realBounds.width(), + (float) renderBounds.height() / realBounds.height()); + canvas.saveLayerAlpha(null, (int) (255 * mView.getAlpha())); + mView.draw(canvas); + canvas.restore(); + } + mSurface.unlockCanvasAndPost(canvas); + logD("draw: done"); + } + + private void forceDraw() { + mDirty = true; + draw(); + } + + private Rect getRealBounds() { + Rect output = new Rect(); + mView.getBoundsOnScreen(output); + return output; + } + + private boolean isAttachedToLeash() { + return mSurfaceControl != null && mSurface != null; + } + + private void logD(String msg) { + if (DEBUG) { + Log.d(TAG, msg); + } + } + + private void setVisible(boolean visible) { + logD("setVisibility: " + visible); + if (isAttachedToLeash()) { + mVisibleOverride = visible; + postDraw(); + } else { + mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); + } + } + + private void setBounds(Rect bounds) { + logD("setBounds: " + bounds); + mViewBoundsOverride = bounds; + if (isAttachedToLeash()) { + postDraw(); + } else { + Log.w(TAG, "setBounds: not attached to leash!"); + } + } + + private void setAlpha(float alpha) { + logD("setAlpha: " + alpha); + mView.setAlpha(alpha); + if (isAttachedToLeash()) { + postDraw(); + } + } + + private void postDraw() { + if (mDirty) { + return; + } + mDirty = true; + mView.post(this::draw); + } + + public static class Transaction implements UIComponent.Transaction<ViewUIComponent> { + private final List<Runnable> mChanges = new ArrayList<>(); + + @Override + public Transaction setAlpha(ViewUIComponent ui, float alpha) { + mChanges.add(() -> ui.setAlpha(alpha)); + return this; + } + + @Override + public Transaction setVisible(ViewUIComponent ui, boolean visible) { + mChanges.add(() -> ui.setVisible(visible)); + return this; + } + + @Override + public Transaction setBounds(ViewUIComponent ui, Rect bounds) { + mChanges.add(() -> ui.setBounds(bounds)); + return this; + } + + @Override + public Transaction attachToTransitionLeash( + ViewUIComponent ui, SurfaceControl transitionLeash, int w, int h) { + mChanges.add(() -> ui.attachToTransitionLeash(transitionLeash, w, h)); + return this; + } + + @Override + public Transaction detachFromTransitionLeash( + ViewUIComponent ui, Executor executor, Runnable onDone) { + mChanges.add(() -> ui.detachFromTransitionLeash(executor, onDone)); + return this; + } + + @Override + public void commit() { + mChanges.forEach(Runnable::run); + mChanges.clear(); + } + } +} diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt index a5f8057b524f..20efea513b3a 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt @@ -28,11 +28,11 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonColors import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import com.android.compose.theme.LocalAndroidColorScheme @Composable fun PlatformButton( @@ -100,12 +100,7 @@ fun PlatformIconButton( @DrawableRes iconResource: Int, contentDescription: String?, ) { - IconButton( - modifier = modifier, - onClick = onClick, - enabled = enabled, - colors = colors, - ) { + IconButton(modifier = modifier, onClick = onClick, enabled = enabled, colors = colors) { Icon( painter = painterResource(id = iconResource), contentDescription = contentDescription, @@ -118,7 +113,7 @@ private val ButtonPaddings = PaddingValues(horizontal = 16.dp, vertical = 8.dp) @Composable private fun filledButtonColors(): ButtonColors { - val colors = LocalAndroidColorScheme.current + val colors = MaterialTheme.colorScheme return ButtonDefaults.buttonColors( containerColor = colors.primary, contentColor = colors.onPrimary, @@ -127,27 +122,22 @@ private fun filledButtonColors(): ButtonColors { @Composable private fun outlineButtonColors(): ButtonColors { - return ButtonDefaults.outlinedButtonColors( - contentColor = LocalAndroidColorScheme.current.onSurface, - ) + return ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.onSurface) } @Composable private fun iconButtonColors(): IconButtonColors { return IconButtonDefaults.filledIconButtonColors( - contentColor = LocalAndroidColorScheme.current.onSurface, + contentColor = MaterialTheme.colorScheme.onSurface ) } @Composable private fun outlineButtonBorder(): BorderStroke { - return BorderStroke( - width = 1.dp, - color = LocalAndroidColorScheme.current.primary, - ) + return BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.primary) } @Composable private fun textButtonColors(): ButtonColors { - return ButtonDefaults.textButtonColors(contentColor = LocalAndroidColorScheme.current.primary) + return ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.primary) } diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt index bfeaf928dfe8..0f6e6a7c4383 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt @@ -28,6 +28,7 @@ import com.android.systemui.keyguard.ui.composable.LockscreenSceneBlueprintModul import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel import com.android.systemui.scene.ui.composable.Scene +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel import dagger.Binds import dagger.Module import dagger.Provides @@ -35,12 +36,7 @@ import dagger.multibindings.IntoSet import javax.inject.Provider import kotlinx.coroutines.ExperimentalCoroutinesApi -@Module( - includes = - [ - LockscreenSceneBlueprintModule::class, - ], -) +@Module(includes = [LockscreenSceneBlueprintModule::class]) interface LockscreenSceneModule { @Binds @IntoSet fun lockscreenScene(scene: LockscreenScene): Scene @@ -51,9 +47,7 @@ interface LockscreenSceneModule { @Provides @SysUISingleton @KeyguardRootView - fun viewProvider( - configurator: Provider<KeyguardViewConfigurator>, - ): () -> View { + fun viewProvider(configurator: Provider<KeyguardViewConfigurator>): () -> View { return { configurator.get().getKeyguardRootView() } } @@ -67,10 +61,16 @@ interface LockscreenSceneModule { @Provides fun providesLockscreenContent( viewModelFactory: LockscreenContentViewModel.Factory, + notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory, blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint>, clockInteractor: KeyguardClockInteractor, ): LockscreenContent { - return LockscreenContent(viewModelFactory, blueprints, clockInteractor) + return LockscreenContent( + viewModelFactory, + notificationScrimViewModelFactory, + blueprints, + clockInteractor, + ) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index f4d1242098f9..bcd333710497 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -274,7 +274,7 @@ fun CommunalHub( if (layoutDirection == LayoutDirection.Rtl) screenWidth - offset.x else offset.x, - offset.y + offset.y, ) - contentOffset val index = firstIndexAtOffset(gridState, adjustedOffset) val key = @@ -310,6 +310,9 @@ fun CommunalHub( it.changedToUp() || it.changedToUpIgnoreConsumed() } ) + + // Reset state once touch ends. + viewModel.onResetTouchState() } } } @@ -330,7 +333,7 @@ fun CommunalHub( if (layoutDirection == LayoutDirection.Rtl) screenWidth - offset.x else offset.x, - offset.y + offset.y, ) - it.positionInWindow() - contentOffset } val index = adjustedOffset?.let { firstIndexAtOffset(gridState, it) } @@ -344,14 +347,11 @@ fun CommunalHub( } } } - }, + } ) { AccessibilityContainer(viewModel) { if (!viewModel.isEditMode && isEmptyState) { - EmptyStateCta( - contentPadding = contentPadding, - viewModel = viewModel, - ) + EmptyStateCta(contentPadding = contentPadding, viewModel = viewModel) } else { val slideOffsetInPx = with(LocalDensity.current) { Dimensions.SlideOffsetY.toPx().toInt() } @@ -364,7 +364,7 @@ fun CommunalHub( ) + slideInVertically( animationSpec = tween(durationMillis = 1000, easing = Emphasized), - initialOffsetY = { -slideOffsetInPx } + initialOffsetY = { -slideOffsetInPx }, ), exit = fadeOut( @@ -372,7 +372,7 @@ fun CommunalHub( ) + slideOutVertically( animationSpec = tween(durationMillis = 1000, easing = Emphasized), - targetOffsetY = { -slideOffsetInPx } + targetOffsetY = { -slideOffsetInPx }, ), modifier = Modifier.fillMaxSize(), ) { @@ -389,7 +389,7 @@ fun CommunalHub( removeEnabled = removeButtonEnabled, offset = gridCoordinates?.let { it.positionInWindow() + offset }, - containerToCheck = removeButtonCoordinates + containerToCheck = removeButtonCoordinates, ) }, gridState = gridState, @@ -410,7 +410,7 @@ fun CommunalHub( enter = fadeIn(animationSpec = tween(durationMillis = 250, easing = LinearEasing)) + slideInVertically( - animationSpec = tween(durationMillis = 1000, easing = Emphasized), + animationSpec = tween(durationMillis = 1000, easing = Emphasized) ), exit = fadeOut(animationSpec = tween(durationMillis = 167, easing = LinearEasing)) + @@ -434,7 +434,7 @@ fun CommunalHub( viewModel.setSelectedKey(null) } }, - removeEnabled = removeButtonEnabled + removeEnabled = removeButtonEnabled, ) } } @@ -451,7 +451,7 @@ fun CommunalHub( title = stringResource(id = R.string.dialog_title_to_allow_any_widget), positiveButtonText = stringResource(id = R.string.button_text_to_open_settings), onConfirm = viewModel::onEnableWidgetDialogConfirm, - onCancel = viewModel::onEnableWidgetDialogCancel + onCancel = viewModel::onEnableWidgetDialogCancel, ) EnableWidgetDialog( @@ -460,7 +460,7 @@ fun CommunalHub( title = stringResource(id = R.string.work_mode_off_title), positiveButtonText = stringResource(id = R.string.work_mode_turn_on), onConfirm = viewModel::onEnableWorkProfileDialogConfirm, - onCancel = viewModel::onEnableWorkProfileDialogCancel + onCancel = viewModel::onEnableWorkProfileDialogCancel, ) } @@ -509,7 +509,7 @@ private fun DisclaimerBottomSheetContent(onButtonClicked: () -> Unit) { imageVector = Icons.Outlined.Widgets, contentDescription = null, tint = colors.primary, - modifier = Modifier.size(32.dp) + modifier = Modifier.size(32.dp), ) Spacer(modifier = Modifier.height(16.dp)) Text( @@ -527,7 +527,7 @@ private fun DisclaimerBottomSheetContent(onButtonClicked: () -> Unit) { Modifier.padding(horizontal = 26.dp, vertical = 16.dp) .widthIn(min = 200.dp) .heightIn(min = 56.dp), - onClick = { onButtonClicked() } + onClick = { onButtonClicked() }, ) { Text( stringResource(R.string.communal_widgets_disclaimer_button), @@ -540,7 +540,7 @@ private fun DisclaimerBottomSheetContent(onButtonClicked: () -> Unit) { @Composable private fun ObserveScrollEffect( gridState: LazyGridState, - communalViewModel: BaseCommunalViewModel + communalViewModel: BaseCommunalViewModel, ) { LaunchedEffect(gridState) { @@ -667,7 +667,7 @@ private fun BoxScope.CommunalHubLazyGrid( rememberGridDragDropState( gridState = gridState, contentListState = contentListState, - updateDragPositionForRemove = updateDragPositionForRemove + updateDragPositionForRemove = updateDragPositionForRemove, ) gridModifier = gridModifier @@ -677,7 +677,7 @@ private fun BoxScope.CommunalHubLazyGrid( LocalLayoutDirection.current, screenWidth, contentOffset, - viewModel + viewModel, ) // for widgets dropped from other activities val dragAndDropTargetState = @@ -709,11 +709,7 @@ private fun BoxScope.CommunalHubLazyGrid( contentType = { _, item -> item.key }, span = { _, item -> GridItemSpan(item.size.span) }, ) { index, item -> - val size = - SizeF( - Dimensions.CardWidth.value, - item.size.dp().value, - ) + val size = SizeF(Dimensions.CardWidth.value, item.size.dp().value) val cardModifier = Modifier.requiredSize(width = size.width.dp, height = size.height.dp) if (viewModel.isEditMode && dragDropState != null) { val selected = item.key == selectedKey.value @@ -765,16 +761,13 @@ private fun BoxScope.CommunalHubLazyGrid( * The empty state displays a fullscreen call-to-action (CTA) tile when no widgets are available. */ @Composable -private fun EmptyStateCta( - contentPadding: PaddingValues, - viewModel: BaseCommunalViewModel, -) { +private fun EmptyStateCta(contentPadding: PaddingValues, viewModel: BaseCommunalViewModel) { val colors = LocalAndroidColorScheme.current Card( modifier = Modifier.height(hubDimensions.GridHeight).padding(contentPadding), colors = CardDefaults.cardColors(containerColor = Color.Transparent), border = BorderStroke(3.adjustedDp, colors.secondary), - shape = RoundedCornerShape(size = 80.adjustedDp) + shape = RoundedCornerShape(size = 80.adjustedDp), ) { Column( modifier = Modifier.fillMaxSize().padding(horizontal = 110.adjustedDp), @@ -788,10 +781,7 @@ private fun EmptyStateCta( textAlign = TextAlign.Center, color = colors.secondary, ) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { Button( modifier = Modifier.height(56.dp), colors = @@ -799,17 +789,13 @@ private fun EmptyStateCta( containerColor = colors.primary, contentColor = colors.onPrimary, ), - onClick = { - viewModel.onOpenWidgetEditor( - shouldOpenWidgetPickerOnStart = true, - ) - }, + onClick = { viewModel.onOpenWidgetEditor(shouldOpenWidgetPickerOnStart = true) }, ) { Icon( imageVector = Icons.Default.Add, contentDescription = stringResource(R.string.label_for_button_in_empty_state_cta), - modifier = Modifier.size(24.dp) + modifier = Modifier.size(24.dp), ) Spacer(Modifier.width(ButtonDefaults.IconSpacing)) Text( @@ -835,7 +821,7 @@ private fun Toolbar( setToolbarSize: (toolbarSize: IntSize) -> Unit, setRemoveButtonCoordinates: (coordinates: LayoutCoordinates?) -> Unit, onOpenWidgetPicker: () -> Unit, - onEditDone: () -> Unit + onEditDone: () -> Unit, ) { if (!removeEnabled) { // Clear any existing coordinates when remove is not enabled. @@ -844,7 +830,7 @@ private fun Toolbar( val removeButtonAlpha: Float by animateFloatAsState( targetValue = if (removeEnabled) 1f else 0.5f, - label = "RemoveButtonAlphaAnimation" + label = "RemoveButtonAlphaAnimation", ) Box( @@ -855,7 +841,7 @@ private fun Toolbar( start = Dimensions.ToolbarPaddingHorizontal, end = Dimensions.ToolbarPaddingHorizontal, ) - .onSizeChanged { setToolbarSize(it) }, + .onSizeChanged { setToolbarSize(it) } ) { val addWidgetText = stringResource(R.string.hub_mode_add_widget_button_text) ToolbarButton( @@ -864,16 +850,14 @@ private fun Toolbar( onClick = onOpenWidgetPicker, ) { Icon(Icons.Default.Add, null) - Text( - text = addWidgetText, - ) + Text(text = addWidgetText) } AnimatedVisibility( modifier = Modifier.align(Alignment.Center), visible = removeEnabled, enter = fadeIn(), - exit = fadeOut() + exit = fadeOut(), ) { Button( onClick = onRemoveClicked, @@ -887,20 +871,18 @@ private fun Toolbar( if (removeEnabled) { setRemoveButtonCoordinates(it) } - } + }, ) { Row( horizontalArrangement = Arrangement.spacedBy( ButtonDefaults.IconSpacing, - Alignment.CenterHorizontally + Alignment.CenterHorizontally, ), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Icon(Icons.Default.Close, contentDescription = null) - Text( - text = stringResource(R.string.button_to_remove_widget), - ) + Text(text = stringResource(R.string.button_to_remove_widget)) } } } @@ -911,9 +893,7 @@ private fun Toolbar( onClick = onEditDone, ) { Icon(Icons.Default.Check, contentDescription = null) - Text( - text = stringResource(R.string.hub_mode_editing_exit_button_text), - ) + Text(text = stringResource(R.string.hub_mode_editing_exit_button_text)) } } } @@ -926,14 +906,14 @@ private fun ToolbarButton( isPrimary: Boolean = true, onClick: () -> Unit, modifier: Modifier = Modifier, - content: @Composable RowScope.() -> Unit + content: @Composable RowScope.() -> Unit, ) { val colors = LocalAndroidColorScheme.current AnimatedVisibility( visible = isPrimary, modifier = modifier, enter = fadeIn(), - exit = fadeOut() + exit = fadeOut(), ) { Button( onClick = onClick, @@ -943,7 +923,7 @@ private fun ToolbarButton( Row( horizontalArrangement = Arrangement.spacedBy(ButtonDefaults.IconSpacing, Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { content() } @@ -954,21 +934,18 @@ private fun ToolbarButton( visible = !isPrimary, modifier = modifier, enter = fadeIn(), - exit = fadeOut() + exit = fadeOut(), ) { OutlinedButton( onClick = onClick, - colors = - ButtonDefaults.outlinedButtonColors( - contentColor = colors.onPrimaryContainer, - ), + colors = ButtonDefaults.outlinedButtonColors(contentColor = colors.onPrimaryContainer), border = BorderStroke(width = 2.0.dp, color = colors.primary), contentPadding = Dimensions.ButtonPadding, ) { Row( horizontalArrangement = Arrangement.spacedBy(ButtonDefaults.IconSpacing, Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { content() } @@ -1041,7 +1018,7 @@ fun HighlightedItem(modifier: Modifier = Modifier, alpha: Float = 1.0f) { size = Size(width = size.width + padding * 2, height = size.height + padding * 2), cornerRadius = CornerRadius(37.adjustedDp.toPx()), - style = Stroke(width = 3.adjustedDp.toPx()) + style = Stroke(width = 3.adjustedDp.toPx()), ) } ) @@ -1061,7 +1038,7 @@ private fun CtaTileInViewModeContent( containerColor = colors.primary, contentColor = colors.onPrimary, ), - shape = RoundedCornerShape(68.adjustedDp, 34.adjustedDp, 68.adjustedDp, 34.adjustedDp) + shape = RoundedCornerShape(68.adjustedDp, 34.adjustedDp, 68.adjustedDp, 34.adjustedDp), ) { Column( modifier = @@ -1081,7 +1058,7 @@ private fun CtaTileInViewModeContent( style = MaterialTheme.typography.titleLarge, fontSize = nonScalableTextSize(22.dp), lineHeight = nonScalableTextSize(28.dp), - modifier = Modifier.verticalScroll(rememberScrollState()).weight(1F) + modifier = Modifier.verticalScroll(rememberScrollState()).weight(1F), ) Spacer(modifier = Modifier.size(16.adjustedDp)) Row( @@ -1093,15 +1070,12 @@ private fun CtaTileInViewModeContent( LocalDensity provides Density( LocalDensity.current.density, - LocalDensity.current.fontScale.coerceIn(0f, 1.25f) + LocalDensity.current.fontScale.coerceIn(0f, 1.25f), ) ) { OutlinedButton( modifier = Modifier.fillMaxHeight().weight(1F), - colors = - ButtonDefaults.buttonColors( - contentColor = colors.onPrimary, - ), + colors = ButtonDefaults.buttonColors(contentColor = colors.onPrimary), border = BorderStroke(width = 1.0.dp, color = colors.primaryContainer), onClick = viewModel::onDismissCtaTile, contentPadding = PaddingValues(0.dp, 0.dp, 0.dp, 0.dp), @@ -1259,7 +1233,7 @@ private fun WidgetContent( visible = selected, model = model, widgetConfigurator = widgetConfigurator, - modifier = Modifier.align(Alignment.BottomEnd) + modifier = Modifier.align(Alignment.BottomEnd), ) } } @@ -1289,14 +1263,14 @@ fun WidgetConfigureButton( containerColor = colors.primary, contentColor = colors.onPrimary, disabledContainerColor = Color.Transparent, - disabledContentColor = Color.Transparent + disabledContentColor = Color.Transparent, ), onClick = { scope.launch { widgetConfigurator.configureWidget(model.appWidgetId) } }, ) { Icon( imageVector = Icons.Outlined.Edit, contentDescription = stringResource(id = R.string.edit_widget), - modifier = Modifier.padding(12.adjustedDp) + modifier = Modifier.padding(12.adjustedDp), ) } } @@ -1323,13 +1297,13 @@ fun DisabledWidgetPlaceholder( .background( color = MaterialTheme.colorScheme.surfaceVariant, shape = - RoundedCornerShape(dimensionResource(system_app_widget_background_radius)) + RoundedCornerShape(dimensionResource(system_app_widget_background_radius)), ) .clickable( enabled = !viewModel.isEditMode, interactionSource = null, indication = null, - onClick = viewModel::onOpenEnableWidgetDialog + onClick = viewModel::onOpenEnableWidgetDialog, ), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, @@ -1360,7 +1334,7 @@ fun PendingWidgetPlaceholder( modifier = modifier.background( color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(dimensionResource(system_app_widget_background_radius)) + shape = RoundedCornerShape(dimensionResource(system_app_widget_background_radius)), ), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, @@ -1418,7 +1392,7 @@ private fun Umo(viewModel: BaseCommunalViewModel, modifier: Modifier = Modifier) MotionEvent.ACTION_MOVE, change.position.x, change.position.y, - 0 + 0, ) viewModel.mediaHost.hostView.dispatchTouchEvent(event) event.recycle() @@ -1429,12 +1403,12 @@ private fun Umo(viewModel: BaseCommunalViewModel, modifier: Modifier = Modifier) layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT + FrameLayout.LayoutParams.MATCH_PARENT, ) } viewModel.mediaHost.hostView }, - onReset = {} + onReset = {}, ) } @@ -1462,7 +1436,7 @@ fun AccessibilityContainer(viewModel: BaseCommunalViewModel, content: @Composabl ) { viewModel.changeScene( CommunalScenes.Blank, - "closed by accessibility" + "closed by accessibility", ) true }, @@ -1471,7 +1445,7 @@ fun AccessibilityContainer(viewModel: BaseCommunalViewModel, content: @Composabl ) { viewModel.onOpenWidgetEditor() true - } + }, ) } } @@ -1514,7 +1488,7 @@ private fun gridContentPadding(isEditMode: Boolean, toolbarSize: IntSize?): Padd start = Dimensions.ToolbarPaddingHorizontal, end = Dimensions.ToolbarPaddingHorizontal, top = verticalPadding + toolbarHeight, - bottom = verticalPadding + bottom = verticalPadding, ) } @@ -1523,7 +1497,7 @@ private fun beforeContentPadding(paddingValues: PaddingValues): ContentPaddingIn return with(LocalDensity.current) { ContentPaddingInPx( start = paddingValues.calculateStartPadding(LocalLayoutDirection.current).toPx(), - top = paddingValues.calculateTopPadding().toPx() + top = paddingValues.calculateTopPadding().toPx(), ) } } @@ -1536,7 +1510,7 @@ private fun beforeContentPadding(paddingValues: PaddingValues): ContentPaddingIn fun isPointerWithinEnabledRemoveButton( removeEnabled: Boolean, offset: Offset?, - containerToCheck: LayoutCoordinates? + containerToCheck: LayoutCoordinates?, ): Boolean { if (!removeEnabled || offset == null || containerToCheck == null) { return false diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt index 0b9669410b8e..69ca0a5f476c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/dialog/ui/composable/AlertDialogContent.kt @@ -38,7 +38,6 @@ import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.layoutId import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.android.compose.theme.LocalAndroidColorScheme import kotlin.math.roundToInt /** @@ -69,7 +68,7 @@ fun AlertDialogContent( Modifier.defaultMinSize(minWidth = defaultSize, minHeight = defaultSize), propagateMinConstraints = true, ) { - val iconColor = LocalAndroidColorScheme.current.primary + val iconColor = MaterialTheme.colorScheme.primary CompositionLocalProvider(LocalContentColor provides iconColor) { icon() } } @@ -77,7 +76,7 @@ fun AlertDialogContent( } // Title. - val titleColor = LocalAndroidColorScheme.current.onSurface + val titleColor = MaterialTheme.colorScheme.onSurface CompositionLocalProvider(LocalContentColor provides titleColor) { ProvideTextStyle( MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center) @@ -88,7 +87,7 @@ fun AlertDialogContent( Spacer(Modifier.height(16.dp)) // Content. - val contentColor = LocalAndroidColorScheme.current.onSurfaceVariant + val contentColor = MaterialTheme.colorScheme.onSurfaceVariant Box { CompositionLocalProvider(LocalContentColor provides contentColor) { ProvideTextStyle( @@ -169,7 +168,7 @@ private fun AlertDialogButtons( negative.width - positive.width - horizontalSpacing.roundToInt(), - 0 + 0, ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt index dbe75382556f..5c5514aec03e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt @@ -30,6 +30,8 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.notifications.ui.composable.NotificationLockscreenScrim +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel /** * Renders the content of the lockscreen. @@ -39,6 +41,7 @@ import com.android.systemui.lifecycle.rememberViewModel */ class LockscreenContent( private val viewModelFactory: LockscreenContentViewModel.Factory, + private val notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory, private val blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint>, private val clockInteractor: KeyguardClockInteractor, ) { @@ -47,10 +50,13 @@ class LockscreenContent( } @Composable - fun SceneScope.Content( - modifier: Modifier = Modifier, - ) { - val viewModel = rememberViewModel("LockscreenContent") { viewModelFactory.create() } + fun SceneScope.Content(modifier: Modifier = Modifier) { + val viewModel = + rememberViewModel("LockscreenContent-viewModel") { viewModelFactory.create() } + val notificationLockscreenScrimViewModel = + rememberViewModel("LockscreenContent-scrimViewModel") { + notificationScrimViewModelFactory.create() + } val isContentVisible: Boolean by viewModel.isContentVisible.collectAsStateWithLifecycle() if (!isContentVisible) { // If the content isn't supposed to be visible, show a large empty box as it's needed @@ -71,6 +77,9 @@ class LockscreenContent( } val blueprint = blueprintByBlueprintId[blueprintId] ?: return - with(blueprint) { Content(viewModel, modifier.sysuiResTag("keyguard_root_view")) } + with(blueprint) { + Content(viewModel, modifier.sysuiResTag("keyguard_root_view")) + NotificationLockscreenScrim(notificationLockscreenScrimViewModel) + } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationLockscreenScrim.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationLockscreenScrim.kt new file mode 100644 index 000000000000..4279be3efad0 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationLockscreenScrim.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.systemui.notifications.ui.composable + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.animation.scene.SceneScope +import com.android.compose.animation.scene.content.state.TransitionState +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel +import kotlinx.coroutines.launch + +/** + * A full-screen notifications scrim that is only visible after transitioning from Shade scene to + * Lockscreen Scene and ending user input, at which point it fades out, visually completing the + * transition. + */ +@Composable +fun SceneScope.NotificationLockscreenScrim( + viewModel: NotificationLockscreenScrimViewModel, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + val shadeMode = viewModel.shadeMode.collectAsStateWithLifecycle() + + // Important: Make sure that shouldShowScrimFadeOut() is checked the first time the Lockscreen + // scene is composed. + val useFadeOutOnComposition = + remember(shadeMode.value) { + layoutState.currentTransition?.let { currentTransition -> + shouldShowScrimFadeOut(currentTransition, shadeMode.value) + } ?: false + } + + val alphaAnimatable = remember { Animatable(1f) } + + LaunchedEffect( + alphaAnimatable, + layoutState.currentTransition, + useFadeOutOnComposition, + shadeMode, + ) { + val currentTransition = layoutState.currentTransition + if ( + useFadeOutOnComposition && + currentTransition != null && + shouldShowScrimFadeOut(currentTransition, shadeMode.value) && + currentTransition.isUserInputOngoing + ) { + // keep scrim visible until user lifts their finger. + viewModel.setAlphaForLockscreenFadeIn(0f) + alphaAnimatable.snapTo(1f) + } else if ( + useFadeOutOnComposition && + (currentTransition == null || + (shouldShowScrimFadeOut(currentTransition, shadeMode.value) && + !currentTransition.isUserInputOngoing)) + ) { + // we no longer want to keep the scrim from fading out, so animate the scrim fade-out + // and pipe the progress to the view model as well, so NSSL can fade-in the stack in + // tandem. + viewModel.setAlphaForLockscreenFadeIn(0f) + coroutineScope.launch { + snapshotFlow { alphaAnimatable.value } + .collect { viewModel.setAlphaForLockscreenFadeIn(1 - it) } + } + alphaAnimatable.animateTo(0f, tween()) + } else { + // disable the scrim fade logic. + viewModel.setAlphaForLockscreenFadeIn(1f) + alphaAnimatable.snapTo(0f) + } + } + + Box( + modifier + .fillMaxSize() + .element(Notifications.Elements.NotificationScrim) + .graphicsLayer { alpha = alphaAnimatable.value } + .background(MaterialTheme.colorScheme.surface) + ) +} + +private fun shouldShowScrimFadeOut( + currentTransition: TransitionState.Transition, + shadeMode: ShadeMode, +): Boolean { + return shadeMode == ShadeMode.Single && + currentTransition.isInitiatedByUserInput && + (currentTransition.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen) || + currentTransition.isTransitioning(from = Scenes.Bouncer, to = Scenes.Lockscreen)) +} 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 fe4a65b8bbd0..2066c9314bc7 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 @@ -86,6 +86,8 @@ import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.animation.scene.NestedScrollBehavior import com.android.compose.animation.scene.SceneScope +import com.android.compose.animation.scene.SceneTransitionLayoutState +import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.modifiers.thenIf import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius import com.android.systemui.res.R @@ -101,7 +103,7 @@ import com.android.systemui.statusbar.notification.stack.ui.viewmodel.Notificati import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import kotlin.math.roundToInt -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch object Notifications { @@ -251,6 +253,7 @@ fun SceneScope.ConstrainedNotificationStack( NotificationPlaceholder( stackScrollView = stackScrollView, viewModel = viewModel, + useStackBounds = { shouldUseLockscreenStackBounds(layoutState.transitionState) }, modifier = Modifier.fillMaxSize(), ) HeadsUpNotificationSpace( @@ -363,7 +366,6 @@ fun SceneScope.NotificationScrollingStack( snapshotFlow { syntheticScroll.value } .collect { delta -> scrollNotificationStack( - scope = coroutineScope, delta = delta, animate = false, scrimOffset = scrimOffset, @@ -383,7 +385,6 @@ fun SceneScope.NotificationScrollingStack( // composed at least once), and our remote input row overlaps with the ime bounds. if (isRemoteInputActive && imeTopValue > 0f && remoteInputRowBottom > imeTopValue) { scrollNotificationStack( - scope = coroutineScope, delta = remoteInputRowBottom - imeTopValue, animate = true, scrimOffset = scrimOffset, @@ -450,7 +451,10 @@ fun SceneScope.NotificationScrollingStack( scrimCornerRadius, screenCornerRadius, { expansionFraction }, - shouldPunchHoleBehindScrim, + shouldAnimateScrimCornerRadius( + layoutState, + shouldPunchHoleBehindScrim, + ), ) .let { scrimRounding.value.toRoundedCornerShape(it) } clip = true @@ -514,6 +518,9 @@ fun SceneScope.NotificationScrollingStack( NotificationPlaceholder( stackScrollView = stackScrollView, viewModel = viewModel, + useStackBounds = { + !shouldUseLockscreenStackBounds(layoutState.transitionState) + }, modifier = Modifier.notificationStackHeight( view = stackScrollView, @@ -600,6 +607,7 @@ fun SceneScope.NotificationStackCutoffGuideline( private fun SceneScope.NotificationPlaceholder( stackScrollView: NotificationScrollView, viewModel: NotificationsPlaceholderViewModel, + useStackBounds: () -> Boolean, modifier: Modifier = Modifier, ) { Box( @@ -609,21 +617,26 @@ private fun SceneScope.NotificationPlaceholder( .debugBackground(viewModel, DEBUG_STACK_COLOR) .onSizeChanged { size -> debugLog(viewModel) { "STACK onSizeChanged: size=$size" } } .onGloballyPositioned { coordinates: LayoutCoordinates -> - val positionInWindow = coordinates.positionInWindow() - debugLog(viewModel) { - "STACK onGloballyPositioned:" + - " size=${coordinates.size}" + - " position=$positionInWindow" + - " bounds=${coordinates.boundsInWindow()}" + // This element is opted out of the shared element system, so there can be + // multiple instances of it during a transition. Thus we need to determine which + // instance should feed its bounds to NSSL to avoid providing conflicting values + val useBounds = useStackBounds() + if (useBounds) { + // NOTE: positionInWindow.y scrolls off screen, but boundsInWindow.top won't + val positionInWindow = coordinates.positionInWindow() + debugLog(viewModel) { + "STACK onGloballyPositioned:" + + " size=${coordinates.size}" + + " position=$positionInWindow" + + " bounds=${coordinates.boundsInWindow()}" + } + stackScrollView.setStackTop(positionInWindow.y) } - // NOTE: positionInWindow.y scrolls off screen, but boundsInWindow.top will not - stackScrollView.setStackTop(positionInWindow.y) } ) } private suspend fun scrollNotificationStack( - scope: CoroutineScope, delta: Float, animate: Boolean, scrimOffset: Animatable<Float, AnimationVector1D>, @@ -638,7 +651,7 @@ private suspend fun scrollNotificationStack( if (animate) { // launch a new coroutine for the remainder animation so that it doesn't suspend the // scrim animation, allowing both to play simultaneously. - scope.launch { scrollState.animateScrollTo(remainingDelta) } + coroutineScope { launch { scrollState.animateScrollTo(remainingDelta) } } } else { scrollState.scrollTo(remainingDelta) } @@ -658,6 +671,18 @@ private suspend fun scrollNotificationStack( } } +private fun shouldUseLockscreenStackBounds(state: TransitionState): Boolean { + return state is TransitionState.Idle && state.currentScene == Scenes.Lockscreen +} + +private fun shouldAnimateScrimCornerRadius( + state: SceneTransitionLayoutState, + shouldPunchHoleBehindScrim: Boolean, +): Boolean { + return shouldPunchHoleBehindScrim || + state.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen) +} + private fun calculateCornerRadius( scrimCornerRadius: Dp, screenCornerRadius: Dp, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt index 58fbf430b20c..303a6f0d942a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt @@ -82,18 +82,35 @@ val SceneContainerTransitions = transitions { sharedElement(Notifications.Elements.HeadsUpNotificationPlaceholder, enabled = false) } from(Scenes.Shade, to = Scenes.QuickSettings) { shadeToQuickSettingsTransition() } + from(Scenes.Shade, to = Scenes.Lockscreen) { + reversed { lockscreenToShadeTransition() } + sharedElement(Notifications.Elements.NotificationStackPlaceholder, enabled = false) + } // Overlay transitions to(Overlays.NotificationsShade) { toNotificationsShadeTransition() } to(Overlays.QuickSettingsShade) { toQuickSettingsShadeTransition() } - from(Overlays.NotificationsShade, Overlays.QuickSettingsShade) { + from(Overlays.NotificationsShade, to = Overlays.QuickSettingsShade) { notificationsShadeToQuickSettingsShadeTransition() } + from(Scenes.Gone, to = Overlays.NotificationsShade, key = SlightlyFasterShadeCollapse) { + toNotificationsShadeTransition(durationScale = 0.9) + } + from(Scenes.Gone, to = Overlays.QuickSettingsShade, key = SlightlyFasterShadeCollapse) { + toQuickSettingsShadeTransition(durationScale = 0.9) + } + from(Scenes.Lockscreen, to = Overlays.NotificationsShade, key = SlightlyFasterShadeCollapse) { + toNotificationsShadeTransition(durationScale = 0.9) + } + from(Scenes.Lockscreen, to = Overlays.QuickSettingsShade, key = SlightlyFasterShadeCollapse) { + toQuickSettingsShadeTransition(durationScale = 0.9) + } // Scene overscroll overscrollDisabled(Scenes.Gone, Orientation.Vertical) + overscrollDisabled(Scenes.Lockscreen, Orientation.Vertical) overscroll(Scenes.Bouncer, Orientation.Vertical) { translate(Bouncer.Elements.Content, y = { absoluteDistance }) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt index ac54896c5031..4c0efd2047ff 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt @@ -4,13 +4,19 @@ import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.tween import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.TransitionBuilder +import com.android.compose.animation.scene.UserActionDistance import com.android.systemui.bouncer.ui.composable.Bouncer const val FROM_LOCK_SCREEN_TO_BOUNCER_FADE_FRACTION = 0.5f +const val FROM_LOCK_SCREEN_TO_BOUNCER_SWIPE_DISTANCE_FRACTION = 0.5f fun TransitionBuilder.lockscreenToBouncerTransition() { spec = tween(durationMillis = 500) + distance = UserActionDistance { fromSceneSize, _ -> + fromSceneSize.height * FROM_LOCK_SCREEN_TO_BOUNCER_SWIPE_DISTANCE_FRACTION + } + translate(Bouncer.Elements.Content, y = 300.dp) fractionRange(end = FROM_LOCK_SCREEN_TO_BOUNCER_FADE_FRACTION) { fade(Bouncer.Elements.Background) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 7f2ee2a8351a..db0fe3e3f79d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -296,7 +296,7 @@ private fun SceneScope.SingleShade( val shouldPunchHoleBehindScrim = layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade) || - layoutState.isTransitioningBetween(Scenes.Lockscreen, Scenes.Shade) + layoutState.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade) // Media is visible and we are in landscape on a small height screen val mediaInRow = isMediaVisible && isLandscape() val mediaOffset by diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt index c163c6fc0a30..0490a26019e1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt @@ -350,7 +350,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() { testScope.runTest { underTest.performDotFeedback(null) - assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.DRAG_INDICATOR) + assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.DRAG_INDICATOR_DISCRETE) assertThat(msdlPlayer.latestPropertiesPlayed).isNull() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt index e25c1a71a5a6..d5020a580d00 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt @@ -109,7 +109,9 @@ class CommunalSceneTransitionInteractorTest : SysuiTestCase() { kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) underTest.start() kosmos.communalSceneRepository.setTransitionState(sceneTransitions) - testScope.launch { keyguardTransitionRepository.emitInitialStepsFromOff(LOCKSCREEN) } + testScope.launch { + keyguardTransitionRepository.emitInitialStepsFromOff(LOCKSCREEN, testSetup = true) + } } /** Transition from blank to glanceable hub. This is the default case. */ diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt index ab33269ec954..d7fe263df581 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt @@ -16,10 +16,10 @@ package com.android.systemui.education.domain.ui.view +import android.app.Dialog import android.app.Notification import android.app.NotificationManager import android.content.applicationContext -import android.widget.Toast import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -34,11 +34,13 @@ import com.android.systemui.education.ui.viewmodel.ContextualEduViewModel import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -51,6 +53,7 @@ import org.mockito.Mock import org.mockito.junit.MockitoJUnit import org.mockito.kotlin.any import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -63,10 +66,12 @@ class ContextualEduUiCoordinatorTest : SysuiTestCase() { private val minDurationForNextEdu = KeyboardTouchpadEduInteractor.minIntervalBetweenEdu + 1.seconds private lateinit var underTest: ContextualEduUiCoordinator - @Mock private lateinit var toast: Toast + @Mock private lateinit var dialog: Dialog @Mock private lateinit var notificationManager: NotificationManager + @Mock private lateinit var accessibilityManagerWrapper: AccessibilityManagerWrapper @get:Rule val mockitoRule = MockitoJUnit.rule() private var toastContent = "" + private val timeoutMillis = 3500L @Before fun setUp() { @@ -75,30 +80,35 @@ class ContextualEduUiCoordinatorTest : SysuiTestCase() { interactor.updateTouchpadFirstConnectionTime() } + whenever(accessibilityManagerWrapper.getRecommendedTimeoutMillis(any(), any())) + .thenReturn(timeoutMillis.toInt()) + val viewModel = ContextualEduViewModel( kosmos.applicationContext.resources, - kosmos.keyboardTouchpadEduInteractor + kosmos.keyboardTouchpadEduInteractor, + accessibilityManagerWrapper, ) + underTest = ContextualEduUiCoordinator( kosmos.applicationCoroutineScope, viewModel, kosmos.applicationContext, notificationManager - ) { content -> - toastContent = content - toast + ) { model -> + toastContent = model.message + dialog } underTest.start() kosmos.keyboardTouchpadEduInteractor.start() } @Test - fun showToastOnNewEdu() = + fun showDialogOnNewEdu() = testScope.runTest { triggerEducation(BACK) - verify(toast).show() + verify(dialog).show() } @Test @@ -111,6 +121,14 @@ class ContextualEduUiCoordinatorTest : SysuiTestCase() { } @Test + fun dismissDialogAfterTimeout() = + testScope.runTest { + triggerEducation(BACK) + advanceTimeBy(timeoutMillis + 1) + verify(dialog).dismiss() + } + + @Test fun verifyBackEduToastContent() = testScope.runTest { triggerEducation(BACK) 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 a08fbbf75805..fa304c99ecc9 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 @@ -198,6 +198,13 @@ class FromDreamingTransitionInteractorTest(flags: FlagsParameterization?) : Sysu @DisableFlags(Flags.FLAG_SCENE_CONTAINER) fun testTransitionToGlanceableHubOnWake() = testScope.runTest { + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DREAMING, + testScope, + ) + reset(transitionRepository) + whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) kosmos.setCommunalAvailable(true) runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt index ba689179c33d..d97909a1347e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt @@ -19,17 +19,22 @@ package com.android.systemui.keyguard.domain.interactor 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.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.shared.model.DismissAction import com.android.systemui.keyguard.shared.model.KeyguardDone +import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.power.domain.interactor.powerInteractor @@ -44,6 +49,7 @@ import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -76,12 +82,12 @@ class KeyguardDismissActionInteractorTest : SysuiTestCase() { transitionInteractor = kosmos.keyguardTransitionInteractor, dismissInteractor = dismissInteractor, applicationScope = testScope.backgroundScope, - sceneInteractor = { kosmos.sceneInteractor }, deviceUnlockedInteractor = { kosmos.deviceUnlockedInteractor }, powerInteractor = kosmos.powerInteractor, alternateBouncerInteractor = kosmos.alternateBouncerInteractor, shadeInteractor = { kosmos.shadeInteractor }, keyguardInteractor = { kosmos.keyguardInteractor }, + sceneInteractor = { kosmos.sceneInteractor }, ) } @@ -180,7 +186,11 @@ class KeyguardDismissActionInteractorTest : SysuiTestCase() { ) assertThat(executeDismissAction).isNull() + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) kosmos.setSceneTransition(Idle(Scenes.Gone)) + kosmos.sceneInteractor.changeScene(Scenes.Gone, "") assertThat(executeDismissAction).isNotNull() } @@ -303,4 +313,78 @@ class KeyguardDismissActionInteractorTest : SysuiTestCase() { underTest.setKeyguardDone(KeyguardDone.IMMEDIATE) assertThat(keyguardDoneTiming).isEqualTo(KeyguardDone.IMMEDIATE) } + + @Test + @EnableSceneContainer + fun dismissAction_executesBeforeItsReset_sceneContainerOn_swipeAuth_fromQsScene() = + testScope.runTest { + val canSwipeToEnter by collectLastValue(kosmos.deviceEntryInteractor.canSwipeToEnter) + val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene) + val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(currentScene!!) + ) + kosmos.sceneInteractor.setTransitionState(transitionState) + val executeDismissAction by collectLastValue(underTest.executeDismissAction) + val resetDismissAction by collectLastValue(underTest.resetDismissAction) + assertThat(executeDismissAction).isNull() + assertThat(resetDismissAction).isNull() + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) + assertThat(canSwipeToEnter).isTrue() + kosmos.sceneInteractor.changeScene(Scenes.QuickSettings, "") + transitionState.value = ObservableTransitionState.Idle(Scenes.QuickSettings) + assertThat(currentScene).isEqualTo(Scenes.QuickSettings) + + assertThat(executeDismissAction).isNull() + assertThat(resetDismissAction).isNull() + + val dismissAction = + DismissAction.RunImmediately( + onDismissAction = { KeyguardDone.LATER }, + onCancelAction = {}, + message = "message", + willAnimateOnLockscreen = true, + ) + underTest.setDismissAction(dismissAction) + // Should still be null because the transition to Gone has not yet happened. + assertThat(executeDismissAction).isNull() + assertThat(resetDismissAction).isNull() + + transitionState.value = + ObservableTransitionState.Transition.ChangeScene( + fromScene = Scenes.QuickSettings, + toScene = Scenes.Gone, + currentScene = flowOf(Scenes.QuickSettings), + currentOverlays = emptySet(), + progress = flowOf(0.5f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(false), + previewProgress = flowOf(0f), + isInPreviewStage = flowOf(false), + ) + runCurrent() + assertThat(executeDismissAction).isNull() + assertThat(resetDismissAction).isNull() + + transitionState.value = + ObservableTransitionState.Transition.ChangeScene( + fromScene = Scenes.QuickSettings, + toScene = Scenes.Gone, + currentScene = flowOf(Scenes.Gone), + currentOverlays = emptySet(), + progress = flowOf(1f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(false), + previewProgress = flowOf(0f), + isInPreviewStage = flowOf(false), + ) + kosmos.sceneInteractor.changeScene(Scenes.Gone, "") + assertThat(currentScene).isEqualTo(Scenes.Gone) + runCurrent() + assertThat(executeDismissAction).isNotNull() + assertThat(resetDismissAction).isNull() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt index 129752e4f106..aab46d8cb73a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt @@ -22,6 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.testKosmos import org.junit.Before import org.junit.Test @@ -44,6 +45,7 @@ class KeyguardBlueprintViewModelTest : SysuiTestCase() { KeyguardBlueprintViewModel( handler = kosmos.fakeExecutorHandler, keyguardBlueprintInteractor = keyguardBlueprintInteractor, + keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt index e6ea64f8ee71..d0da2e9671c0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt @@ -89,9 +89,12 @@ class LockscreenToOccludedTransitionViewModelTest(flags: FlagsParameterization) } @Test - fun lockscreenFadeOut() = + fun lockscreenFadeOut_shadeNotExpanded() = testScope.runTest { val values by collectValues(underTest.lockscreenAlpha) + shadeExpanded(false) + runCurrent() + repository.sendTransitionSteps( steps = listOf( @@ -104,10 +107,34 @@ class LockscreenToOccludedTransitionViewModelTest(flags: FlagsParameterization) ), testScope = testScope, ) - // Only 5 values should be present, since the dream overlay runs for a small fraction - // of the overall animation time assertThat(values.size).isEqualTo(5) - values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } + assertThat(values[0]).isEqualTo(1f) + assertThat(values[1]).isEqualTo(1f) + assertThat(values[2]).isIn(Range.open(0f, 1f)) + assertThat(values[3]).isIn(Range.open(0f, 1f)) + assertThat(values[4]).isEqualTo(0f) + } + + @Test + fun lockscreenFadeOut_shadeExpanded() = + testScope.runTest { + val values by collectValues(underTest.lockscreenAlpha) + shadeExpanded(true) + runCurrent() + + repository.sendTransitionSteps( + steps = + listOf( + step(0f, TransitionState.STARTED), // Should start running here... + step(0f), + step(.1f), + step(.4f), + step(.7f), // ...up to here + step(1f), + ), + testScope = testScope, + ) + values.forEach { assertThat(it).isEqualTo(0f) } } @Test @@ -115,7 +142,7 @@ class LockscreenToOccludedTransitionViewModelTest(flags: FlagsParameterization) testScope.runTest { configurationRepository.setDimensionPixelSize( R.dimen.lockscreen_to_occluded_transition_lockscreen_translation_y, - 100 + 100, ) val values by collectValues(underTest.lockscreenTranslationY) repository.sendTransitionSteps( @@ -138,7 +165,7 @@ class LockscreenToOccludedTransitionViewModelTest(flags: FlagsParameterization) testScope.runTest { configurationRepository.setDimensionPixelSize( R.dimen.lockscreen_to_occluded_transition_lockscreen_translation_y, - 100 + 100, ) val values by collectValues(underTest.lockscreenTranslationY) repository.sendTransitionSteps( @@ -171,7 +198,7 @@ class LockscreenToOccludedTransitionViewModelTest(flags: FlagsParameterization) listOf( step(0f, TransitionState.STARTED), step(.5f), - step(1f, TransitionState.FINISHED) + step(1f, TransitionState.FINISHED), ), testScope = testScope, ) @@ -228,7 +255,7 @@ class LockscreenToOccludedTransitionViewModelTest(flags: FlagsParameterization) to = KeyguardState.OCCLUDED, value = value, transitionState = state, - ownerName = "LockscreenToOccludedTransitionViewModelTest" + ownerName = "LockscreenToOccludedTransitionViewModelTest", ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelTest.kt new file mode 100644 index 000000000000..0b7a38eb9ebd --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelTest.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.ui.viewmodel + +import android.platform.test.flag.junit.FlagsParameterization +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.andSceneContainer +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.collect.Range +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +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 + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(ParameterizedAndroidJunit4::class) +class OffToLockscreenTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { + val kosmos = testKosmos() + val testScope = kosmos.testScope + val repository = kosmos.fakeKeyguardTransitionRepository + lateinit var underTest: OffToLockscreenTransitionViewModel + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf().andSceneContainer() + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + + @Before + fun setup() { + underTest = kosmos.offToLockscreenTransitionViewModel + } + + @Test + fun lockscreenAlpha() = + testScope.runTest { + val alpha by collectLastValue(underTest.lockscreenAlpha) + + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) + repository.sendTransitionStep(step(0f)) + assertThat(alpha).isEqualTo(0f) + + repository.sendTransitionStep(step(0.66f)) + assertThat(alpha).isIn(Range.open(.1f, .9f)) + + repository.sendTransitionStep(step(1f)) + assertThat(alpha).isEqualTo(1f) + } + + private fun step( + value: Float, + state: TransitionState = TransitionState.RUNNING, + ): TransitionStep { + return TransitionStep( + from = KeyguardState.OFF, + to = KeyguardState.LOCKSCREEN, + value = value, + transitionState = state, + ownerName = "OffToLockscreenTransitionViewModelTest", + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt index 3e3aa4f079f7..e12c67b24893 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt @@ -18,7 +18,7 @@ import com.android.systemui.util.mockito.whenever import com.android.wm.shell.recents.RecentTasks import com.android.wm.shell.shared.GroupedRecentTaskInfo import com.android.wm.shell.shared.split.SplitBounds -import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50 +import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50 import com.google.common.truth.Truth.assertThat import java.util.Optional import java.util.function.Consumer @@ -268,7 +268,7 @@ class ShellRecentTaskListProviderTest : SysuiTestCase() { GroupedRecentTaskInfo.forSplitTasks( createTaskInfo(taskId1, userId1, isVisible), createTaskInfo(taskId2, userId2, isVisible), - SplitBounds(Rect(), Rect(), taskId1, taskId2, SNAP_TO_50_50) + SplitBounds(Rect(), Rect(), taskId1, taskId2, SNAP_TO_2_50_50) ) private fun createTaskInfo(taskId: Int, userId: Int, isVisible: Boolean = false) = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt index 88a1df147489..ada2138d4d52 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt @@ -16,46 +16,130 @@ package com.android.systemui.notifications.ui.viewmodel +import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.AuthenticationResult +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest +import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.domain.startable.sceneContainerStartable import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayContentViewModel 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) @TestableLooper.RunWithLooper @EnableSceneContainer +@EnableFlags(DualShade.FLAG_NAME) class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val sceneInteractor = kosmos.sceneInteractor - private val underTest = kosmos.notificationsShadeOverlayContentViewModel + private val underTest by lazy { kosmos.notificationsShadeOverlayContentViewModel } + + @Before + fun setUp() { + kosmos.sceneContainerStartable.start() + underTest.activateIn(testScope) + } @Test fun onScrimClicked_hidesShade() = testScope.runTest { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - sceneInteractor.showOverlay( - overlay = Overlays.NotificationsShade, - loggingReason = "test", - ) + sceneInteractor.showOverlay(Overlays.NotificationsShade, "test") assertThat(currentOverlays).contains(Overlays.NotificationsShade) underTest.onScrimClicked() assertThat(currentOverlays).doesNotContain(Overlays.NotificationsShade) } + + @Test + fun deviceLocked_hidesShade() = + testScope.runTest { + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + unlockDevice() + sceneInteractor.showOverlay(Overlays.NotificationsShade, "test") + assertThat(currentOverlays).contains(Overlays.NotificationsShade) + + lockDevice() + + assertThat(currentOverlays).doesNotContain(Overlays.NotificationsShade) + } + + @Test + fun bouncerShown_hidesShade() = + testScope.runTest { + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + lockDevice() + sceneInteractor.showOverlay(Overlays.NotificationsShade, "test") + assertThat(currentOverlays).contains(Overlays.NotificationsShade) + + sceneInteractor.changeScene(Scenes.Bouncer, "test") + runCurrent() + + assertThat(currentOverlays).doesNotContain(Overlays.NotificationsShade) + } + + @Test + fun shadeNotTouchable_hidesShade() = + testScope.runTest { + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + val isShadeTouchable by collectLastValue(kosmos.shadeInteractor.isShadeTouchable) + assertThat(isShadeTouchable).isTrue() + sceneInteractor.showOverlay(Overlays.NotificationsShade, "test") + assertThat(currentOverlays).contains(Overlays.NotificationsShade) + + lockDevice() + assertThat(isShadeTouchable).isFalse() + assertThat(currentOverlays).doesNotContain(Overlays.NotificationsShade) + } + + private fun TestScope.lockDevice() { + val currentScene by collectLastValue(sceneInteractor.currentScene) + kosmos.powerInteractor.setAsleepForTest() + runCurrent() + + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + } + + private suspend fun TestScope.unlockDevice() { + val currentScene by collectLastValue(sceneInteractor.currentScene) + kosmos.powerInteractor.setAwakeForTest() + runCurrent() + assertThat( + kosmos.authenticationInteractor.authenticate( + FakeAuthenticationRepository.DEFAULT_PIN + ) + ) + .isEqualTo(AuthenticationResult.SUCCEEDED) + + assertThat(currentScene).isEqualTo(Scenes.Gone) + } } 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 new file mode 100644 index 000000000000..4bbdfa44e087 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt @@ -0,0 +1,69 @@ +/* + * 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.viewmodel + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.testing.TestLifecycleOwner +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.testKosmos +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestResult +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before + +@OptIn(ExperimentalCoroutinesApi::class) +abstract class AbstractQSFragmentComposeViewModelTest : SysuiTestCase() { + protected val kosmos = testKosmos() + + protected val lifecycleOwner = + TestLifecycleOwner( + initialState = Lifecycle.State.CREATED, + coroutineDispatcher = kosmos.testDispatcher, + ) + + protected val underTest by lazy { + kosmos.qsFragmentComposeViewModelFactory.create(lifecycleOwner.lifecycleScope) + } + + @Before + fun setUp() { + Dispatchers.setMain(kosmos.testDispatcher) + } + + @After + fun teardown() { + Dispatchers.resetMain() + } + + protected inline fun TestScope.testWithinLifecycle( + crossinline block: suspend TestScope.() -> TestResult + ): TestResult { + return runTest { + lifecycleOwner.setCurrentState(Lifecycle.State.RESUMED) + lifecycleOwner.lifecycleScope.launch { underTest.activate() } + block().also { lifecycleOwner.setCurrentState(Lifecycle.State.DESTROYED) } + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt new file mode 100644 index 000000000000..57a9377cea9e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt @@ -0,0 +1,98 @@ +/* + * 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.viewmodel + +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.sysuiStatusBarStateController +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@SmallTest +@RunWith(Parameterized::class) +@RunWithLooper +class QSFragmentComposeViewModelForceQSTest(private val testData: TestData) : + AbstractQSFragmentComposeViewModelTest() { + + @Test + fun forceQs_orRealExpansion() = + with(kosmos) { + testScope.testWithinLifecycle { + val expansionState by collectLastValue(underTest.expansionState) + + with(testData) { + sysuiStatusBarStateController.setState(statusBarState) + underTest.isQSExpanded = expanded + underTest.isStackScrollerOverscrolling = stackScrollerOverScrolling + fakeDeviceEntryRepository.setBypassEnabled(bypassEnabled) + underTest.isTransitioningToFullShade = transitioningToFullShade + underTest.isInSplitShade = inSplitShade + + underTest.qsExpansionValue = EXPANSION + assertThat(expansionState!!.progress) + .isEqualTo(if (expectedForceQS) 1f else EXPANSION) + } + } + } + + data class TestData( + val statusBarState: Int, + val expanded: Boolean, + val stackScrollerOverScrolling: Boolean, + val bypassEnabled: Boolean, + val transitioningToFullShade: Boolean, + val inSplitShade: Boolean, + ) { + private val inKeyguard = statusBarState == StatusBarState.KEYGUARD + + private val showCollapsedOnKeyguard = + bypassEnabled || (transitioningToFullShade && !inSplitShade) + + val expectedForceQS = + (expanded || stackScrollerOverScrolling) && (inKeyguard && !showCollapsedOnKeyguard) + } + + companion object { + private const val EXPANSION = 0.3f + + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun createTestData(): List<TestData> { + return statusBarStates.flatMap { statusBarState -> + (0u..31u).map { bitfield -> + TestData( + statusBarState, + expanded = (bitfield or 1u) == 1u, + stackScrollerOverScrolling = (bitfield or 2u) == 1u, + bypassEnabled = (bitfield or 4u) == 1u, + transitioningToFullShade = (bitfield or 8u) == 1u, + inSplitShade = (bitfield or 16u) == 1u, + ) + } + } + } + + private val statusBarStates = + setOf(StatusBarState.SHADE, StatusBarState.KEYGUARD, StatusBarState.SHADE_LOCKED) + } +} 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 6f20e70f84a8..c19e4b834c7c 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 @@ -19,64 +19,28 @@ package com.android.systemui.qs.composefragment.viewmodel import android.app.StatusBarManager import android.content.testableContext import android.testing.TestableLooper.RunWithLooper -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -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.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.qs.fgsManagerController +import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor import com.android.systemui.res.R import com.android.systemui.shade.largeScreenHeaderHelper import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository import com.android.systemui.statusbar.sysuiStatusBarStateController -import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat -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 -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper -@OptIn(ExperimentalCoroutinesApi::class) -class QSFragmentComposeViewModelTest : SysuiTestCase() { - private val kosmos = testKosmos() - - private val lifecycleOwner = - TestLifecycleOwner( - initialState = Lifecycle.State.CREATED, - coroutineDispatcher = kosmos.testDispatcher, - ) - - private val underTest by lazy { - kosmos.qsFragmentComposeViewModelFactory.create(lifecycleOwner.lifecycleScope) - } - - @Before - fun setUp() { - Dispatchers.setMain(kosmos.testDispatcher) - } - - @After - fun teardown() { - Dispatchers.resetMain() - } +class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() { @Test fun qsExpansionValueChanges_correctExpansionState() = @@ -205,16 +169,30 @@ class QSFragmentComposeViewModelTest : SysuiTestCase() { } } - private inline fun TestScope.testWithinLifecycle( - crossinline block: suspend TestScope.() -> TestResult - ): TestResult { - return runTest { - lifecycleOwner.setCurrentState(Lifecycle.State.RESUMED) - block().also { lifecycleOwner.setCurrentState(Lifecycle.State.DESTROYED) } + @Test + fun squishinessInExpansion_setInInteractor() = + with(kosmos) { + testScope.testWithinLifecycle { + val squishiness by collectLastValue(tileSquishinessInteractor.squishiness) + + underTest.squishinessFractionValue = 0.3f + assertThat(squishiness).isWithin(epsilon).of(0.3f.constrainSquishiness()) + + underTest.squishinessFractionValue = 0f + assertThat(squishiness).isWithin(epsilon).of(0f.constrainSquishiness()) + + underTest.squishinessFractionValue = 1f + assertThat(squishiness).isWithin(epsilon).of(1f.constrainSquishiness()) + } } - } companion object { private const val QS_DISABLE_FLAG = StatusBarManager.DISABLE2_QUICK_SETTINGS + + private fun Float.constrainSquishiness(): Float { + return (0.1f + this * 0.9f).coerceIn(0f, 1f) + } + + private const val epsilon = 0.001f } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt index 9e90090549dd..a9a527fb8df6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt @@ -22,10 +22,8 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testScope import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository -import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout +import com.android.systemui.qs.panels.domain.interactor.infiniteGridLayout import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel -import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel -import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -44,8 +42,7 @@ class InfiniteGridLayoutTest : SysuiTestCase() { } } - private val underTest = - with(kosmos) { InfiniteGridLayout(iconTilesViewModel, fixedColumnsSizeViewModel) } + private val underTest = kosmos.infiniteGridLayout @Test fun correctPagination_underOnePage_sameOrder() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java index 2580ac2c8da7..7798f46fdb46 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java @@ -14,6 +14,8 @@ package com.android.systemui.qs.tileimpl; +import static com.android.systemui.Flags.FLAG_QS_NEW_TILES; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.mockito.ArgumentMatchers.any; @@ -21,11 +23,16 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.Context; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.service.quicksettings.Tile; import android.testing.UiThreadTest; import android.widget.ImageView; @@ -47,7 +54,6 @@ import org.mockito.Mockito; @UiThreadTest @SmallTest public class QSIconViewImplTest extends SysuiTestCase { - private QSIconViewImpl mIconView; @Before @@ -106,6 +112,34 @@ public class QSIconViewImplTest extends SysuiTestCase { verify(iv).setImageTintList(argThat(stateList -> stateList.getColors()[0] == desiredColor)); } + + @EnableFlags(FLAG_QS_NEW_TILES) + @Test + public void testIconPreloaded_withFlagOn_immediatelyLoadsAll3TintColors() { + Context ctx = spy(mContext); + + QSIconViewImpl iconView = new QSIconViewImpl(ctx); + + verify(ctx, times(3)).obtainStyledAttributes(any()); + + iconView.getColor(new State()); // this should not increase the call count + + verify(ctx, times(3)).obtainStyledAttributes(any()); + } + + @DisableFlags(FLAG_QS_NEW_TILES) + @Test + public void testIconPreloaded_withFlagOff_loadsOneTintColorAfterIconColorIsRead() { + Context ctx = spy(mContext); + QSIconViewImpl iconView = new QSIconViewImpl(ctx); + + verify(ctx, never()).obtainStyledAttributes(any()); // none of the colors are preloaded + + iconView.getColor(new State()); + + verify(ctx, times(1)).obtainStyledAttributes(any()); + } + @Test public void testStateSetCorrectly_toString() { ImageView iv = mock(ImageView.class); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt index 620e90dcaa62..d32ba47204c0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt @@ -17,13 +17,17 @@ package com.android.systemui.qs.tiles.impl.internet.domain import android.graphics.drawable.TestStubDrawable +import android.os.fakeExecutorHandler import android.widget.Switch import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.settingslib.graph.SignalDrawable import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text +import com.android.systemui.common.shared.model.Text.Companion.loadText import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel @@ -31,6 +35,9 @@ import com.android.systemui.qs.tiles.impl.internet.qsInternetTileConfig import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.res.R import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS +import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel +import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel +import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel import org.junit.Test import org.junit.runner.RunWith @@ -39,25 +46,93 @@ import org.junit.runner.RunWith class InternetTileMapperTest : SysuiTestCase() { private val kosmos = Kosmos() private val internetTileConfig = kosmos.qsInternetTileConfig + private val handler = kosmos.fakeExecutorHandler private val mapper by lazy { InternetTileMapper( context.orCreateTestableResources .apply { addOverride(R.drawable.ic_qs_no_internet_unavailable, TestStubDrawable()) + addOverride(R.drawable.ic_satellite_connected_2, TestStubDrawable()) addOverride(wifiRes, TestStubDrawable()) } .resources, context.theme, - context + context, + handler, ) } @Test - fun withActiveModel_mappedStateMatchesDataModel() { + fun withActiveCellularModel_mappedStateMatchesDataModel() { val inputModel = InternetTileModel.Active( secondaryLabel = Text.Resource(R.string.quick_settings_networks_available), - iconId = wifiRes, + icon = InternetTileIconModel.Cellular(3), + stateDescription = null, + contentDescription = + ContentDescription.Resource(R.string.quick_settings_internet_label), + ) + + val outputState = mapper.map(internetTileConfig, inputModel) + + val signalDrawable = SignalDrawable(context, handler) + signalDrawable.setLevel(3) + val expectedState = + createInternetTileState( + QSTileState.ActivationState.ACTIVE, + context.getString(R.string.quick_settings_networks_available), + Icon.Loaded(signalDrawable, null), + null, + context.getString(R.string.quick_settings_internet_label), + ) + QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) + } + + @Test + fun withActiveSatelliteModel_mappedStateMatchesDataModel() { + val inputIcon = + SignalIconModel.Satellite( + 3, + Icon.Resource( + res = R.drawable.ic_satellite_connected_2, + contentDescription = + ContentDescription.Resource( + R.string.accessibility_status_bar_satellite_good_connection + ), + ), + ) + val inputModel = + InternetTileModel.Active( + secondaryLabel = Text.Resource(R.string.quick_settings_networks_available), + icon = InternetTileIconModel.Satellite(inputIcon.icon), + stateDescription = null, + contentDescription = + ContentDescription.Resource( + R.string.accessibility_status_bar_satellite_good_connection + ), + ) + + val outputState = mapper.map(internetTileConfig, inputModel) + + val expectedSatIcon = SatelliteIconModel.fromSignalStrength(3) + + val expectedState = + createInternetTileState( + QSTileState.ActivationState.ACTIVE, + inputModel.secondaryLabel.loadText(context).toString(), + Icon.Loaded(context.getDrawable(expectedSatIcon!!.res)!!, null), + expectedSatIcon.res, + expectedSatIcon.contentDescription.loadContentDescription(context).toString(), + ) + QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) + } + + @Test + fun withActiveWifiModel_mappedStateMatchesDataModel() { + val inputModel = + InternetTileModel.Active( + secondaryLabel = Text.Resource(R.string.quick_settings_networks_available), + icon = InternetTileIconModel.ResourceId(wifiRes), stateDescription = null, contentDescription = ContentDescription.Resource(R.string.quick_settings_internet_label), @@ -71,7 +146,7 @@ class InternetTileMapperTest : SysuiTestCase() { context.getString(R.string.quick_settings_networks_available), Icon.Loaded(context.getDrawable(wifiRes)!!, contentDescription = null), wifiRes, - context.getString(R.string.quick_settings_internet_label) + context.getString(R.string.quick_settings_internet_label), ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) } @@ -81,7 +156,7 @@ class InternetTileMapperTest : SysuiTestCase() { val inputModel = InternetTileModel.Inactive( secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable), - iconId = R.drawable.ic_qs_no_internet_unavailable, + icon = InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable), stateDescription = null, contentDescription = ContentDescription.Resource(R.string.quick_settings_networks_unavailable), @@ -95,10 +170,10 @@ class InternetTileMapperTest : SysuiTestCase() { context.getString(R.string.quick_settings_networks_unavailable), Icon.Loaded( context.getDrawable(R.drawable.ic_qs_no_internet_unavailable)!!, - contentDescription = null + contentDescription = null, ), R.drawable.ic_qs_no_internet_unavailable, - context.getString(R.string.quick_settings_networks_unavailable) + context.getString(R.string.quick_settings_networks_unavailable), ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) } @@ -107,7 +182,7 @@ class InternetTileMapperTest : SysuiTestCase() { activationState: QSTileState.ActivationState, secondaryLabel: String, icon: Icon, - iconRes: Int, + iconRes: Int? = null, contentDescription: String, ): QSTileState { val label = context.getString(R.string.quick_settings_internet_label) @@ -120,13 +195,13 @@ class InternetTileMapperTest : SysuiTestCase() { setOf( QSTileState.UserAction.CLICK, QSTileState.UserAction.TOGGLE_CLICK, - QSTileState.UserAction.LONG_CLICK + QSTileState.UserAction.LONG_CLICK, ), contentDescription, null, QSTileState.SideViewIcon.Chevron, QSTileState.EnabledState.ENABLED, - Switch::class.qualifiedName + Switch::class.qualifiedName, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt index 5a4506086058..5259aa84b193 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt @@ -18,14 +18,12 @@ package com.android.systemui.qs.tiles.impl.internet.domain.interactor import android.graphics.drawable.TestStubDrawable import android.os.UserHandle -import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.AccessibilityContentDescriptions import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription -import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.common.shared.model.Text.Companion.loadText import com.android.systemui.coroutines.collectLastValue @@ -49,6 +47,7 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIc import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel @@ -60,9 +59,7 @@ import com.android.systemui.util.mockito.mock 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.Assume.assumeFalse import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -144,7 +141,6 @@ class InternetTileDataInteractorTest : SysuiTestCase() { underTest = InternetTileDataInteractor( context, - testScope.coroutineContext, testScope.backgroundScope, airplaneModeRepository, connectivityRepository, @@ -164,9 +160,11 @@ class InternetTileDataInteractorTest : SysuiTestCase() { connectivityRepository.defaultConnections.value = DefaultConnectionModel() + val expectedIcon = + InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable) assertThat(latest?.secondaryLabel) .isEqualTo(Text.Resource(R.string.quick_settings_networks_unavailable)) - assertThat(latest?.iconId).isEqualTo(R.drawable.ic_qs_no_internet_unavailable) + assertThat(latest?.icon).isEqualTo(expectedIcon) } @Test @@ -183,11 +181,8 @@ class InternetTileDataInteractorTest : SysuiTestCase() { underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest)) ) - val networkModel = - WifiNetworkModel.Active.of( - level = 4, - ssid = "test ssid", - ) + val networkModel = WifiNetworkModel.Active.of(level = 4, ssid = "test ssid") + val wifiIcon = WifiIcon.fromModel(model = networkModel, context = context, showHotspotInfo = true) as WifiIcon.Visible @@ -198,12 +193,9 @@ class InternetTileDataInteractorTest : SysuiTestCase() { assertThat(latest?.secondaryTitle).isEqualTo("test ssid") assertThat(latest?.secondaryLabel).isNull() - val expectedIcon = - Icon.Loaded(context.getDrawable(WifiIcons.WIFI_NO_INTERNET_ICONS[4])!!, null) - val actualIcon = latest?.icon - assertThat(actualIcon).isEqualTo(expectedIcon) - assertThat(latest?.iconId).isEqualTo(WifiIcons.WIFI_NO_INTERNET_ICONS[4]) + val expectedIcon = InternetTileIconModel.ResourceId(WifiIcons.WIFI_NO_INTERNET_ICONS[4]) + assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.contentDescription.loadContentDescription(context)) .isEqualTo("$internet,test ssid") val expectedSd = wifiIcon.contentDescription @@ -229,8 +221,7 @@ class InternetTileDataInteractorTest : SysuiTestCase() { wifiRepository.setIsWifiDefault(true) wifiRepository.setWifiNetwork(networkModel) - val expectedIcon = - Icon.Loaded(context.getDrawable(WifiIcons.WIFI_NO_INTERNET_ICONS[4])!!, null) + val expectedIcon = InternetTileIconModel.ResourceId(WifiIcons.WIFI_NO_INTERNET_ICONS[4]) assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) .doesNotContain( @@ -249,9 +240,8 @@ class InternetTileDataInteractorTest : SysuiTestCase() { setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.TABLET) val expectedIcon = - Icon.Loaded( - context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_tablet)!!, - null + InternetTileIconModel.ResourceId( + com.android.settingslib.R.drawable.ic_hotspot_tablet ) assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) @@ -271,9 +261,8 @@ class InternetTileDataInteractorTest : SysuiTestCase() { setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.LAPTOP) val expectedIcon = - Icon.Loaded( - context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_laptop)!!, - null + InternetTileIconModel.ResourceId( + com.android.settingslib.R.drawable.ic_hotspot_laptop ) assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) @@ -293,10 +282,10 @@ class InternetTileDataInteractorTest : SysuiTestCase() { setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.WATCH) val expectedIcon = - Icon.Loaded( - context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_watch)!!, - null + InternetTileIconModel.ResourceId( + com.android.settingslib.R.drawable.ic_hotspot_watch ) + assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) .isEqualTo( @@ -315,10 +304,7 @@ class InternetTileDataInteractorTest : SysuiTestCase() { setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.AUTO) val expectedIcon = - Icon.Loaded( - context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_auto)!!, - null - ) + InternetTileIconModel.ResourceId(com.android.settingslib.R.drawable.ic_hotspot_auto) assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) .isEqualTo( @@ -336,9 +322,8 @@ class InternetTileDataInteractorTest : SysuiTestCase() { setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.PHONE) val expectedIcon = - Icon.Loaded( - context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!, - null + InternetTileIconModel.ResourceId( + com.android.settingslib.R.drawable.ic_hotspot_phone ) assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) @@ -358,9 +343,8 @@ class InternetTileDataInteractorTest : SysuiTestCase() { setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.UNKNOWN) val expectedIcon = - Icon.Loaded( - context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!, - null + InternetTileIconModel.ResourceId( + com.android.settingslib.R.drawable.ic_hotspot_phone ) assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) @@ -380,10 +364,10 @@ class InternetTileDataInteractorTest : SysuiTestCase() { setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.INVALID) val expectedIcon = - Icon.Loaded( - context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!, - null + InternetTileIconModel.ResourceId( + com.android.settingslib.R.drawable.ic_hotspot_phone ) + assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) .isEqualTo( @@ -426,8 +410,9 @@ class InternetTileDataInteractorTest : SysuiTestCase() { assertThat(latest?.secondaryLabel).isNull() assertThat(latest?.secondaryTitle) .isEqualTo(context.getString(R.string.quick_settings_networks_available)) - assertThat(latest?.icon).isNull() - assertThat(latest?.iconId).isEqualTo(R.drawable.ic_qs_no_internet_available) + val expectedIcon = + InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_available) + assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription).isNull() val expectedCd = "$internet,${context.getString(R.string.quick_settings_networks_available)}" @@ -435,54 +420,19 @@ class InternetTileDataInteractorTest : SysuiTestCase() { .isEqualTo(expectedCd) } - /** - * We expect a RuntimeException because [underTest] instantiates a SignalDrawable on the - * provided context, and so the SignalDrawable constructor attempts to instantiate a Handler() - * on the mentioned context. Since that context does not have a looper assigned to it, the - * handler instantiation will throw a RuntimeException. - * - * TODO(b/338068066): Robolectric behavior differs in that it does not throw the exception So - * either we should make Robolectric behave similar to the device test, or change this test to - * look for a different signal than the exception, when run by Robolectric. For now we just - * assume the test is not Robolectric. - */ - @Test(expected = java.lang.RuntimeException::class) - fun mobileDefault_usesNetworkNameAndIcon_throwsRunTimeException() = - testScope.runTest { - assumeFalse(isRobolectricTest()) - - collectLastValue(underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))) - - connectivityRepository.setMobileConnected() - mobileConnectionsRepository.mobileIsDefault.value = true - mobileConnectionRepository.apply { - setAllLevels(3) - setAllRoaming(false) - networkName.value = NetworkNameModel.Default("test network") - } - - runCurrent() - } - - /** - * See [mobileDefault_usesNetworkNameAndIcon_throwsRunTimeException] for description of the - * problem this test solves. The solution here is to assign a looper to the context via - * RunWithLooper. In the production code, the solution is to use a Main CoroutineContext for - * creating the SignalDrawable. - */ - @TestableLooper.RunWithLooper @Test - fun mobileDefault_run_withLooper_usesNetworkNameAndIcon() = + fun mobileDefault_usesNetworkNameAndIcon() = testScope.runTest { val latest by collectLastValue( underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest)) ) + val iconLevel = 3 connectivityRepository.setMobileConnected() mobileConnectionsRepository.mobileIsDefault.value = true mobileConnectionRepository.apply { - setAllLevels(3) + setAllLevels(iconLevel) setAllRoaming(false) networkName.value = NetworkNameModel.Default("test network") } @@ -491,8 +441,9 @@ class InternetTileDataInteractorTest : SysuiTestCase() { assertThat(latest?.secondaryTitle).isNotNull() assertThat(latest?.secondaryTitle.toString()).contains("test network") assertThat(latest?.secondaryLabel).isNull() - assertThat(latest?.icon).isInstanceOf(Icon.Loaded::class.java) - assertThat(latest?.iconId).isNull() + val expectedIcon = InternetTileIconModel.Cellular(iconLevel) + + assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription.loadContentDescription(context)) .isEqualTo(latest?.secondaryTitle.toString()) assertThat(latest?.contentDescription.loadContentDescription(context)) @@ -513,8 +464,8 @@ class InternetTileDataInteractorTest : SysuiTestCase() { assertThat(latest?.secondaryLabel.loadText(context)) .isEqualTo(ethernetIcon!!.contentDescription.loadContentDescription(context)) assertThat(latest?.secondaryTitle).isNull() - assertThat(latest?.iconId).isEqualTo(R.drawable.stat_sys_ethernet_fully) - assertThat(latest?.icon).isNull() + val expectedIcon = InternetTileIconModel.ResourceId(R.drawable.stat_sys_ethernet_fully) + assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription).isNull() assertThat(latest?.contentDescription.loadContentDescription(context)) .isEqualTo(latest?.secondaryLabel.loadText(context)) @@ -534,8 +485,8 @@ class InternetTileDataInteractorTest : SysuiTestCase() { assertThat(latest?.secondaryLabel.loadText(context)) .isEqualTo(ethernetIcon!!.contentDescription.loadContentDescription(context)) assertThat(latest?.secondaryTitle).isNull() - assertThat(latest?.iconId).isEqualTo(R.drawable.stat_sys_ethernet) - assertThat(latest?.icon).isNull() + val expectedIcon = InternetTileIconModel.ResourceId(R.drawable.stat_sys_ethernet) + assertThat(latest?.icon).isEqualTo(expectedIcon) assertThat(latest?.stateDescription).isNull() assertThat(latest?.contentDescription.loadContentDescription(context)) .isEqualTo(latest?.secondaryLabel.loadText(context)) @@ -543,11 +494,7 @@ class InternetTileDataInteractorTest : SysuiTestCase() { private fun setWifiNetworkWithHotspot(hotspot: WifiNetworkModel.HotspotDeviceType) { val networkModel = - WifiNetworkModel.Active.of( - level = 4, - ssid = "test ssid", - hotspotDeviceType = hotspot, - ) + WifiNetworkModel.Active.of(level = 4, ssid = "test ssid", hotspotDeviceType = hotspot) connectivityRepository.setWifiConnected() wifiRepository.setIsWifiDefault(true) @@ -560,7 +507,7 @@ class InternetTileDataInteractorTest : SysuiTestCase() { val NOT_CONNECTED_NETWORKS_UNAVAILABLE = InternetTileModel.Inactive( secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable), - iconId = R.drawable.ic_qs_no_internet_unavailable, + icon = InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable), stateDescription = null, contentDescription = ContentDescription.Resource(R.string.quick_settings_networks_unavailable), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt index abd1e2c7df82..f32894dfd383 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt @@ -16,45 +16,129 @@ package com.android.systemui.qs.ui.viewmodel +import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.AuthenticationResult +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest +import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.domain.startable.sceneContainerStartable import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.shared.flag.DualShade 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) @TestableLooper.RunWithLooper @EnableSceneContainer +@EnableFlags(DualShade.FLAG_NAME) class QuickSettingsShadeOverlayContentViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val sceneInteractor = kosmos.sceneInteractor - private val underTest = kosmos.quickSettingsShadeOverlayContentViewModel + private val underTest by lazy { kosmos.quickSettingsShadeOverlayContentViewModel } + + @Before + fun setUp() { + kosmos.sceneContainerStartable.start() + underTest.activateIn(testScope) + } @Test fun onScrimClicked_hidesShade() = testScope.runTest { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - sceneInteractor.showOverlay( - overlay = Overlays.QuickSettingsShade, - loggingReason = "test", - ) + sceneInteractor.showOverlay(Overlays.QuickSettingsShade, "test") assertThat(currentOverlays).contains(Overlays.QuickSettingsShade) underTest.onScrimClicked() assertThat(currentOverlays).doesNotContain(Overlays.QuickSettingsShade) } + + @Test + fun deviceLocked_hidesShade() = + testScope.runTest { + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + unlockDevice() + sceneInteractor.showOverlay(Overlays.QuickSettingsShade, "test") + assertThat(currentOverlays).contains(Overlays.QuickSettingsShade) + + lockDevice() + + assertThat(currentOverlays).isEmpty() + } + + @Test + fun bouncerShown_hidesShade() = + testScope.runTest { + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + lockDevice() + sceneInteractor.showOverlay(Overlays.QuickSettingsShade, "test") + assertThat(currentOverlays).contains(Overlays.QuickSettingsShade) + + sceneInteractor.changeScene(Scenes.Bouncer, "test") + runCurrent() + + assertThat(currentOverlays).doesNotContain(Overlays.QuickSettingsShade) + } + + @Test + fun shadeNotTouchable_hidesShade() = + testScope.runTest { + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + val isShadeTouchable by collectLastValue(kosmos.shadeInteractor.isShadeTouchable) + assertThat(isShadeTouchable).isTrue() + sceneInteractor.showOverlay(Overlays.QuickSettingsShade, "test") + assertThat(currentOverlays).contains(Overlays.QuickSettingsShade) + + lockDevice() + assertThat(isShadeTouchable).isFalse() + assertThat(currentOverlays).doesNotContain(Overlays.QuickSettingsShade) + } + + private fun TestScope.lockDevice() { + val currentScene by collectLastValue(sceneInteractor.currentScene) + kosmos.powerInteractor.setAsleepForTest() + runCurrent() + + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + } + + private suspend fun TestScope.unlockDevice() { + val currentScene by collectLastValue(sceneInteractor.currentScene) + kosmos.powerInteractor.setAwakeForTest() + runCurrent() + assertThat( + kosmos.authenticationInteractor.authenticate( + FakeAuthenticationRepository.DEFAULT_PIN + ) + ) + .isEqualTo(AuthenticationResult.SUCCEEDED) + + assertThat(currentScene).isEqualTo(Scenes.Gone) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt index 57cfe1b9e902..3e5dee69c85c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt @@ -47,7 +47,7 @@ import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) -class IssueRecordingServiceCommandHandlerTest : SysuiTestCase() { +class IssueRecordingServiceSessionTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } private val bgExecutor = kosmos.fakeExecutor @@ -61,13 +61,13 @@ class IssueRecordingServiceCommandHandlerTest : SysuiTestCase() { private val notificationManager = mock<NotificationManager>() private val panelInteractor = mock<PanelInteractor>() - private lateinit var underTest: IssueRecordingServiceCommandHandler + private lateinit var underTest: IssueRecordingServiceSession @Before fun setup() { traceurMessageSender = mock<TraceurMessageSender>() underTest = - IssueRecordingServiceCommandHandler( + IssueRecordingServiceSession( bgExecutor, dialogTransitionAnimator, panelInteractor, @@ -75,13 +75,13 @@ class IssueRecordingServiceCommandHandlerTest : SysuiTestCase() { issueRecordingState, iActivityManager, notificationManager, - userContextProvider + userContextProvider, ) } @Test fun startsTracing_afterReceivingActionStartCommand() { - underTest.handleStartCommand() + underTest.start() bgExecutor.runAllReady() Truth.assertThat(issueRecordingState.isRecording).isTrue() @@ -90,7 +90,7 @@ class IssueRecordingServiceCommandHandlerTest : SysuiTestCase() { @Test fun stopsTracing_afterReceivingStopTracingCommand() { - underTest.handleStopCommand(mContext.contentResolver) + underTest.stop(mContext.contentResolver) bgExecutor.runAllReady() Truth.assertThat(issueRecordingState.isRecording).isFalse() @@ -99,7 +99,7 @@ class IssueRecordingServiceCommandHandlerTest : SysuiTestCase() { @Test fun cancelsNotification_afterReceivingShareCommand() { - underTest.handleShareCommand(0, null, mContext) + underTest.share(0, null, mContext) bgExecutor.runAllReady() verify(notificationManager).cancelAsUser(isNull(), anyInt(), any<UserHandle>()) @@ -110,7 +110,7 @@ class IssueRecordingServiceCommandHandlerTest : SysuiTestCase() { issueRecordingState.takeBugreport = true val uri = mock<Uri>() - underTest.handleShareCommand(0, uri, mContext) + underTest.share(0, uri, mContext) bgExecutor.runAllReady() verify(iActivityManager).requestBugReportWithExtraAttachment(uri) @@ -121,7 +121,7 @@ class IssueRecordingServiceCommandHandlerTest : SysuiTestCase() { issueRecordingState.takeBugreport = false val uri = mock<Uri>() - underTest.handleShareCommand(0, uri, mContext) + underTest.share(0, uri, mContext) bgExecutor.runAllReady() verify(traceurMessageSender).shareTraces(mContext, uri) @@ -131,7 +131,7 @@ class IssueRecordingServiceCommandHandlerTest : SysuiTestCase() { fun closesShade_afterReceivingShareCommand() { val uri = mock<Uri>() - underTest.handleShareCommand(0, uri, mContext) + underTest.share(0, uri, mContext) bgExecutor.runAllReady() verify(panelInteractor).collapsePanels() 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 a0cafcbd5ad1..c9e958dd1cc0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -341,7 +341,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { bouncerActionButton?.onClick?.invoke() runCurrent() - // TODO(b/298026988): Assert that an activity was started once we use ActivityStarter. + // TODO(b/369765704): Assert that an activity was started once we use ActivityStarter. } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt index d163abf66b05..19ac0cf40160 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt @@ -287,7 +287,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun anyExpansion_shadeGreater() = - testScope.runTest() { + testScope.runTest { // WHEN shade is more expanded than QS shadeTestUtil.setShadeAndQsExpansion(.5f, 0f) runCurrent() @@ -298,7 +298,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun anyExpansion_qsGreater() = - testScope.runTest() { + testScope.runTest { // WHEN qs is more expanded than shade shadeTestUtil.setShadeAndQsExpansion(0f, .5f) runCurrent() @@ -308,6 +308,36 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + fun isShadeAnyExpanded_shadeCollapsed() = + testScope.runTest { + val isShadeAnyExpanded by collectLastValue(underTest.isShadeAnyExpanded) + shadeTestUtil.setShadeExpansion(0f) + runCurrent() + + assertThat(isShadeAnyExpanded).isFalse() + } + + @Test + fun isShadeAnyExpanded_shadePartiallyExpanded() = + testScope.runTest { + val isShadeAnyExpanded by collectLastValue(underTest.isShadeAnyExpanded) + shadeTestUtil.setShadeExpansion(0.01f) + runCurrent() + + assertThat(isShadeAnyExpanded).isTrue() + } + + @Test + fun isShadeAnyExpanded_shadeFullyExpanded() = + testScope.runTest { + val isShadeAnyExpanded by collectLastValue(underTest.isShadeAnyExpanded) + shadeTestUtil.setShadeExpansion(1f) + runCurrent() + + assertThat(isShadeAnyExpanded).isTrue() + } + + @Test fun isShadeTouchable_isFalse_whenDeviceAsleepAndNotPulsing() = testScope.runTest { powerRepository.updateWakefulness( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt index 109cd05368e6..4592b60e7c2c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt @@ -35,6 +35,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith @@ -382,4 +383,44 @@ class ShadeInteractorLegacyImplTest : SysuiTestCase() { // THEN user is not interacting assertThat(actual).isFalse() } + + @Test + fun expandNotificationsShade_unsupported() = + testScope.runTest { + assertThrows(UnsupportedOperationException::class.java) { + underTest.expandNotificationsShade("reason") + } + } + + @Test + fun expandQuickSettingsShade_unsupported() = + testScope.runTest { + assertThrows(UnsupportedOperationException::class.java) { + underTest.expandQuickSettingsShade("reason") + } + } + + @Test + fun collapseNotificationsShade_unsupported() = + testScope.runTest { + assertThrows(UnsupportedOperationException::class.java) { + underTest.collapseNotificationsShade("reason") + } + } + + @Test + fun collapseQuickSettingsShade_unsupported() = + testScope.runTest { + assertThrows(UnsupportedOperationException::class.java) { + underTest.collapseQuickSettingsShade("reason") + } + } + + @Test + fun collapseEitherShade_unsupported() = + testScope.runTest { + assertThrows(UnsupportedOperationException::class.java) { + underTest.collapseEitherShade("reason") + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt index f6fe667ff813..eb8ea8ba15cf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt @@ -1,12 +1,15 @@ package com.android.systemui.shade.ui.viewmodel import android.content.Intent +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.provider.AlarmClock import android.provider.Settings import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET 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.OverlayKey import com.android.compose.animation.scene.SceneKey import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -18,7 +21,9 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.plugins.activityStarter import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobileIconsInteractor import com.android.systemui.testKosmos @@ -48,7 +53,7 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { private val sceneInteractor = kosmos.sceneInteractor private val deviceEntryInteractor = kosmos.deviceEntryInteractor - private val underTest: ShadeHeaderViewModel = kosmos.shadeHeaderViewModel + private val underTest by lazy { kosmos.shadeHeaderViewModel } @Before fun setUp() { @@ -96,6 +101,7 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(DualShade.FLAG_NAME) fun onSystemIconContainerClicked_locked_collapsesShadeToLockscreen() = testScope.runTest { setDeviceEntered(false) @@ -108,6 +114,25 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(DualShade.FLAG_NAME) + fun onSystemIconContainerClicked_lockedOnDualShade_collapsesShadeToLockscreen() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + setDeviceEntered(false) + setScene(Scenes.Lockscreen) + setOverlay(Overlays.NotificationsShade) + assertThat(currentOverlays).isNotEmpty() + + underTest.onSystemIconContainerClicked() + runCurrent() + + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + assertThat(currentOverlays).isEmpty() + } + + @Test + @DisableFlags(DualShade.FLAG_NAME) fun onSystemIconContainerClicked_unlocked_collapsesShadeToGone() = testScope.runTest { setDeviceEntered(true) @@ -119,6 +144,24 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone) } + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun onSystemIconContainerClicked_unlockedOnDualShade_collapsesShadeToGone() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + setDeviceEntered(true) + setScene(Scenes.Gone) + setOverlay(Overlays.NotificationsShade) + assertThat(currentOverlays).isNotEmpty() + + underTest.onSystemIconContainerClicked() + runCurrent() + + assertThat(currentScene).isEqualTo(Scenes.Gone) + assertThat(currentOverlays).isEmpty() + } + companion object { private val SUB_1 = SubscriptionModel( @@ -144,6 +187,17 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { testScope.runCurrent() } + private fun setOverlay(key: OverlayKey) { + val currentOverlays = sceneInteractor.currentOverlays.value + key + sceneInteractor.showOverlay(key, "test") + sceneInteractor.setTransitionState( + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(sceneInteractor.currentScene.value, currentOverlays) + ) + ) + testScope.runCurrent() + } + private fun TestScope.setDeviceEntered(isEntered: Boolean) { if (isEntered) { // Unlock the device marking the device has entered. diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt index 7b87aeb60c13..d772e3effbeb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt @@ -42,7 +42,8 @@ import com.android.systemui.statusbar.notification.collection.modifyEntry import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.domain.interactor.lockScreenNotificationMinimalismSetting +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.testKosmos import com.android.systemui.util.settings.FakeSettings @@ -66,7 +67,7 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) -@EnableFlags(NotificationMinimalismPrototype.FLAG_NAME) +@EnableFlags(NotificationMinimalism.FLAG_NAME) class LockScreenMinimalismCoordinatorTest : SysuiTestCase() { private val kosmos = @@ -76,7 +77,7 @@ class LockScreenMinimalismCoordinatorTest : SysuiTestCase() { mock<SysuiStatusBarStateController>().also { mock -> doAnswer { statusBarState.ordinal }.whenever(mock).state } - fakeSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1) + lockScreenNotificationMinimalismSetting = true } private val notifPipeline: NotifPipeline = mock() private var statusBarState: StatusBarState = StatusBarState.KEYGUARD @@ -193,7 +194,7 @@ class LockScreenMinimalismCoordinatorTest : SysuiTestCase() { kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse() assertThat(promoter.shouldPromoteToTopLevel(child2)) - .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen) + .isEqualTo(NotificationMinimalism.ungroupTopUnseen) assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse() assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse() @@ -201,7 +202,7 @@ class LockScreenMinimalismCoordinatorTest : SysuiTestCase() { kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue() assertThat(promoter.shouldPromoteToTopLevel(child2)) - .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen) + .isEqualTo(NotificationMinimalism.ungroupTopUnseen) assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse() assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt index 2159b864d2a2..ea2e25e8eb1c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt @@ -21,7 +21,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest @@ -57,7 +57,7 @@ class SeenNotificationsInteractorTest : SysuiTestCase() { } @Test - @EnableFlags(NotificationMinimalismPrototype.FLAG_NAME) + @EnableFlags(NotificationMinimalism.FLAG_NAME) fun topOngoingAndUnseenNotification() = runTest { val entry1 = NotificationEntryBuilder().setTag("entry1").build() val entry2 = NotificationEntryBuilder().setTag("entry2").build() @@ -91,4 +91,17 @@ class SeenNotificationsInteractorTest : SysuiTestCase() { testScheduler.runCurrent() assertThat(settingEnabled).isTrue() } + + fun testLockScreenNotificationMinimalismSetting() = runTest { + val settingEnabled by + collectLastValue(underTest.isLockScreenNotificationMinimalismEnabled()) + + kosmos.lockScreenNotificationMinimalismSetting = false + testScheduler.runCurrent() + assertThat(settingEnabled).isFalse() + + kosmos.lockScreenNotificationMinimalismSetting = true + testScheduler.runCurrent() + assertThat(settingEnabled).isTrue() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt new file mode 100644 index 000000000000..28857a08c2bd --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt @@ -0,0 +1,290 @@ +/* + * 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.emptyshade.ui.viewmodel + +import android.app.Flags +import android.app.NotificationManager.Policy +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization +import android.provider.Settings +import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST +import androidx.test.filters.SmallTest +import com.android.settingslib.notification.data.repository.updateNotificationPolicy +import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.andSceneContainer +import com.android.systemui.kosmos.testScope +import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRepository +import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository +import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix +import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor +import com.android.systemui.statusbar.policy.data.repository.zenModeRepository +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 +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(ParameterizedAndroidJunit4::class) +@SmallTest +@EnableFlags(FooterViewRefactor.FLAG_NAME) +class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val zenModeRepository = kosmos.zenModeRepository + private val activeNotificationListRepository = kosmos.activeNotificationListRepository + private val fakeSecureSettingsRepository = kosmos.fakeSecureSettingsRepository + + private val underTest = kosmos.emptyShadeViewModel + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf().andSceneContainer() + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + + @Test + fun areNotificationsHiddenInShade_true() = + testScope.runTest { + val hidden by collectLastValue(underTest.areNotificationsHiddenInShade) + + zenModeRepository.updateNotificationPolicy( + suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST + ) + zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) + runCurrent() + + assertThat(hidden).isTrue() + } + + @Test + fun areNotificationsHiddenInShade_false() = + testScope.runTest { + val hidden by collectLastValue(underTest.areNotificationsHiddenInShade) + + zenModeRepository.updateNotificationPolicy( + suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST + ) + zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF) + runCurrent() + + assertThat(hidden).isFalse() + } + + @Test + fun hasFilteredOutSeenNotifications_true() = + testScope.runTest { + val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications) + + activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true + runCurrent() + + assertThat(hasFilteredNotifs).isTrue() + } + + @Test + fun hasFilteredOutSeenNotifications_false() = + testScope.runTest { + val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications) + + activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false + runCurrent() + + assertThat(hasFilteredNotifs).isFalse() + } + + @Test + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME) + @DisableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) + fun text_changesWhenNotifsHiddenInShade() = + testScope.runTest { + val text by collectLastValue(underTest.text) + + zenModeRepository.updateNotificationPolicy( + suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST + ) + zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF) + runCurrent() + + assertThat(text).isEqualTo("No notifications") + + zenModeRepository.updateNotificationPolicy( + suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST + ) + zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) + runCurrent() + + assertThat(text).isEqualTo("Notifications paused by Do Not Disturb") + } + + @Test + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) + fun text_reflectsModesHidingNotifications() = + testScope.runTest { + val text by collectLastValue(underTest.text) + + assertThat(text).isEqualTo("No notifications") + + zenModeRepository.addMode( + TestModeBuilder() + .setId("Do not disturb") + .setName("Do not disturb") + .setActive(true) + .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false) + .build() + ) + runCurrent() + assertThat(text).isEqualTo("Notifications paused by Do not disturb") + + zenModeRepository.addMode( + TestModeBuilder() + .setId("Work") + .setName("Work") + .setActive(true) + .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false) + .build() + ) + runCurrent() + assertThat(text).isEqualTo("Notifications paused by Do not disturb and one other mode") + + zenModeRepository.addMode( + TestModeBuilder() + .setId("Gym") + .setName("Gym") + .setActive(true) + .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false) + .build() + ) + runCurrent() + assertThat(text).isEqualTo("Notifications paused by Do not disturb and 2 other modes") + + zenModeRepository.deactivateMode("Do not disturb") + zenModeRepository.deactivateMode("Work") + runCurrent() + assertThat(text).isEqualTo("Notifications paused by Gym") + } + + @Test + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME) + fun footer_isVisibleWhenSeenNotifsAreFilteredOut() = + testScope.runTest { + val footerVisible by collectLastValue(underTest.footer.isVisible) + + activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false + runCurrent() + + assertThat(footerVisible).isFalse() + + activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true + runCurrent() + + assertThat(footerVisible).isTrue() + } + + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME) + @Test + fun onClick_whenHistoryDisabled_leadsToSettingsPage() = + testScope.runTest { + val onClick by collectLastValue(underTest.onClick) + runCurrent() + + fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0) + + assertThat(onClick?.targetIntent?.action) + .isEqualTo(Settings.ACTION_NOTIFICATION_SETTINGS) + assertThat(onClick?.backStack).isEmpty() + } + + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME) + @Test + fun onClick_whenHistoryEnabled_leadsToHistoryPage() = + testScope.runTest { + val onClick by collectLastValue(underTest.onClick) + runCurrent() + + fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 1) + + assertThat(onClick?.targetIntent?.action) + .isEqualTo(Settings.ACTION_NOTIFICATION_HISTORY) + assertThat(onClick?.backStack?.map { it.action }) + .containsExactly(Settings.ACTION_NOTIFICATION_SETTINGS) + } + + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME) + @Test + fun onClick_whenOneModeHidingNotifications_leadsToModeSettings() = + testScope.runTest { + val onClick by collectLastValue(underTest.onClick) + runCurrent() + + zenModeRepository.addMode( + TestModeBuilder() + .setId("ID") + .setActive(true) + .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false) + .build() + ) + runCurrent() + + assertThat(onClick?.targetIntent?.action) + .isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS) + assertThat( + onClick?.targetIntent?.extras?.getString(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID) + ) + .isEqualTo("ID") + assertThat(onClick?.backStack?.map { it.action }) + .containsExactly(Settings.ACTION_ZEN_MODE_SETTINGS) + } + + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME) + @Test + fun onClick_whenMultipleModesHidingNotifications_leadsToGeneralModesSettings() = + testScope.runTest { + val onClick by collectLastValue(underTest.onClick) + runCurrent() + + zenModeRepository.addMode( + TestModeBuilder() + .setActive(true) + .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false) + .build() + ) + zenModeRepository.addMode( + TestModeBuilder() + .setActive(true) + .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false) + .build() + ) + runCurrent() + + assertThat(onClick?.targetIntent?.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS) + assertThat(onClick?.backStack).isEmpty() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt index 26e1a4d9e961..d12d6f6b885d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt @@ -18,12 +18,9 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel -import android.app.NotificationManager.Policy import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization -import android.provider.Settings import androidx.test.filters.SmallTest -import com.android.settingslib.notification.data.repository.updateNotificationPolicy import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.DisableSceneContainer @@ -46,7 +43,6 @@ import com.android.systemui.statusbar.notification.data.repository.setActiveNoti import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository -import com.android.systemui.statusbar.policy.data.repository.zenModeRepository import com.android.systemui.statusbar.policy.fakeConfigurationController import com.android.systemui.testKosmos import com.android.systemui.util.ui.isAnimating @@ -79,7 +75,6 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas private val fakeRemoteInputRepository = kosmos.fakeRemoteInputRepository private val fakeUserSetupRepository = kosmos.fakeUserSetupRepository private val headsUpRepository = kosmos.headsUpNotificationRepository - private val zenModeRepository = kosmos.zenModeRepository private val shadeTestUtil by lazy { kosmos.shadeTestUtil } @@ -266,56 +261,6 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas } @Test - fun areNotificationsHiddenInShade_true() = - testScope.runTest { - val hidden by collectLastValue(underTest.areNotificationsHiddenInShade) - - zenModeRepository.updateNotificationPolicy( - suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST - ) - zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) - runCurrent() - - assertThat(hidden).isTrue() - } - - @Test - fun areNotificationsHiddenInShade_false() = - testScope.runTest { - val hidden by collectLastValue(underTest.areNotificationsHiddenInShade) - - zenModeRepository.updateNotificationPolicy( - suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST - ) - zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF) - runCurrent() - - assertThat(hidden).isFalse() - } - - @Test - fun hasFilteredOutSeenNotifications_true() = - testScope.runTest { - val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications) - - activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true - runCurrent() - - assertThat(hasFilteredNotifs).isTrue() - } - - @Test - fun hasFilteredOutSeenNotifications_false() = - testScope.runTest { - val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications) - - activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false - runCurrent() - - assertThat(hasFilteredNotifs).isFalse() - } - - @Test fun shouldIncludeFooterView_trueWhenShade() = testScope.runTest { val shouldIncludeFooterView by collectFooterViewVisibility() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt index 0f6dc0723f42..c5ccf9e6a1d1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt @@ -25,6 +25,7 @@ import android.provider.Settings.Secure.ZEN_DURATION import android.provider.Settings.Secure.ZEN_DURATION_FOREVER import android.provider.Settings.Secure.ZEN_DURATION_PROMPT import android.service.notification.SystemZenRules +import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.R @@ -34,6 +35,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.shared.settings.data.repository.secureSettingsRepository +import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository import com.android.systemui.testKosmos @@ -379,4 +381,46 @@ class ZenModeInteractorTest : SysuiTestCase() { assertThat(dndMode!!.isActive).isTrue() } + + @Test + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) + fun modesHidingNotifications_onlyIncludesModesWithNotifListSuppression() = + testScope.runTest { + val modesHidingNotifications by collectLastValue(underTest.modesHidingNotifications) + + zenModeRepository.addModes( + listOf( + TestModeBuilder() + .setName("Not active, no list suppression") + .setActive(false) + .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ true) + .build(), + TestModeBuilder() + .setName("Not active, has list suppression") + .setActive(false) + .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false) + .build(), + TestModeBuilder() + .setName("No list suppression") + .setActive(true) + .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ true) + .build(), + TestModeBuilder() + .setName("Has list suppression 1") + .setActive(true) + .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false) + .build(), + TestModeBuilder() + .setName("Has list suppression 2") + .setActive(true) + .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false) + .build(), + ) + ) + runCurrent() + + assertThat(modesHidingNotifications?.map { it.name }) + .containsExactly("Has list suppression 1", "Has list suppression 2") + .inOrder() + } } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java index be44dee0aae6..73626b457dcf 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java @@ -184,7 +184,10 @@ public interface QSTile { } } - /** Get the text for secondaryLabel. */ + /** + * If the current secondaryLabel value is not empty, ignore the given input and return + * the current value. Otherwise return current value. + */ public CharSequence getSecondaryLabel(CharSequence stateText) { // Use a local reference as the value might change from other threads CharSequence localSecondaryLabel = secondaryLabel; diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index e94248dc72ce..629c94f9a044 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -2047,4 +2047,6 @@ <!-- SliceView icon size --> <dimen name="abc_slice_big_pic_min_height">64dp</dimen> <dimen name="abc_slice_big_pic_max_height">64dp</dimen> + + <dimen name="contextual_edu_dialog_bottom_margin">70dp</dimen> </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 24b657943e37..c76b35f0cf9b 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1473,6 +1473,16 @@ <!-- The text to show in the notifications shade when dnd is suppressing notifications. [CHAR LIMIT=100] --> <string name="dnd_suppressing_shade_text">Notifications paused by Do Not Disturb</string> + <!-- The text to show in the notifications shade when a mode is suppressing notifications. [CHAR LIMIT=100] --> + <string name="modes_suppressing_shade_text"> + {count, plural, offset:1 + =0 {No notifications} + =1 {Notifications paused by {mode}} + =2 {Notifications paused by {mode} and one other mode} + other {Notifications paused by {mode} and # other modes} + } + </string> + <!-- Media projection permission dialog action text. [CHAR LIMIT=60] --> <string name="media_projection_action_text">Start now</string> @@ -3808,6 +3818,8 @@ Action + ESC for this.</string> <!-- Main text of the one line view of a redacted notification --> <string name="redacted_notification_single_line_text">Unlock to view</string> + <!-- Content description for contextual education dialog [CHAR LIMIT=NONE] --> + <string name="contextual_education_dialog_title">Contextual education</string> <!-- Education notification title for Back [CHAR_LIMIT=100] --> <string name="back_edu_notification_title">Use your touchpad to go back</string> <!-- Education notification text for Back [CHAR_LIMIT=100] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index a02c35461031..b34d6e4067b6 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -1720,4 +1720,10 @@ <style name="ShortcutHelperTheme" parent="@style/ShortcutHelperThemeCommon"> <item name="android:windowLightNavigationBar">true</item> </style> + + <style name="ContextualEduDialog" parent="@android:style/Theme.DeviceDefault.Dialog.NoActionBar"> + <!-- To make the dialog wrap to content when the education text is short --> + <item name="windowMinWidthMajor">0%</item> + <item name="windowMinWidthMinor">0%</item> + </style> </resources> diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp index 8f55961af4e9..0f1da509468a 100644 --- a/packages/SystemUI/shared/Android.bp +++ b/packages/SystemUI/shared/Android.bp @@ -70,6 +70,7 @@ android_library { "jsr330", "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", "//frameworks/libs/systemui:msdl", + "//frameworks/libs/systemui:view_capture", ], resource_dirs: [ "res", diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java index f358ba2d3ccd..4db6ab6ea579 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java @@ -16,6 +16,9 @@ package com.android.systemui.shared.rotation; +import static com.android.app.viewcapture.ViewCaptureFactory.getViewCaptureAwareWindowManagerInstance; +import static com.android.systemui.Flags.enableViewCaptureTracing; + import android.annotation.DimenRes; import android.annotation.IdRes; import android.annotation.LayoutRes; @@ -30,7 +33,6 @@ import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.FrameLayout; @@ -38,6 +40,7 @@ import android.widget.FrameLayout; import androidx.annotation.BoolRes; import androidx.core.view.OneShotPreDrawListener; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.shared.rotation.FloatingRotationButtonPositionCalculator.Position; /** @@ -47,7 +50,7 @@ public class FloatingRotationButton implements RotationButton { private static final int MARGIN_ANIMATION_DURATION_MILLIS = 300; - private final WindowManager mWindowManager; + private final ViewCaptureAwareWindowManager mWindowManager; private final ViewGroup mKeyButtonContainer; private final FloatingRotationButtonView mKeyButtonView; @@ -88,7 +91,8 @@ public class FloatingRotationButton implements RotationButton { @DimenRes int taskbarBottomMargin, @DimenRes int buttonDiameter, @DimenRes int rippleMaxWidth, @BoolRes int floatingRotationBtnPositionLeftResource) { mContext = context; - mWindowManager = mContext.getSystemService(WindowManager.class); + mWindowManager = getViewCaptureAwareWindowManagerInstance(mContext, + enableViewCaptureTracing()); mKeyButtonContainer = (ViewGroup) LayoutInflater.from(mContext).inflate(layout, null); mKeyButtonView = mKeyButtonContainer.findViewById(keyButtonId); mKeyButtonView.setVisibility(View.VISIBLE); diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java index 76df9c96c801..fb00d6e16dcc 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java @@ -75,6 +75,9 @@ import javax.inject.Named; * touches are consumed. */ public class TouchMonitor { + // An incrementing id used to identify the touch monitor instance. + private static int sNextInstanceId = 0; + private final Logger mLogger; // This executor is used to protect {@code mActiveTouchSessions} from being modified // concurrently. Any operation that adds or removes values should use this executor. @@ -138,7 +141,7 @@ public class TouchMonitor { completer.set(predecessor); } - if (mActiveTouchSessions.isEmpty()) { + if (mActiveTouchSessions.isEmpty() && mInitialized) { if (mStopMonitoringPending) { stopMonitoring(false); } else { @@ -271,7 +274,7 @@ public class TouchMonitor { @Override public void onDestroy(LifecycleOwner owner) { - stopMonitoring(true); + destroy(); } }; @@ -279,6 +282,11 @@ public class TouchMonitor { * When invoked, instantiates a new {@link InputSession} to monitor touch events. */ private void startMonitoring() { + if (!mInitialized) { + mLogger.w("attempting to startMonitoring when not initialized"); + return; + } + mLogger.i("startMonitoring(): monitoring started"); stopMonitoring(true); @@ -587,7 +595,7 @@ public class TouchMonitor { mDisplayHelper = displayHelper; mWindowManagerService = windowManagerService; mConfigurationInteractor = configurationInteractor; - mLoggingName = loggingName + ":TouchMonitor"; + mLoggingName = loggingName + ":TouchMonitor[" + sNextInstanceId++ + "]"; mLogger = new Logger(logBuffer, mLoggingName); } @@ -613,7 +621,8 @@ public class TouchMonitor { */ public void destroy() { if (!mInitialized) { - throw new IllegalStateException("TouchMonitor not initialized"); + // In the case that we've already been destroyed, this is a no-op + return; } stopMonitoring(true); diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt index 8b5a09b3d9fd..2c026c0bb5ce 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt @@ -103,7 +103,7 @@ constructor( prepareToPerformAction() returnToCall() }, - onLongClick = null + onLongClick = null, ) } @@ -115,15 +115,15 @@ constructor( dozeLogger.logEmergencyCall() startEmergencyDialerActivity() }, - // TODO(b/308001302): The long click detector doesn't work properly, investigate. + // TODO(b/369767936): The long click detector doesn't work properly, investigate. onLongClick = { if (emergencyAffordanceManager.needsEmergencyAffordance()) { prepareToPerformAction() - // TODO(b/298026988): Check that !longPressWasDragged before invoking. + // TODO(b/369767936): Check that !longPressWasDragged before invoking. emergencyAffordanceManager.performEmergencyCall() } - } + }, ) } @@ -143,7 +143,7 @@ constructor( applicationContext.startActivityAsUser( this, ActivityOptions.makeCustomAnimation(applicationContext, 0, 0).toBundle(), - UserHandle(selectedUserInteractor.getSelectedUserId()) + UserHandle(selectedUserInteractor.getSelectedUserId()), ) } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt index b8c30fe9d4a8..d6b92115c64b 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt @@ -69,7 +69,7 @@ class BouncerHapticPlayer @Inject constructor(private val msdlPlayer: dagger.Laz HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING, ) } else { - msdlPlayer.get().playToken(MSDLToken.DRAG_INDICATOR) + msdlPlayer.get().playToken(MSDLToken.DRAG_INDICATOR_DISCRETE) } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index cbea87676d3a..8da4d460b7a5 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -30,7 +30,7 @@ import com.android.systemui.dreams.AssistantAttentionMonitor import com.android.systemui.dreams.DreamMonitor import com.android.systemui.dreams.homecontrols.HomeControlsDreamStartable import com.android.systemui.globalactions.GlobalActionsComponent -import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialCoreStartable +import com.android.systemui.haptics.msdl.MSDLCoreStartable import com.android.systemui.keyboard.KeyboardUI import com.android.systemui.keyboard.PhysicalKeyboardCoreStartable import com.android.systemui.keyguard.KeyguardViewConfigurator @@ -323,4 +323,9 @@ abstract class SystemUICoreStartableModule { @IntoMap @ClassKey(BatteryControllerStartable::class) abstract fun bindsBatteryControllerStartable(impl: BatteryControllerStartable): CoreStartable + + @Binds + @IntoMap + @ClassKey(MSDLCoreStartable::class) + abstract fun bindMSDLCoreStartable(impl: MSDLCoreStartable): CoreStartable } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index 113e0011f5bd..83f86a718029 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -65,6 +65,7 @@ import com.android.systemui.dreams.dagger.DreamOverlayComponent; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.navigationbar.gestural.domain.GestureInteractor; import com.android.systemui.navigationbar.gestural.domain.TaskMatcher; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.touch.TouchInsetManager; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -499,8 +500,11 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mDreamOverlayContainerViewController = dreamOverlayComponent.getDreamOverlayContainerViewController(); - mTouchMonitor = ambientTouchComponent.getTouchMonitor(); - mTouchMonitor.init(); + + if (!SceneContainerFlag.isEnabled()) { + mTouchMonitor = ambientTouchComponent.getTouchMonitor(); + mTouchMonitor.init(); + } mStateController.setShouldShowComplications(shouldShowComplications()); diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduDialog.kt b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduDialog.kt new file mode 100644 index 000000000000..287e85ca4358 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduDialog.kt @@ -0,0 +1,64 @@ +/* + * 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.systemui.education.ui.view + +import android.app.AlertDialog +import android.content.Context +import android.os.Bundle +import android.view.Gravity +import android.view.WindowManager +import android.widget.ToastPresenter +import com.android.systemui.education.ui.viewmodel.ContextualEduToastViewModel +import com.android.systemui.res.R + +class ContextualEduDialog(context: Context, private val model: ContextualEduToastViewModel) : + AlertDialog(context, R.style.ContextualEduDialog) { + override fun onCreate(savedInstanceState: Bundle?) { + setUpWindowProperties() + setWindowPosition() + // title is used for a11y announcement + window?.setTitle(context.getString(R.string.contextual_education_dialog_title)) + // TODO: b/369791926 - replace the below toast view with a custom dialog view + val toastView = ToastPresenter.getTextToastView(context, model.message) + setView(toastView) + super.onCreate(savedInstanceState) + } + + private fun setUpWindowProperties() { + window?.apply { + setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG) + clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + } + setCanceledOnTouchOutside(false) + } + + private fun setWindowPosition() { + window?.apply { + setGravity(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL) + this.attributes = + WindowManager.LayoutParams().apply { + width = WindowManager.LayoutParams.WRAP_CONTENT + height = WindowManager.LayoutParams.WRAP_CONTENT + copyFrom(attributes) + y = + context.resources.getDimensionPixelSize( + R.dimen.contextual_edu_dialog_bottom_margin + ) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt index e62b26bfe53d..913ecdd4aadc 100644 --- a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt @@ -16,6 +16,7 @@ package com.android.systemui.education.ui.view +import android.app.Dialog import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager @@ -24,7 +25,6 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.os.UserHandle -import android.widget.Toast import androidx.core.app.NotificationCompat import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton @@ -49,7 +49,7 @@ constructor( private val viewModel: ContextualEduViewModel, private val context: Context, private val notificationManager: NotificationManager, - private val createToast: (String) -> Toast + private val createDialog: (ContextualEduToastViewModel) -> Dialog, ) : CoreStartable { companion object { @@ -69,16 +69,23 @@ constructor( viewModel, context, notificationManager, - createToast = { message -> Toast.makeText(context, message, Toast.LENGTH_LONG) } + createDialog = { model -> ContextualEduDialog(context, model) }, ) + var dialog: Dialog? = null + override fun start() { createEduNotificationChannel() applicationScope.launch { viewModel.eduContent.collect { contentModel -> - when (contentModel) { - is ContextualEduToastViewModel -> showToast(contentModel) - is ContextualEduNotificationViewModel -> showNotification(contentModel) + if (contentModel != null) { + when (contentModel) { + is ContextualEduToastViewModel -> showDialog(contentModel) + is ContextualEduNotificationViewModel -> showNotification(contentModel) + } + } else { + dialog?.dismiss() + dialog = null } } } @@ -95,9 +102,9 @@ constructor( notificationManager.createNotificationChannel(channel) } - private fun showToast(model: ContextualEduToastViewModel) { - val toast = createToast(model.message) - toast.show() + private fun showDialog(model: ContextualEduToastViewModel) { + dialog = createDialog(model) + dialog?.show() } private fun showNotification(model: ContextualEduNotificationViewModel) { diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt index cd4a8ad8dbda..32e7f41f36b8 100644 --- a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.education.ui.viewmodel import android.content.res.Resources +import android.view.accessibility.AccessibilityManager import com.android.systemui.contextualeducation.GestureType.ALL_APPS import com.android.systemui.contextualeducation.GestureType.BACK import com.android.systemui.contextualeducation.GestureType.HOME @@ -27,23 +28,63 @@ import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInter import com.android.systemui.education.shared.model.EducationInfo import com.android.systemui.education.shared.model.EducationUiType import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper import javax.inject.Inject +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map +@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) @SysUISingleton class ContextualEduViewModel @Inject -constructor(@Main private val resources: Resources, interactor: KeyboardTouchpadEduInteractor) { - val eduContent: Flow<ContextualEduContentViewModel> = - interactor.educationTriggered.filterNotNull().map { - if (it.educationUiType == EducationUiType.Notification) { - ContextualEduNotificationViewModel(getEduTitle(it), getEduContent(it), it.userId) - } else { - ContextualEduToastViewModel(getEduContent(it), it.userId) +constructor( + @Main private val resources: Resources, + interactor: KeyboardTouchpadEduInteractor, + private val accessibilityManagerWrapper: AccessibilityManagerWrapper, +) { + + companion object { + const val DEFAULT_DIALOG_TIMEOUT_MILLIS = 3500 + } + + private val timeoutMillis: Long + get() = + accessibilityManagerWrapper + .getRecommendedTimeoutMillis( + DEFAULT_DIALOG_TIMEOUT_MILLIS, + AccessibilityManager.FLAG_CONTENT_TEXT, + ) + .toLong() + + val eduContent: Flow<ContextualEduContentViewModel?> = + interactor.educationTriggered + .filterNotNull() + .map { + if (it.educationUiType == EducationUiType.Notification) { + ContextualEduNotificationViewModel( + getEduTitle(it), + getEduContent(it), + it.userId, + ) + } else { + ContextualEduToastViewModel(getEduContent(it), it.userId) + } + } + .timeout(timeoutMillis, emitAfterTimeout = null) + + private fun <T> Flow<T>.timeout(timeoutMillis: Long, emitAfterTimeout: T): Flow<T> { + return flatMapLatest { + flow { + emit(it) + delay(timeoutMillis) + emit(emitAfterTimeout) } } + } private fun getEduContent(educationInfo: EducationInfo): String { val resourceId = diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index 0b775ab486bd..47f0ecfb237a 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -34,10 +34,14 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.shared.flag.DualShade +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.statusbar.core.StatusBarSimpleFragment import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag +import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix +import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection import javax.inject.Inject @@ -55,7 +59,9 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // Internal notification frontend dependencies NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token - NotificationMinimalismPrototype.token dependsOn NotificationThrottleHun.token + NotificationMinimalism.token dependsOn NotificationThrottleHun.token + ModesEmptyShadeFix.token dependsOn FooterViewRefactor.token + ModesEmptyShadeFix.token dependsOn modesUi // SceneContainer dependencies SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta } @@ -69,6 +75,8 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // Status bar chip dependencies statusBarCallChipNotificationIconToken dependsOn statusBarUseReposForCallChipToken statusBarCallChipNotificationIconToken dependsOn statusBarScreenSharingChipsToken + + StatusBarConnectedDisplays.token dependsOn StatusBarSimpleFragment.token } private inline val politeNotifications diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index 493afde96aff..aa1873c7ad41 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -2298,9 +2298,11 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene } @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, + public boolean onScroll(@Nullable MotionEvent e1, MotionEvent e2, + float distanceX, float distanceY) { if (distanceY < 0 && distanceY > distanceX + && e1 != null && e1.getY() <= mStatusBarWindowController.getStatusBarHeight()) { // Downwards scroll from top openShadeAndDismiss(); @@ -2310,9 +2312,11 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene } @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + public boolean onFling(@Nullable MotionEvent e1, MotionEvent e2, + float velocityX, float velocityY) { if (velocityY > 0 && Math.abs(velocityY) > Math.abs(velocityX) + && e1 != null && e1.getY() <= mStatusBarWindowController.getStatusBarHeight()) { // Downwards fling from top openShadeAndDismiss(); diff --git a/packages/SystemUI/src/com/android/systemui/haptics/msdl/MSDLCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/haptics/msdl/MSDLCoreStartable.kt new file mode 100644 index 000000000000..58736c608af3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/haptics/msdl/MSDLCoreStartable.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.haptics.msdl + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.google.android.msdl.domain.MSDLPlayer +import com.google.android.msdl.logging.MSDLHistoryLogger +import java.io.PrintWriter +import javax.inject.Inject + +@SysUISingleton +class MSDLCoreStartable @Inject constructor(private val msdlPlayer: MSDLPlayer) : CoreStartable { + override fun start() {} + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println("MSDLPlayer history of the last ${MSDLHistoryLogger.HISTORY_SIZE} events:") + msdlPlayer.getHistory().forEach { event -> pw.println("$event") } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialMetricsLogger.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialMetricsLogger.kt new file mode 100644 index 000000000000..144c5ead1bb8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialMetricsLogger.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.inputdevice.tutorial + +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_ENTRY_POINT_CONTEXTUAL_EDU +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_ENTRY_POINT_SCHEDULER +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEYBOARD +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_TOUCHPAD +import com.android.systemui.shared.system.SysUiStatsLog +import javax.inject.Inject + +class KeyboardTouchpadTutorialMetricsLogger @Inject constructor() { + + fun logPeripheralTutorialLaunched(entryPointExtra: String?, tutorialTypeExtra: String?) { + val entryPoint = + when (entryPointExtra) { + INTENT_TUTORIAL_ENTRY_POINT_SCHEDULER -> + SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__ENTRY_POINT__SCHEDULED + INTENT_TUTORIAL_ENTRY_POINT_CONTEXTUAL_EDU -> + SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__ENTRY_POINT__CONTEXTUAL_EDU + else -> SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__ENTRY_POINT__APP + } + + val tutorialType = + when (tutorialTypeExtra) { + INTENT_TUTORIAL_TYPE_KEYBOARD -> + SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__TUTORIAL_TYPE__KEYBOARD + INTENT_TUTORIAL_TYPE_TOUCHPAD -> + SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__TUTORIAL_TYPE__TOUCHPAD + else -> SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__TUTORIAL_TYPE__BOTH + } + + SysUiStatsLog.write(SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED, entryPoint, tutorialType) + } + + fun logPeripheralTutorialLaunchedFromSettings() { + val entryPoint = SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__ENTRY_POINT__SETTINGS + val tutorialType = SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED__TUTORIAL_TYPE__TOUCHPAD + SysUiStatsLog.write(SysUiStatsLog.PERIPHERAL_TUTORIAL_LAUNCHED, entryPoint, tutorialType) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt index 5d9dda3899cd..f2afaee1870b 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt @@ -31,6 +31,8 @@ import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSched import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor.Companion.TAG import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor.TutorialType import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_ENTRY_POINT_KEY +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_ENTRY_POINT_SCHEDULER import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_BOTH import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEY import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEYBOARD @@ -48,7 +50,7 @@ constructor( @Background private val backgroundScope: CoroutineScope, @Application private val context: Context, private val tutorialSchedulerInteractor: TutorialSchedulerInteractor, - private val notificationManager: NotificationManager + private val notificationManager: NotificationManager, ) { fun start() { backgroundScope.launch { @@ -68,7 +70,7 @@ constructor( val extras = Bundle() extras.putString( Notification.EXTRA_SUBSTITUTE_APP_NAME, - context.getString(com.android.internal.R.string.android_system_label) + context.getString(com.android.internal.R.string.android_system_label), ) val info = getNotificationInfo(tutorialType)!! @@ -91,7 +93,7 @@ constructor( NotificationChannel( CHANNEL_ID, context.getString(com.android.internal.R.string.android_system_label), - NotificationManager.IMPORTANCE_DEFAULT + NotificationManager.IMPORTANCE_DEFAULT, ) notificationManager.createNotificationChannel(channel) } @@ -100,13 +102,14 @@ constructor( val intent = Intent(context, KeyboardTouchpadTutorialActivity::class.java).apply { putExtra(INTENT_TUTORIAL_TYPE_KEY, tutorialType) + putExtra(INTENT_TUTORIAL_ENTRY_POINT_KEY, INTENT_TUTORIAL_ENTRY_POINT_SCHEDULER) flags = Intent.FLAG_ACTIVITY_NEW_TASK } return PendingIntent.getActivity( context, /* requestCode= */ 0, intent, - PendingIntent.FLAG_IMMUTABLE + PendingIntent.FLAG_IMMUTABLE, ) } @@ -118,13 +121,13 @@ constructor( NotificationInfo( context.getString(R.string.launch_keyboard_tutorial_notification_title), context.getString(R.string.launch_keyboard_tutorial_notification_content), - INTENT_TUTORIAL_TYPE_KEYBOARD + INTENT_TUTORIAL_TYPE_KEYBOARD, ) TutorialType.TOUCHPAD -> NotificationInfo( context.getString(R.string.launch_touchpad_tutorial_notification_title), context.getString(R.string.launch_touchpad_tutorial_notification_content), - INTENT_TUTORIAL_TYPE_TOUCHPAD + INTENT_TUTORIAL_TYPE_TOUCHPAD, ) TutorialType.BOTH -> NotificationInfo( @@ -134,7 +137,7 @@ constructor( context.getString( R.string.launch_keyboard_touchpad_tutorial_notification_content ), - INTENT_TUTORIAL_TYPE_BOTH + INTENT_TUTORIAL_TYPE_BOTH, ) TutorialType.NONE -> null } diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt index c130c6c7fe12..29febd32e925 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt @@ -30,6 +30,7 @@ import androidx.lifecycle.lifecycleScope import com.android.compose.theme.PlatformTheme import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger.TutorialContext +import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialMetricsLogger import com.android.systemui.inputdevice.tutorial.TouchpadTutorialScreensProvider import com.android.systemui.inputdevice.tutorial.ui.composable.ActionKeyTutorialScreen import com.android.systemui.inputdevice.tutorial.ui.viewmodel.KeyboardTouchpadTutorialViewModel @@ -51,6 +52,7 @@ constructor( private val viewModelFactoryAssistedProvider: ViewModelFactoryAssistedProvider, private val touchpadTutorialScreensProvider: Optional<TouchpadTutorialScreensProvider>, private val logger: InputDeviceTutorialLogger, + private val metricsLogger: KeyboardTouchpadTutorialMetricsLogger, ) : ComponentActivity() { companion object { @@ -58,6 +60,9 @@ constructor( const val INTENT_TUTORIAL_TYPE_TOUCHPAD = "touchpad" const val INTENT_TUTORIAL_TYPE_KEYBOARD = "keyboard" const val INTENT_TUTORIAL_TYPE_BOTH = "both" + const val INTENT_TUTORIAL_ENTRY_POINT_KEY = "entry_point" + const val INTENT_TUTORIAL_ENTRY_POINT_SCHEDULER = "scheduler" + const val INTENT_TUTORIAL_ENTRY_POINT_CONTEXTUAL_EDU = "contextual_edu" } private val vm by @@ -86,6 +91,10 @@ constructor( PlatformTheme { KeyboardTouchpadTutorialContainer(vm, touchpadTutorialScreensProvider) } } if (savedInstanceState == null) { + metricsLogger.logPeripheralTutorialLaunched( + intent.getStringExtra(INTENT_TUTORIAL_ENTRY_POINT_KEY), + intent.getStringExtra(INTENT_TUTORIAL_TYPE_KEY), + ) logger.logOpenTutorial(TutorialContext.KEYBOARD_TOUCHPAD_TUTORIAL) } } @@ -109,7 +118,7 @@ fun KeyboardTouchpadTutorialContainer( ACTION_KEY -> ActionKeyTutorialScreen( onDoneButtonClicked = vm::onDoneButtonClicked, - onBack = vm::onBack + onBack = vm::onBack, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt index b9a16c402e59..52263ce64a85 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyboard.shortcut.ui.view import android.content.ActivityNotFoundException import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.res.Configuration import android.os.Bundle import android.provider.Settings @@ -125,7 +126,7 @@ constructor(private val userTracker: UserTracker, private val viewModel: Shortcu private fun onKeyboardSettingsClicked() { try { startActivityAsUser( - Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS), + Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS).addFlags(FLAG_ACTIVITY_NEW_TASK), userTracker.userHandle, ) } catch (e: ActivityNotFoundException) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java index 68a252b2caba..654c76359505 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java @@ -515,7 +515,13 @@ public class KeyguardSliceProvider extends SliceProvider implements } protected void notifyChange() { - mBgHandler.post(() -> mContentResolver.notifyChange(mSliceUri, null /* observer */)); + mBgHandler.post(() -> { + try { + mContentResolver.notifyChange(mSliceUri, null /* observer */); + } catch (Exception e) { + Log.e(TAG, "Error on mContentResolver.notifyChange()", e); + } + }); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 416eabae78eb..063adc834f30 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -63,6 +63,7 @@ import com.android.systemui.shade.NotificationShadeWindowView import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.KeyguardIndicationController import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator @@ -102,6 +103,7 @@ constructor( private val keyguardClockViewModel: KeyguardClockViewModel, private val smartspaceViewModel: KeyguardSmartspaceViewModel, private val lockscreenContentViewModelFactory: LockscreenContentViewModel.Factory, + private val notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory, private val lockscreenSceneBlueprintsLazy: Lazy<Set<LockscreenSceneBlueprint>>, private val clockInteractor: KeyguardClockInteractor, private val keyguardViewMediator: KeyguardViewMediator, @@ -207,6 +209,7 @@ constructor( private fun createLockscreen( context: Context, viewModelFactory: LockscreenContentViewModel.Factory, + notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory, blueprints: Set<@JvmSuppressWildcards LockscreenSceneBlueprint>, ): View { val sceneBlueprints = @@ -222,6 +225,8 @@ constructor( with( LockscreenContent( viewModelFactory = viewModelFactory, + notificationScrimViewModelFactory = + notificationScrimViewModelFactory, blueprints = sceneBlueprints, clockInteractor = clockInteractor, ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 0a38ce07a798..9c7cc81c34aa 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -147,6 +147,7 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.SystemPropertiesHelper; import com.android.systemui.keyguard.dagger.KeyguardModule; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionBootInteractor; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.log.SessionTracker; import com.android.systemui.navigationbar.NavigationModeController; @@ -265,6 +266,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private static final int NOTIFY_STARTED_GOING_TO_SLEEP = 17; private static final int SYSTEM_READY = 18; private static final int CANCEL_KEYGUARD_EXIT_ANIM = 19; + private static final int BOOT_INTERACTOR = 20; /** Enum for reasons behind updating wakeAndUnlock state. */ @Retention(RetentionPolicy.SOURCE) @@ -1390,6 +1392,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private final DozeParameters mDozeParameters; private final SelectedUserInteractor mSelectedUserInteractor; private final KeyguardInteractor mKeyguardInteractor; + private final KeyguardTransitionBootInteractor mTransitionBootInteractor; @VisibleForTesting protected FoldGracePeriodProvider mFoldGracePeriodProvider = new FoldGracePeriodProvider(); @@ -1484,6 +1487,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager, SelectedUserInteractor selectedUserInteractor, KeyguardInteractor keyguardInteractor, + KeyguardTransitionBootInteractor transitionBootInteractor, WindowManagerOcclusionManager wmOcclusionManager) { mContext = context; mUserTracker = userTracker; @@ -1524,6 +1528,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mDozeParameters = dozeParameters; mSelectedUserInteractor = selectedUserInteractor; mKeyguardInteractor = keyguardInteractor; + mTransitionBootInteractor = transitionBootInteractor; mStatusBarStateController = statusBarStateController; statusBarStateController.addCallback(this); @@ -1678,6 +1683,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, adjustStatusBarLocked(); mDreamOverlayStateController.addCallback(mDreamOverlayStateCallback); + mHandler.obtainMessage(BOOT_INTERACTOR).sendToTarget(); + final DreamViewModel dreamViewModel = mDreamViewModel.get(); final CommunalTransitionViewModel communalViewModel = mCommunalTransitionViewModel.get(); @@ -2705,11 +2712,19 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, message = "SYSTEM_READY"; handleSystemReady(); break; + case BOOT_INTERACTOR: + message = "BOOT_INTERACTOR"; + handleBootInteractor(); + break; } Log.d(TAG, "KeyguardViewMediator queue processing message: " + message); } }; + private void handleBootInteractor() { + mTransitionBootInteractor.start(); + } + private void tryKeyguardDone() { if (DEBUG) { Log.d(TAG, "tryKeyguardDone: pending - " + mKeyguardDonePending + ", animRan - " 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 8a3d01707540..d0a40ec3a361 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -59,6 +59,7 @@ import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffor import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthModule; 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; @@ -175,6 +176,7 @@ public interface KeyguardModule { Lazy<WindowManagerLockscreenVisibilityManager> wmLockscreenVisibilityManager, SelectedUserInteractor selectedUserInteractor, KeyguardInteractor keyguardInteractor, + KeyguardTransitionBootInteractor transitionBootInteractor, WindowManagerOcclusionManager windowManagerOcclusionManager) { return new KeyguardViewMediator( context, @@ -225,6 +227,7 @@ public interface KeyguardModule { wmLockscreenVisibilityManager, selectedUserInteractor, keyguardInteractor, + transitionBootInteractor, windowManagerOcclusionManager); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index 797a4ec419a9..690ae71aa56e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -23,6 +23,7 @@ import android.annotation.FloatRange import android.annotation.SuppressLint import android.os.Trace import android.util.Log +import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.withContext import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main @@ -95,7 +96,7 @@ interface KeyguardTransitionRepository { * Emits STARTED and FINISHED transition steps to the given state. This is used during boot to * seed the repository with the appropriate initial state. */ - suspend fun emitInitialStepsFromOff(to: KeyguardState) + suspend fun emitInitialStepsFromOff(to: KeyguardState, testSetup: Boolean = false) /** * Allows manual control of a transition. When calling [startTransition], the consumer must pass @@ -108,16 +109,14 @@ interface KeyguardTransitionRepository { suspend fun updateTransition( transitionId: UUID, @FloatRange(from = 0.0, to = 1.0) value: Float, - state: TransitionState + state: TransitionState, ) } @SysUISingleton class KeyguardTransitionRepositoryImpl @Inject -constructor( - @Main val mainDispatcher: CoroutineDispatcher, -) : KeyguardTransitionRepository { +constructor(@Main val mainDispatcher: CoroutineDispatcher) : KeyguardTransitionRepository { /** * Each transition between [KeyguardState]s will have an associated Flow. In order to collect * these events, clients should call [transition]. @@ -140,7 +139,7 @@ constructor( ownerName = "", from = KeyguardState.OFF, to = KeyguardState.OFF, - animator = null + animator = null, ) ) override var currentTransitionInfoInternal = _currentTransitionInfo.asStateFlow() @@ -159,12 +158,7 @@ constructor( // to either GONE or LOCKSCREEN once we're booted up and can determine which state we should // start in. emitTransition( - TransitionStep( - KeyguardState.OFF, - KeyguardState.OFF, - 1f, - TransitionState.FINISHED, - ) + TransitionStep(KeyguardState.OFF, KeyguardState.OFF, 1f, TransitionState.FINISHED) ) } @@ -217,7 +211,7 @@ constructor( TransitionStep( info, (animation.animatedValue as Float), - TransitionState.RUNNING + TransitionState.RUNNING, ) ) } @@ -266,7 +260,7 @@ constructor( override suspend fun updateTransition( transitionId: UUID, @FloatRange(from = 0.0, to = 1.0) value: Float, - state: TransitionState + state: TransitionState, ) { // There is no fairness guarantee with 'withContext', which means that transitions could // be processed out of order. Use a Mutex to guarantee ordering. [startTransition] @@ -282,7 +276,7 @@ constructor( private suspend fun updateTransitionInternal( transitionId: UUID, @FloatRange(from = 0.0, to = 1.0) value: Float, - state: TransitionState + state: TransitionState, ) { if (updateTransitionId != transitionId) { Log.e(TAG, "Attempting to update with old/invalid transitionId: $transitionId") @@ -303,34 +297,51 @@ constructor( lastStep = nextStep } - override suspend fun emitInitialStepsFromOff(to: KeyguardState) { - _currentTransitionInfo.value = - TransitionInfo( - ownerName = "KeyguardTransitionRepository(boot)", - from = KeyguardState.OFF, - to = to, - animator = null + override suspend fun emitInitialStepsFromOff(to: KeyguardState, testSetup: Boolean) { + val ownerName = "KeyguardTransitionRepository(boot)" + // Tests runs on testDispatcher, which is not the main thread, causing the animator thread + // check to fail + if (testSetup) { + _currentTransitionInfo.value = + TransitionInfo( + ownerName = ownerName, + from = KeyguardState.OFF, + to = to, + animator = null, + ) + emitTransition( + TransitionStep( + KeyguardState.OFF, + to, + 0f, + TransitionState.STARTED, + ownerName = ownerName, + ) ) - emitTransition( - TransitionStep( - KeyguardState.OFF, - to, - 0f, - TransitionState.STARTED, - ownerName = "KeyguardTransitionRepository(boot)", + emitTransition( + TransitionStep( + KeyguardState.OFF, + to, + 1f, + TransitionState.FINISHED, + ownerName = ownerName, + ) ) - ) - - emitTransition( - TransitionStep( - KeyguardState.OFF, - to, - 1f, - TransitionState.FINISHED, - ownerName = "KeyguardTransitionRepository(boot)", - ), - ) + } else { + startTransition( + TransitionInfo( + ownerName = ownerName, + from = KeyguardState.OFF, + to = to, + animator = + ValueAnimator().apply { + interpolator = Interpolators.LINEAR + duration = 933L + }, + ) + ) + } } private fun logAndTrace(step: TransitionStep, isManual: Boolean) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt index 0343786bb1fb..840bc0fb5f99 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt @@ -106,7 +106,7 @@ constructor( startTransitionToLockscreenOrHub( isIdleOnCommunal, showCommunalFromOccluded, - dreamFromOccluded + dreamFromOccluded, ) } } @@ -127,7 +127,7 @@ constructor( startTransitionToLockscreenOrHub( isIdleOnCommunal, showCommunalFromOccluded, - dreamFromOccluded + dreamFromOccluded, ) } } @@ -147,7 +147,7 @@ constructor( communalSceneInteractor.changeScene( newScene = CommunalScenes.Communal, loggingReason = "occluded to hub", - transitionKey = CommunalTransitionKeys.SimpleFade + transitionKey = CommunalTransitionKeys.SimpleFade, ) } else { startTransitionTo(KeyguardState.GLANCEABLE_HUB) @@ -210,8 +210,9 @@ constructor( duration = when (toState) { - KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION + KeyguardState.ALTERNATE_BOUNCER -> TO_ALTERNATE_BOUNCER_DURATION KeyguardState.GLANCEABLE_HUB -> TO_GLANCEABLE_HUB_DURATION + KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION else -> DEFAULT_DURATION }.inWholeMilliseconds } @@ -220,9 +221,10 @@ constructor( companion object { const val TAG = "FromOccludedTransitionInteractor" private val DEFAULT_DURATION = 500.milliseconds - val TO_LOCKSCREEN_DURATION = 933.milliseconds - val TO_GLANCEABLE_HUB_DURATION = 250.milliseconds + val TO_ALTERNATE_BOUNCER_DURATION = DEFAULT_DURATION val TO_AOD_DURATION = DEFAULT_DURATION val TO_DOZING_DURATION = DEFAULT_DURATION + val TO_GLANCEABLE_HUB_DURATION = 250.milliseconds + val TO_LOCKSCREEN_DURATION = 933.milliseconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt index ea80911335fa..258232b30670 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt @@ -60,12 +60,12 @@ constructor( transitionInteractor: KeyguardTransitionInteractor, val dismissInteractor: KeyguardDismissInteractor, @Application private val applicationScope: CoroutineScope, - sceneInteractor: Lazy<SceneInteractor>, deviceUnlockedInteractor: Lazy<DeviceUnlockedInteractor>, powerInteractor: PowerInteractor, alternateBouncerInteractor: AlternateBouncerInteractor, shadeInteractor: Lazy<ShadeInteractor>, keyguardInteractor: Lazy<KeyguardInteractor>, + sceneInteractor: Lazy<SceneInteractor>, ) { val dismissAction: Flow<DismissAction> = repository.dismissAction @@ -102,20 +102,20 @@ constructor( private val isOnShadeWhileUnlocked: Flow<Boolean> = if (SceneContainerFlag.isEnabled) { combine( - sceneInteractor.get().currentScene, + shadeInteractor.get().isAnyExpanded, deviceUnlockedInteractor.get().deviceUnlockStatus, - ) { scene, unlockStatus -> - unlockStatus.isUnlocked && - (scene == Scenes.QuickSettings || scene == Scenes.Shade) + ) { isAnyExpanded, unlockStatus -> + isAnyExpanded && unlockStatus.isUnlocked } .distinctUntilChanged() } else if (ComposeBouncerFlags.isOnlyComposeBouncerEnabled()) { combine( - shadeInteractor.get().isAnyExpanded, - keyguardInteractor.get().isKeyguardDismissible, - ) { isAnyExpanded, keyguardDismissible -> - isAnyExpanded && keyguardDismissible - } + shadeInteractor.get().isAnyExpanded, + keyguardInteractor.get().isKeyguardDismissible, + ) { isAnyExpanded, keyguardDismissible -> + isAnyExpanded && keyguardDismissible + } + .distinctUntilChanged() } else { flow { error( @@ -127,7 +127,20 @@ constructor( val executeDismissAction: Flow<() -> KeyguardDone> = merge( - finishedTransitionToGone, + if (SceneContainerFlag.isEnabled) { + // Using currentScene instead of finishedTransitionToGone because of a race + // condition that forms between finishedTransitionToGone and + // isOnShadeWhileUnlocked where the latter emits false before the former emits + // true, causing the merge to not emit until it's too late. + sceneInteractor + .get() + .currentScene + .map { it == Scenes.Gone } + .distinctUntilChanged() + .filter { it } + } else { + finishedTransitionToGone + }, isOnShadeWhileUnlocked.filter { it }.map {}, dismissInteractor.dismissKeyguardRequestWithImmediateDismissAction, ) @@ -137,10 +150,24 @@ constructor( val resetDismissAction: Flow<Unit> = combine( - transitionInteractor.isFinishedIn( - scene = Scenes.Gone, - stateWithoutSceneContainer = GONE, - ), + if (SceneContainerFlag.isEnabled) { + // Using currentScene instead of isFinishedIn because of a race condition that + // forms between isFinishedIn(Gone) and isOnShadeWhileUnlocked where the latter + // emits false before the former emits true, causing the evaluation of the + // combine to come up with true, temporarily, before settling on false, which is + // a valid final state. That causes an incorrect reset of the dismiss action to + // occur before it gets executed. + sceneInteractor + .get() + .currentScene + .map { it == Scenes.Gone } + .distinctUntilChanged() + } else { + transitionInteractor.isFinishedIn( + scene = Scenes.Gone, + stateWithoutSceneContainer = GONE, + ) + }, transitionInteractor.isFinishedIn( scene = Scenes.Bouncer, stateWithoutSceneContainer = PRIMARY_BOUNCER, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt index b2183007c48c..89f636d4a270 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt @@ -17,7 +17,6 @@ package com.android.systemui.keyguard.domain.interactor import android.util.Log -import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor @@ -46,7 +45,7 @@ constructor( val keyguardTransitionInteractor: KeyguardTransitionInteractor, val internalTransitionInteractor: InternalKeyguardTransitionInteractor, val repository: KeyguardTransitionRepository, -) : CoreStartable { +) { /** * Whether the lockscreen should be showing when the device starts up for the first time. If not @@ -60,14 +59,14 @@ constructor( } } - override fun start() { + fun start() { scope.launch { if (internalTransitionInteractor.currentTransitionInfoInternal.value.from != OFF) { Log.e( "KeyguardTransitionInteractor", "showLockscreenOnBoot emitted, but we've already " + "transitioned to a state other than OFF. We'll respect that " + - "transition, but this should not happen." + "transition, but this should not happen.", ) } else { if (SceneContainerFlag.isEnabled) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt index 25b8fd32e82a..b71533389e2d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt @@ -27,7 +27,6 @@ class KeyguardTransitionCoreStartable constructor( private val interactors: Set<TransitionInteractor>, private val auditLogger: KeyguardTransitionAuditLogger, - private val bootInteractor: KeyguardTransitionBootInteractor, private val statusBarDisableFlagsInteractor: StatusBarDisableFlagsInteractor, private val keyguardStateCallbackInteractor: KeyguardStateCallbackInteractor, ) : CoreStartable { @@ -54,7 +53,6 @@ constructor( it.start() } auditLogger.start() - bootInteractor.start() statusBarDisableFlagsInteractor.start() keyguardStateCallbackInteractor.start() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt index f1b9cba11051..00aa44fe795b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt @@ -47,56 +47,53 @@ object KeyguardBlueprintViewBinder { constraintLayout.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { launch("$TAG#viewModel.blueprint") { - viewModel.blueprint - .pairwise( - null as KeyguardBlueprint?, - ) - .collect { (prevBlueprint, blueprint) -> - val config = Config.DEFAULT - val transition = - if ( - !KeyguardBottomAreaRefactor.isEnabled && - prevBlueprint != null && - prevBlueprint != blueprint - ) { - BaseBlueprintTransition(clockViewModel) - .addTransition( - IntraBlueprintTransition( - config, - clockViewModel, - smartspaceViewModel - ) + viewModel.blueprint.pairwise(null as KeyguardBlueprint?).collect { + (prevBlueprint, blueprint) -> + val config = Config.DEFAULT + val transition = + if ( + !KeyguardBottomAreaRefactor.isEnabled && + prevBlueprint != null && + prevBlueprint != blueprint + ) { + BaseBlueprintTransition(clockViewModel) + .addTransition( + IntraBlueprintTransition( + config, + clockViewModel, + smartspaceViewModel, ) - } else { - IntraBlueprintTransition( - config, - clockViewModel, - smartspaceViewModel ) - } - - viewModel.runTransition(constraintLayout, transition, config) { - // Replace sections from the previous blueprint with the new ones - blueprint.replaceViews( - constraintLayout, - prevBlueprint, - config.rebuildSections + } else { + IntraBlueprintTransition( + config, + clockViewModel, + smartspaceViewModel, ) + } + + viewModel.runTransition(constraintLayout, transition, config) { + // Replace sections from the previous blueprint with the new ones + blueprint.replaceViews( + constraintLayout, + prevBlueprint, + config.rebuildSections, + ) - val cs = - ConstraintSet().apply { - clone(constraintLayout) - val emptyLayout = ConstraintSet.Layout() - knownIds.forEach { - getConstraint(it).layout.copyFrom(emptyLayout) - } - blueprint.applyConstraints(this) + val cs = + ConstraintSet().apply { + clone(constraintLayout) + val emptyLayout = ConstraintSet.Layout() + knownIds.forEach { + getConstraint(it).layout.copyFrom(emptyLayout) } + blueprint.applyConstraints(this) + } - logAlphaVisibilityScaleOfAppliedConstraintSet(cs, clockViewModel) - cs.applyTo(constraintLayout) - } + logAlphaVisibilityScaleOfAppliedConstraintSet(cs, clockViewModel) + cs.applyTo(constraintLayout) } + } } launch("$TAG#viewModel.refreshTransition") { @@ -105,7 +102,8 @@ object KeyguardBlueprintViewBinder { viewModel.runTransition( constraintLayout, - IntraBlueprintTransition(config, clockViewModel, smartspaceViewModel), + clockViewModel, + smartspaceViewModel, config, ) { blueprint.rebuildViews(constraintLayout, config.rebuildSections) @@ -126,7 +124,7 @@ object KeyguardBlueprintViewBinder { private fun logAlphaVisibilityScaleOfAppliedConstraintSet( cs: ConstraintSet, - viewModel: KeyguardClockViewModel + viewModel: KeyguardClockViewModel, ) { val currentClock = viewModel.currentClock.value if (!DEBUG || currentClock == null) return @@ -137,19 +135,19 @@ object KeyguardBlueprintViewBinder { TAG, "applyCsToSmallClock: vis=${cs.getVisibility(smallClockViewId)} " + "alpha=${cs.getConstraint(smallClockViewId).propertySet.alpha} " + - "scale=${cs.getConstraint(smallClockViewId).transform.scaleX} " + "scale=${cs.getConstraint(smallClockViewId).transform.scaleX} ", ) Log.i( TAG, "applyCsToLargeClock: vis=${cs.getVisibility(largeClockViewId)} " + "alpha=${cs.getConstraint(largeClockViewId).propertySet.alpha} " + "scale=${cs.getConstraint(largeClockViewId).transform.scaleX} " + - "pivotX=${cs.getConstraint(largeClockViewId).transform.transformPivotX} " + "pivotX=${cs.getConstraint(largeClockViewId).transform.transformPivotX} ", ) Log.i( TAG, "applyCsToSmartspaceDate: vis=${cs.getVisibility(smartspaceDateId)} " + - "alpha=${cs.getConstraint(smartspaceDateId).propertySet.alpha}" + "alpha=${cs.getConstraint(smartspaceDateId).propertySet.alpha}", ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt index aa0a9d9cee1f..9a55f7bab33b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt @@ -29,18 +29,18 @@ class IntraBlueprintTransition( smartspaceViewModel: KeyguardSmartspaceViewModel, ) : TransitionSet() { - enum class Type( - val priority: Int, - val animateNotifChanges: Boolean, - ) { + enum class Type(val priority: Int, val animateNotifChanges: Boolean) { ClockSize(100, true), ClockCenter(99, false), DefaultClockStepping(98, false), - SmartspaceVisibility(2, true), - DefaultTransition(1, false), + SmartspaceVisibility(3, true), + DefaultTransition(2, false), // When transition between blueprint, we don't need any duration or interpolator but we need // all elements go to correct state - NoTransition(0, false), + NoTransition(1, false), + // Similar to NoTransition, except also does not explicitly update any alpha. Used in + // OFF->LOCKSCREEN transition + Init(0, false), } data class Config( @@ -57,6 +57,7 @@ class IntraBlueprintTransition( init { ordering = ORDERING_TOGETHER when (config.type) { + Type.Init -> {} Type.NoTransition -> {} Type.DefaultClockStepping -> addTransition( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index ff848264db68..a1c963b3137a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -53,14 +53,11 @@ import kotlinx.coroutines.DisposableHandle internal fun ConstraintSet.setVisibility(views: Iterable<View>, visibility: Int) = views.forEach { view -> this.setVisibility(view.id, visibility) } -internal fun ConstraintSet.setAlpha(views: Iterable<View>, alpha: Float) = - views.forEach { view -> this.setAlpha(view.id, alpha) } +internal fun ConstraintSet.setScaleX(views: Iterable<View>, scaleX: Float) = + views.forEach { view -> this.setScaleX(view.id, scaleX) } -internal fun ConstraintSet.setScaleX(views: Iterable<View>, alpha: Float) = - views.forEach { view -> this.setScaleX(view.id, alpha) } - -internal fun ConstraintSet.setScaleY(views: Iterable<View>, alpha: Float) = - views.forEach { view -> this.setScaleY(view.id, alpha) } +internal fun ConstraintSet.setScaleY(views: Iterable<View>, scaleY: Float) = + views.forEach { view -> this.setScaleY(view.id, scaleY) } @SysUISingleton class ClockSection @@ -126,8 +123,6 @@ constructor( return constraintSet.apply { setVisibility(getTargetClockFace(clock).views, VISIBLE) setVisibility(getNonTargetClockFace(clock).views, GONE) - setAlpha(getTargetClockFace(clock).views, 1F) - setAlpha(getNonTargetClockFace(clock).views, 0F) if (!keyguardClockViewModel.isLargeClockVisible.value) { connect(sharedR.id.bc_smartspace_view, TOP, sharedR.id.date_smartspace_view, BOTTOM) } else { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToOccludedTransitionViewModel.kt index 3f2ef29c9570..c49e7833e349 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToOccludedTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToOccludedTransitionViewModel.kt @@ -28,22 +28,22 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow /** - * Breaks down ALTERNATE_BOUNCER->GONE transition into discrete steps for corresponding views to + * Breaks down ALTERNATE_BOUNCER->OCCLUDED transition into discrete steps for corresponding views to * consume. */ @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class AlternateBouncerToOccludedTransitionViewModel @Inject -constructor( - animationFlow: KeyguardTransitionAnimationFlow, -) : DeviceEntryIconTransition { +constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition { private val transitionAnimation = animationFlow.setup( duration = TO_OCCLUDED_DURATION, edge = Edge.create(from = ALTERNATE_BOUNCER, to = OCCLUDED), ) + val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f) + override val deviceEntryParentViewAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt index a021de446911..ca1a8006c6bb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt @@ -56,6 +56,7 @@ constructor( occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel, occludedToDozingTransitionViewModel: OccludedToDozingTransitionViewModel, occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel, + offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel, primaryBouncerToAodTransitionViewModel: PrimaryBouncerToAodTransitionViewModel, primaryBouncerToDozingTransitionViewModel: PrimaryBouncerToDozingTransitionViewModel, primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel, @@ -67,14 +68,14 @@ constructor( .map { Utils.getColorAttrDefaultColor( context, - com.android.internal.R.attr.colorSurface + com.android.internal.R.attr.colorSurface, ) } .onStart { emit( Utils.getColorAttrDefaultColor( context, - com.android.internal.R.attr.colorSurface + com.android.internal.R.attr.colorSurface, ) ) } @@ -86,23 +87,23 @@ constructor( deviceEntryIconViewModel.useBackgroundProtection.flatMapLatest { useBackground -> if (useBackground) { setOf( - lockscreenToAodTransitionViewModel.deviceEntryBackgroundViewAlpha, + alternateBouncerToAodTransitionViewModel.deviceEntryBackgroundViewAlpha, + alternateBouncerToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha, aodToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha, + dozingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha, + dreamingToAodTransitionViewModel.deviceEntryBackgroundViewAlpha, + dreamingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha, goneToAodTransitionViewModel.deviceEntryBackgroundViewAlpha, - primaryBouncerToAodTransitionViewModel.deviceEntryBackgroundViewAlpha, + goneToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha, + goneToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha, + lockscreenToAodTransitionViewModel.deviceEntryBackgroundViewAlpha, occludedToAodTransitionViewModel.deviceEntryBackgroundViewAlpha, + occludedToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha, occludedToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha, - dreamingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha, - alternateBouncerToAodTransitionViewModel.deviceEntryBackgroundViewAlpha, - goneToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha, - goneToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha, + offToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha, + primaryBouncerToAodTransitionViewModel.deviceEntryBackgroundViewAlpha, primaryBouncerToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha, - dozingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha, - alternateBouncerToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha, - dreamingToAodTransitionViewModel.deviceEntryBackgroundViewAlpha, - primaryBouncerToLockscreenTransitionViewModel - .deviceEntryBackgroundViewAlpha, - occludedToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha, + primaryBouncerToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha, ) .merge() .onStart { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt index 4cf3c4e7f6d0..1289036c7ae0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt @@ -24,6 +24,9 @@ import android.util.Log import androidx.constraintlayout.widget.ConstraintLayout import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type import javax.inject.Inject @@ -37,6 +40,7 @@ class KeyguardBlueprintViewModel constructor( @Main private val handler: Handler, private val keyguardBlueprintInteractor: KeyguardBlueprintInteractor, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, ) { val blueprint = keyguardBlueprintInteractor.blueprint val blueprintId = keyguardBlueprintInteractor.blueprintId @@ -49,12 +53,12 @@ constructor( private val transitionListener = object : Transition.TransitionListener { override fun onTransitionCancel(transition: Transition) { - if (DEBUG) Log.e(TAG, "onTransitionCancel: ${transition::class.simpleName}") + if (DEBUG) Log.w(TAG, "onTransitionCancel: ${transition::class.simpleName}") updateTransitions(null) { remove(transition) } } override fun onTransitionEnd(transition: Transition) { - if (DEBUG) Log.e(TAG, "onTransitionEnd: ${transition::class.simpleName}") + if (DEBUG) Log.i(TAG, "onTransitionEnd: ${transition::class.simpleName}") updateTransitions(null) { remove(transition) } } @@ -86,6 +90,28 @@ constructor( fun runTransition( constraintLayout: ConstraintLayout, + clockViewModel: KeyguardClockViewModel, + smartspaceViewModel: KeyguardSmartspaceViewModel, + config: Config, + apply: () -> Unit, + ) { + val newConfig = + if (keyguardTransitionInteractor.getCurrentState() == KeyguardState.OFF) { + config.copy(type = Type.Init) + } else { + config + } + + runTransition( + constraintLayout, + IntraBlueprintTransition(newConfig, clockViewModel, smartspaceViewModel), + config, + apply, + ) + } + + fun runTransition( + constraintLayout: ConstraintLayout, transition: Transition, config: Config, apply: () -> Unit, @@ -103,21 +129,29 @@ constructor( return } + // Don't allow transitions with animations while in OFF state + val newConfig = + if (keyguardTransitionInteractor.getCurrentState() == KeyguardState.OFF) { + config.copy(type = Type.Init) + } else { + config + } + if (DEBUG) { Log.i( TAG, "runTransition: running ${transition::class.simpleName}: " + - "currentPriority=$currentPriority; config=$config", + "currentPriority=$currentPriority; config=$newConfig", ) } // beginDelayedTransition makes a copy, so we temporarially add the uncopied transition to // the running set until the copy is started by the handler. - updateTransitions(TransitionData(config)) { add(transition) } + updateTransitions(TransitionData(newConfig)) { add(transition) } transition.addListener(transitionListener) handler.post { - if (config.terminatePrevious) { + if (newConfig.terminatePrevious) { TransitionManager.endTransitions(constraintLayout) } 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 10a2e5c04b00..3705c2c19110 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 @@ -20,7 +20,6 @@ package com.android.systemui.keyguard.ui.viewmodel import android.graphics.Point import android.util.MathUtils import android.view.View.VISIBLE -import com.android.app.tracing.coroutines.launch import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton @@ -35,6 +34,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED +import com.android.systemui.keyguard.shared.model.KeyguardState.OFF import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.shared.model.TransitionState.STARTED @@ -88,6 +88,8 @@ constructor( AlternateBouncerToGoneTransitionViewModel, private val alternateBouncerToLockscreenTransitionViewModel: AlternateBouncerToLockscreenTransitionViewModel, + private val alternateBouncerToOccludedTransitionViewModel: + AlternateBouncerToOccludedTransitionViewModel, private val aodToGoneTransitionViewModel: AodToGoneTransitionViewModel, private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel, @@ -112,9 +114,12 @@ constructor( private val lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel, private val lockscreenToPrimaryBouncerTransitionViewModel: LockscreenToPrimaryBouncerTransitionViewModel, + private val occludedToAlternateBouncerTransitionViewModel: + OccludedToAlternateBouncerTransitionViewModel, private val occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel, private val occludedToDozingTransitionViewModel: OccludedToDozingTransitionViewModel, private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel, + private val offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel, private val primaryBouncerToAodTransitionViewModel: PrimaryBouncerToAodTransitionViewModel, private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel, private val primaryBouncerToLockscreenTransitionViewModel: @@ -201,6 +206,10 @@ constructor( notificationShadeWindowModel.isKeyguardOccluded, communalInteractor.isIdleOnCommunal, keyguardTransitionInteractor + .transitionValue(OFF) + .map { it > 1f - offToLockscreenTransitionViewModel.alphaStartAt } + .onStart { emit(false) }, + keyguardTransitionInteractor .transitionValue(scene = Scenes.Gone, stateWithoutSceneContainer = GONE) .map { it == 1f } .onStart { emit(false) }, @@ -227,6 +236,7 @@ constructor( alternateBouncerToAodTransitionViewModel.lockscreenAlpha(viewState), alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState), alternateBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState), + alternateBouncerToOccludedTransitionViewModel.lockscreenAlpha, aodToGoneTransitionViewModel.lockscreenAlpha(viewState), aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState), aodToOccludedTransitionViewModel.lockscreenAlpha(viewState), @@ -249,14 +259,16 @@ constructor( lockscreenToGoneTransitionViewModel.lockscreenAlpha(viewState), lockscreenToOccludedTransitionViewModel.lockscreenAlpha, lockscreenToPrimaryBouncerTransitionViewModel.lockscreenAlpha, + occludedToAlternateBouncerTransitionViewModel.lockscreenAlpha, occludedToAodTransitionViewModel.lockscreenAlpha, occludedToDozingTransitionViewModel.lockscreenAlpha, occludedToLockscreenTransitionViewModel.lockscreenAlpha, + offToLockscreenTransitionViewModel.lockscreenAlpha, primaryBouncerToAodTransitionViewModel.lockscreenAlpha, primaryBouncerToGoneTransitionViewModel.lockscreenAlpha, primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState), ) - .onStart { emit(1f) }, + .onStart { emit(0f) }, ) { hideKeyguard, alpha -> if (hideKeyguard) { 0f diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt index 8d9ccef95f0d..88e8968501dd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt @@ -52,18 +52,26 @@ constructor( /** Lockscreen views alpha */ val lockscreenAlpha: Flow<Float> = - transitionAnimation.sharedFlow( - duration = 250.milliseconds, - onStep = { 1f - it }, - name = "LOCKSCREEN->OCCLUDED: lockscreenAlpha", + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = + transitionAnimation.sharedFlow( + duration = 250.milliseconds, + onStep = { 1f - it }, + name = "LOCKSCREEN->OCCLUDED: lockscreenAlpha", + ), + flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(0f), ) val shortcutsAlpha: Flow<Float> = - transitionAnimation.sharedFlow( - duration = 250.milliseconds, - onStep = { 1 - it }, - onFinish = { 0f }, - onCancel = { 1f }, + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = + transitionAnimation.sharedFlow( + duration = 250.milliseconds, + onStep = { 1f - it }, + onFinish = { 0f }, + onCancel = { 1f }, + ), + flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(0f), ) /** Lockscreen views y-translation */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModel.kt new file mode 100644 index 000000000000..5bfcccbaccaa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModel.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_ALTERNATE_BOUNCER_DURATION +import com.android.systemui.keyguard.shared.model.Edge +import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER +import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow + +/** + * Breaks down OCCLUDED->ALTERNATE_BOUNCER transition into discrete steps for corresponding views to + * consume. + */ +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class OccludedToAlternateBouncerTransitionViewModel +@Inject +constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition { + private val transitionAnimation = + animationFlow.setup( + duration = TO_ALTERNATE_BOUNCER_DURATION, + edge = Edge.create(from = OCCLUDED, to = ALTERNATE_BOUNCER), + ) + + val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f) + + override val deviceEntryParentViewAlpha: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0f) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt index 1eecbd5fbda1..b4acce66da4f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN @@ -29,23 +30,29 @@ import kotlinx.coroutines.flow.Flow @SysUISingleton class OffToLockscreenTransitionViewModel @Inject -constructor( - animationFlow: KeyguardTransitionAnimationFlow, -) : DeviceEntryIconTransition { +constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition { + + private val startTime = 300.milliseconds + private val alphaDuration = 633.milliseconds + val alphaStartAt = startTime / (alphaDuration + startTime) private val transitionAnimation = animationFlow.setup( - duration = 250.milliseconds, + duration = startTime + alphaDuration, edge = Edge.create(from = OFF, to = LOCKSCREEN), ) - val shortcutsAlpha: Flow<Float> = + val lockscreenAlpha: Flow<Float> = transitionAnimation.sharedFlow( - duration = 250.milliseconds, + startTime = startTime, + duration = alphaDuration, + interpolator = EMPHASIZED_ACCELERATE, onStep = { it }, - onCancel = { 0f }, ) - override val deviceEntryParentViewAlpha: Flow<Float> = - transitionAnimation.immediatelyTransitionTo(1f) + val shortcutsAlpha: Flow<Float> = lockscreenAlpha + + override val deviceEntryParentViewAlpha: Flow<Float> = lockscreenAlpha + + val deviceEntryBackgroundViewAlpha: Flow<Float> = lockscreenAlpha } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt index 84aae652795e..222d783ab79a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt @@ -111,7 +111,7 @@ private val ART_URIS = arrayOf( MediaMetadata.METADATA_KEY_ALBUM_ART_URI, MediaMetadata.METADATA_KEY_ART_URI, - MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI + MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, ) private const val TAG = "MediaDataManager" @@ -136,7 +136,7 @@ private val LOADING = active = true, resumeAction = null, instanceId = InstanceId.fakeInstanceId(-1), - appUid = Process.INVALID_UID + appUid = Process.INVALID_UID, ) internal val EMPTY_SMARTSPACE_MEDIA_DATA = @@ -163,7 +163,7 @@ private fun allowMediaRecommendations(context: Context): Boolean { Settings.Secure.getInt( context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, - 1 + 1, ) return Utils.useQsMediaPlayer(context) && flag > 0 } @@ -217,7 +217,7 @@ class LegacyMediaDataManagerImpl( private val themeText = com.android.settingslib.Utils.getColorAttr( context, - com.android.internal.R.attr.textColorPrimary + com.android.internal.R.attr.textColorPrimary, ) .defaultColor @@ -387,7 +387,7 @@ class LegacyMediaDataManagerImpl( uiExecutor, SmartspaceSession.OnTargetsAvailableListener { targets -> smartspaceMediaDataProvider.onTargetsAvailable(targets) - } + }, ) } smartspaceSession?.let { it.requestSmartspaceUpdate() } @@ -398,12 +398,12 @@ class LegacyMediaDataManagerImpl( if (!allowMediaRecommendations) { dismissSmartspaceRecommendation( key = smartspaceMediaData.targetId, - delay = 0L + delay = 0L, ) } } }, - Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, ) } @@ -461,7 +461,7 @@ class LegacyMediaDataManagerImpl( token: MediaSession.Token, appName: String, appIntent: PendingIntent, - packageName: String + packageName: String, ) { // Resume controls don't have a notification key, so store by package name instead if (!mediaEntries.containsKey(packageName)) { @@ -497,7 +497,7 @@ class LegacyMediaDataManagerImpl( token, appName, appIntent, - packageName + packageName, ) } } else { @@ -509,7 +509,7 @@ class LegacyMediaDataManagerImpl( token, appName, appIntent, - packageName + packageName, ) } } @@ -609,14 +609,14 @@ class LegacyMediaDataManagerImpl( result.appUid, sbn.packageName, instanceId, - result.playbackLocation + result.playbackLocation, ) } else if (result.playbackLocation != currentEntry?.playbackLocation) { logger.logPlaybackLocationChange( result.appUid, sbn.packageName, instanceId, - result.playbackLocation + result.playbackLocation, ) } @@ -722,30 +722,32 @@ class LegacyMediaDataManagerImpl( /** Called when the player's [PlaybackState] has been updated with new actions and/or state */ private fun updateState(key: String, state: PlaybackState) { mediaEntries.get(key)?.let { - val token = it.token - if (token == null) { - if (DEBUG) Log.d(TAG, "State updated, but token was null") - return - } - val actions = - createActionsFromState( - it.packageName, - mediaControllerFactory.create(it.token), - UserHandle(it.userId) - ) - - // Control buttons - // If flag is enabled and controller has a PlaybackState, - // create actions from session info - // otherwise, no need to update semantic actions. - val data = - if (actions != null) { - it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state)) - } else { - it.copy(isPlaying = isPlayingState(state.state)) + backgroundExecutor.execute { + val token = it.token + if (token == null) { + if (DEBUG) Log.d(TAG, "State updated, but token was null") + return@execute } - if (DEBUG) Log.d(TAG, "State updated outside of notification") - onMediaDataLoaded(key, key, data) + val actions = + createActionsFromState( + it.packageName, + mediaControllerFactory.create(it.token), + UserHandle(it.userId), + ) + + // Control buttons + // If flag is enabled and controller has a PlaybackState, + // create actions from session info + // otherwise, no need to update semantic actions. + val data = + if (actions != null) { + it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state)) + } else { + it.copy(isPlaying = isPlayingState(state.state)) + } + if (DEBUG) Log.d(TAG, "State updated outside of notification") + foregroundExecutor.execute { onMediaDataLoaded(key, key, data) } + } } } @@ -773,7 +775,7 @@ class LegacyMediaDataManagerImpl( } foregroundExecutor.executeDelayed( { removeEntry(key = key, userInitiated = userInitiated) }, - delay + delay, ) return existed } @@ -793,12 +795,12 @@ class LegacyMediaDataManagerImpl( smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( targetId = smartspaceMediaData.targetId, - instanceId = smartspaceMediaData.instanceId + instanceId = smartspaceMediaData.instanceId, ) } foregroundExecutor.executeDelayed( { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) }, - delay + delay, ) } @@ -826,7 +828,7 @@ class LegacyMediaDataManagerImpl( token: MediaSession.Token, appName: String, appIntent: PendingIntent, - packageName: String + packageName: String, ) = withContext(backgroundDispatcher) { val lastActive = systemClock.elapsedRealtime() @@ -843,7 +845,7 @@ class LegacyMediaDataManagerImpl( token, appName, appIntent, - packageName + packageName, ) if (result == null || desc.title.isNullOrBlank()) { Log.d(TAG, "No MediaData result for resumption") @@ -882,7 +884,7 @@ class LegacyMediaDataManagerImpl( appUid = result.appUid, isExplicit = result.isExplicit, resumeProgress = result.resumeProgress, - ) + ), ) } } @@ -895,7 +897,7 @@ class LegacyMediaDataManagerImpl( token: MediaSession.Token, appName: String, appIntent: PendingIntent, - packageName: String + packageName: String, ) { if (desc.title.isNullOrBlank()) { Log.e(TAG, "Description incomplete") @@ -966,7 +968,7 @@ class LegacyMediaDataManagerImpl( appUid = appUid, isExplicit = isExplicit, resumeProgress = progress, - ) + ), ) } } @@ -981,7 +983,7 @@ class LegacyMediaDataManagerImpl( val token = sbn.notification.extras.getParcelable( Notification.EXTRA_MEDIA_SESSION, - MediaSession.Token::class.java + MediaSession.Token::class.java, ) if (token == null) { return @@ -993,7 +995,7 @@ class LegacyMediaDataManagerImpl( val appInfo = notif.extras.getParcelable( Notification.EXTRA_BUILDER_APPLICATION_INFO, - ApplicationInfo::class.java + ApplicationInfo::class.java, ) ?: getAppInfoFromPackage(sbn.packageName) // App name @@ -1057,7 +1059,7 @@ class LegacyMediaDataManagerImpl( val deviceIntent = extras.getParcelable( Notification.EXTRA_MEDIA_REMOTE_INTENT, - PendingIntent::class.java + PendingIntent::class.java, ) Log.d(TAG, "$key is RCN for $deviceName") @@ -1073,7 +1075,7 @@ class LegacyMediaDataManagerImpl( deviceDrawable, deviceName, deviceIntent, - showBroadcastButton = false + showBroadcastButton = false, ) } } @@ -1160,7 +1162,7 @@ class LegacyMediaDataManagerImpl( mediaData.copy( resumeAction = oldResumeAction, hasCheckedForResume = oldHasCheckedForResume, - active = oldActive + active = oldActive, ) onMediaDataLoaded(key, oldKey, mediaData) } @@ -1169,7 +1171,7 @@ class LegacyMediaDataManagerImpl( private fun logSingleVsMultipleMediaAdded( appUid: Int, packageName: String, - instanceId: InstanceId + instanceId: InstanceId, ) { if (mediaEntries.size == 1) { logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId) @@ -1207,7 +1209,7 @@ class LegacyMediaDataManagerImpl( private fun createActionsFromState( packageName: String, controller: MediaController, - user: UserHandle + user: UserHandle, ): MediaButton? { if (!mediaFlags.areMediaSessionActionsEnabled(packageName, user)) { return null @@ -1245,7 +1247,7 @@ class LegacyMediaDataManagerImpl( packageName, ContentProvider.getUriWithoutUserId(uri), Intent.FLAG_GRANT_READ_URI_PERMISSION, - ContentProvider.getUserIdFromUri(uri, userId) + ContentProvider.getUserIdFromUri(uri, userId), ) return loadBitmapFromUri(uri) } catch (e: SecurityException) { @@ -1282,7 +1284,7 @@ class LegacyMediaDataManagerImpl( val scale = MediaDataUtils.getScaleFactor( APair(width, height), - APair(artworkWidth, artworkHeight) + APair(artworkWidth, artworkHeight), ) // Downscale if needed @@ -1307,7 +1309,7 @@ class LegacyMediaDataManagerImpl( .loadDrawable(context), action, context.getString(R.string.controls_media_resume), - context.getDrawable(R.drawable.ic_media_play_container) + context.getDrawable(R.drawable.ic_media_play_container), ) } @@ -1371,10 +1373,7 @@ class LegacyMediaDataManagerImpl( // There should NOT be more than 1 Smartspace media update. When it happens, it // indicates a bad state or an error. Reset the status accordingly. Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...") - notifySmartspaceMediaDataRemoved( - smartspaceMediaData.targetId, - immediately = false, - ) + notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false) smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA } } @@ -1420,7 +1419,7 @@ class LegacyMediaDataManagerImpl( private fun handlePossibleRemoval( key: String, removed: MediaData, - notificationRemoved: Boolean = false + notificationRemoved: Boolean = false, ) { val hasSession = removed.token != null if (hasSession && removed.semanticActions != null) { @@ -1445,7 +1444,7 @@ class LegacyMediaDataManagerImpl( Log.d( TAG, "Notification ($notificationRemoved) and/or session " + - "($hasSession) gone for inactive player $key" + "($hasSession) gone for inactive player $key", ) } convertToResumePlayer(key, removed) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt index f2825d0465ad..4f9791353b8a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt @@ -16,6 +16,7 @@ package com.android.systemui.media.controls.domain.pipeline +import android.annotation.WorkerThread import android.app.ActivityOptions import android.app.BroadcastOptions import android.app.Notification @@ -50,6 +51,7 @@ private const val TAG = "MediaActions" * @return a Pair consisting of a list of media actions, and a list of ints representing which of * those actions should be shown in the compact player */ +@WorkerThread fun createActionsFromState( context: Context, packageName: String, @@ -69,7 +71,7 @@ fun createActionsFromState( context.getString(R.string.controls_media_button_connecting), context.getDrawable(R.drawable.ic_media_connecting_container), // Specify a rebind id to prevent the spinner from restarting on later binds. - com.android.internal.R.drawable.progress_small_material + com.android.internal.R.drawable.progress_small_material, ) } else if (isPlayingState(state.state)) { getStandardAction(context, controller, state.actions, PlaybackState.ACTION_PAUSE) @@ -128,7 +130,7 @@ fun createActionsFromState( nextCustomAction(), nextCustomAction(), reserveNext, - reservePrev + reservePrev, ) } @@ -146,7 +148,7 @@ private fun getStandardAction( context: Context, controller: MediaController, stateActions: Long, - @PlaybackState.Actions action: Long + @PlaybackState.Actions action: Long, ): MediaAction? { if (!includesAction(stateActions, action)) { return null @@ -158,7 +160,7 @@ private fun getStandardAction( context.getDrawable(R.drawable.ic_media_play), { controller.transportControls.play() }, context.getString(R.string.controls_media_button_play), - context.getDrawable(R.drawable.ic_media_play_container) + context.getDrawable(R.drawable.ic_media_play_container), ) } PlaybackState.ACTION_PAUSE -> { @@ -166,7 +168,7 @@ private fun getStandardAction( context.getDrawable(R.drawable.ic_media_pause), { controller.transportControls.pause() }, context.getString(R.string.controls_media_button_pause), - context.getDrawable(R.drawable.ic_media_pause_container) + context.getDrawable(R.drawable.ic_media_pause_container), ) } PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { @@ -174,7 +176,7 @@ private fun getStandardAction( MediaControlDrawables.getPrevIcon(context), { controller.transportControls.skipToPrevious() }, context.getString(R.string.controls_media_button_prev), - null + null, ) } PlaybackState.ACTION_SKIP_TO_NEXT -> { @@ -182,7 +184,7 @@ private fun getStandardAction( MediaControlDrawables.getNextIcon(context), { controller.transportControls.skipToNext() }, context.getString(R.string.controls_media_button_next), - null + null, ) } else -> null @@ -194,13 +196,13 @@ private fun getCustomAction( context: Context, packageName: String, controller: MediaController, - customAction: PlaybackState.CustomAction + customAction: PlaybackState.CustomAction, ): MediaAction { return MediaAction( Icon.createWithResource(packageName, customAction.icon).loadDrawable(context), { controller.transportControls.sendCustomAction(customAction, customAction.extras) }, customAction.name, - null + null, ) } @@ -218,7 +220,7 @@ private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Lo /** Generate action buttons based on notification actions */ fun createActionsFromNotification( context: Context, - sbn: StatusBarNotification + sbn: StatusBarNotification, ): Pair<List<MediaNotificationAction>, List<Int>> { val notif = sbn.notification val actionIcons: MutableList<MediaNotificationAction> = ArrayList() @@ -229,7 +231,7 @@ fun createActionsFromNotification( if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) { Log.e( TAG, - "Too many compact actions for ${sbn.key}, limiting to first $MAX_COMPACT_ACTIONS" + "Too many compact actions for ${sbn.key}, limiting to first $MAX_COMPACT_ACTIONS", ) actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS) } @@ -239,7 +241,7 @@ fun createActionsFromNotification( Log.w( TAG, "Too many notification actions for ${sbn.key}, " + - "limiting to first $MAX_NOTIFICATION_ACTIONS" + "limiting to first $MAX_NOTIFICATION_ACTIONS", ) } @@ -253,7 +255,7 @@ fun createActionsFromNotification( val themeText = com.android.settingslib.Utils.getColorAttr( context, - com.android.internal.R.attr.textColorPrimary + com.android.internal.R.attr.textColorPrimary, ) .defaultColor @@ -271,7 +273,7 @@ fun createActionsFromNotification( action.isAuthenticationRequired, action.actionIntent, mediaActionIcon, - action.title + action.title, ) actionIcons.add(mediaAction) } @@ -288,7 +290,7 @@ fun createActionsFromNotification( */ fun getNotificationActions( actions: List<MediaNotificationAction>, - activityStarter: ActivityStarter + activityStarter: ActivityStarter, ): List<MediaAction> { return actions.map { action -> val runnable = @@ -303,7 +305,7 @@ fun getNotificationActions( activityStarter.dismissKeyguardThenExecute( { sendPendingIntent(action.actionIntent) }, {}, - true + true, ) else -> sendPendingIntent(actionIntent) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt index 5f0a9f82b9ae..fd7b6dcfebbc 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt @@ -119,7 +119,7 @@ private val ART_URIS = arrayOf( MediaMetadata.METADATA_KEY_ALBUM_ART_URI, MediaMetadata.METADATA_KEY_ART_URI, - MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI + MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, ) private const val TAG = "MediaDataProcessor" @@ -177,7 +177,7 @@ class MediaDataProcessor( private val themeText = com.android.settingslib.Utils.getColorAttr( context, - com.android.internal.R.attr.textColorPrimary + com.android.internal.R.attr.textColorPrimary, ) .defaultColor @@ -365,7 +365,7 @@ class MediaDataProcessor( secureSettings.getBoolForUser( Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, true, - UserHandle.USER_CURRENT + UserHandle.USER_CURRENT, ) useQsMediaPlayer && flag @@ -386,7 +386,7 @@ class MediaDataProcessor( if (!allowMediaRecommendations) { dismissSmartspaceRecommendation( key = mediaDataRepository.smartspaceMediaData.value.targetId, - delay = 0L + delay = 0L, ) } } @@ -413,7 +413,7 @@ class MediaDataProcessor( token: MediaSession.Token, appName: String, appIntent: PendingIntent, - packageName: String + packageName: String, ) { // Resume controls don't have a notification key, so store by package name instead if (!mediaDataRepository.mediaEntries.value.containsKey(packageName)) { @@ -449,7 +449,7 @@ class MediaDataProcessor( token, appName, appIntent, - packageName + packageName, ) } } else { @@ -461,7 +461,7 @@ class MediaDataProcessor( token, appName, appIntent, - packageName + packageName, ) } } @@ -582,30 +582,37 @@ class MediaDataProcessor( /** Called when the player's [PlaybackState] has been updated with new actions and/or state */ internal fun updateState(key: String, state: PlaybackState) { mediaDataRepository.mediaEntries.value.get(key)?.let { - val token = it.token - if (token == null) { - if (DEBUG) Log.d(TAG, "State updated, but token was null") - return - } - val actions = - createActionsFromState( - it.packageName, - mediaControllerFactory.create(it.token), - UserHandle(it.userId) - ) + applicationScope.launch { + withContext(backgroundDispatcher) { + val token = it.token + if (token == null) { + if (DEBUG) Log.d(TAG, "State updated, but token was null") + return@withContext + } + val actions = + createActionsFromState( + it.packageName, + mediaControllerFactory.create(it.token), + UserHandle(it.userId), + ) - // Control buttons - // If flag is enabled and controller has a PlaybackState, - // create actions from session info - // otherwise, no need to update semantic actions. - val data = - if (actions != null) { - it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state)) - } else { - it.copy(isPlaying = isPlayingState(state.state)) + // Control buttons + // If flag is enabled and controller has a PlaybackState, + // create actions from session info + // otherwise, no need to update semantic actions. + val data = + if (actions != null) { + it.copy( + semanticActions = actions, + isPlaying = isPlayingState(state.state), + ) + } else { + it.copy(isPlaying = isPlayingState(state.state)) + } + if (DEBUG) Log.d(TAG, "State updated outside of notification") + withContext(mainDispatcher) { onMediaDataLoaded(key, key, data) } } - if (DEBUG) Log.d(TAG, "State updated outside of notification") - onMediaDataLoaded(key, key, data) + } } } @@ -633,7 +640,7 @@ class MediaDataProcessor( } foregroundExecutor.executeDelayed( { removeEntry(key, userInitiated = userInitiated) }, - delayMs + delayMs, ) return existed } @@ -657,7 +664,7 @@ class MediaDataProcessor( if (mediaDataRepository.dismissSmartspaceRecommendation(key)) { foregroundExecutor.executeDelayed( { notifySmartspaceMediaDataRemoved(key, immediately = true) }, - delay + delay, ) } } @@ -677,7 +684,7 @@ class MediaDataProcessor( token: MediaSession.Token, appName: String, appIntent: PendingIntent, - packageName: String + packageName: String, ) = withContext(backgroundDispatcher) { val lastActive = systemClock.elapsedRealtime() @@ -694,7 +701,7 @@ class MediaDataProcessor( token, appName, appIntent, - packageName + packageName, ) if (result == null || desc.title.isNullOrBlank()) { Log.d(TAG, "No MediaData result for resumption") @@ -733,7 +740,7 @@ class MediaDataProcessor( appUid = result.appUid, isExplicit = result.isExplicit, resumeProgress = result.resumeProgress, - ) + ), ) } } @@ -746,7 +753,7 @@ class MediaDataProcessor( token: MediaSession.Token, appName: String, appIntent: PendingIntent, - packageName: String + packageName: String, ) { if (desc.title.isNullOrBlank()) { Log.e(TAG, "Description incomplete") @@ -818,7 +825,7 @@ class MediaDataProcessor( isExplicit = isExplicit, resumeProgress = progress, smartspaceId = SmallHash.hash(appUid + systemClock.currentTimeMillis().toInt()), - ) + ), ) } } @@ -887,14 +894,14 @@ class MediaDataProcessor( result.appUid, sbn.packageName, instanceId, - result.playbackLocation + result.playbackLocation, ) } else if (result.playbackLocation != oldEntry?.playbackLocation) { logger.logPlaybackLocationChange( result.appUid, sbn.packageName, instanceId, - result.playbackLocation + result.playbackLocation, ) } @@ -911,7 +918,7 @@ class MediaDataProcessor( val token = sbn.notification.extras.getParcelable( Notification.EXTRA_MEDIA_SESSION, - MediaSession.Token::class.java + MediaSession.Token::class.java, ) if (token == null) { return @@ -923,7 +930,7 @@ class MediaDataProcessor( val appInfo = notif.extras.getParcelable( Notification.EXTRA_BUILDER_APPLICATION_INFO, - ApplicationInfo::class.java + ApplicationInfo::class.java, ) ?: getAppInfoFromPackage(sbn.packageName) // App name @@ -987,7 +994,7 @@ class MediaDataProcessor( val deviceIntent = extras.getParcelable( Notification.EXTRA_MEDIA_REMOTE_INTENT, - PendingIntent::class.java + PendingIntent::class.java, ) Log.d(TAG, "$key is RCN for $deviceName") @@ -1003,7 +1010,7 @@ class MediaDataProcessor( deviceDrawable, deviceName, deviceIntent, - showBroadcastButton = false + showBroadcastButton = false, ) } } @@ -1093,7 +1100,7 @@ class MediaDataProcessor( mediaData.copy( resumeAction = oldResumeAction, hasCheckedForResume = oldHasCheckedForResume, - active = oldActive + active = oldActive, ) onMediaDataLoaded(key, oldKey, mediaData) } @@ -1102,7 +1109,7 @@ class MediaDataProcessor( private fun logSingleVsMultipleMediaAdded( appUid: Int, packageName: String, - instanceId: InstanceId + instanceId: InstanceId, ) { if (mediaDataRepository.mediaEntries.value.size == 1) { logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId) @@ -1151,7 +1158,7 @@ class MediaDataProcessor( private fun createActionsFromState( packageName: String, controller: MediaController, - user: UserHandle + user: UserHandle, ): MediaButton? { if (!mediaFlags.areMediaSessionActionsEnabled(packageName, user)) { return null @@ -1189,7 +1196,7 @@ class MediaDataProcessor( packageName, ContentProvider.getUriWithoutUserId(uri), Intent.FLAG_GRANT_READ_URI_PERMISSION, - ContentProvider.getUserIdFromUri(uri, userId) + ContentProvider.getUserIdFromUri(uri, userId), ) return loadBitmapFromUri(uri) } catch (e: SecurityException) { @@ -1226,7 +1233,7 @@ class MediaDataProcessor( val scale = MediaDataUtils.getScaleFactor( APair(width, height), - APair(artworkWidth, artworkHeight) + APair(artworkWidth, artworkHeight), ) // Downscale if needed @@ -1251,7 +1258,7 @@ class MediaDataProcessor( .loadDrawable(context), action, context.getString(R.string.controls_media_resume), - context.getDrawable(R.drawable.ic_media_play_container) + context.getDrawable(R.drawable.ic_media_play_container), ) } @@ -1291,7 +1298,7 @@ class MediaDataProcessor( } else { notifySmartspaceMediaDataRemoved( smartspaceMediaData.targetId, - immediately = false + immediately = false, ) mediaDataRepository.setRecommendation( SmartspaceMediaData( @@ -1362,7 +1369,7 @@ class MediaDataProcessor( private fun handlePossibleRemoval( key: String, removed: MediaData, - notificationRemoved: Boolean = false + notificationRemoved: Boolean = false, ) { val hasSession = removed.token != null if (hasSession && removed.semanticActions != null) { @@ -1387,7 +1394,7 @@ class MediaDataProcessor( Log.d( TAG, "Notification ($notificationRemoved) and/or session " + - "($hasSession) gone for inactive player $key" + "($hasSession) gone for inactive player $key", ) } convertToResumePlayer(key, removed) @@ -1513,7 +1520,7 @@ class MediaDataProcessor( data: MediaData, immediately: Boolean = true, receivedSmartspaceCardLatency: Int = 0, - isSsReactivated: Boolean = false + isSsReactivated: Boolean = false, ) {} /** @@ -1526,7 +1533,7 @@ class MediaDataProcessor( fun onSmartspaceMediaDataLoaded( key: String, data: SmartspaceMediaData, - shouldPrioritize: Boolean = false + shouldPrioritize: Boolean = false, ) {} /** Called whenever a previously existing Media notification was removed. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt index 275f1eecd4db..39cedc36dbec 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt @@ -128,7 +128,7 @@ constructor( data: MediaData, immediately: Boolean, receivedSmartspaceCardLatency: Int, - isSsReactivated: Boolean + isSsReactivated: Boolean, ) { var reusedListener: PlaybackStateListener? = null @@ -183,7 +183,7 @@ constructor( override fun onSmartspaceMediaDataLoaded( key: String, data: SmartspaceMediaData, - shouldPrioritize: Boolean + shouldPrioritize: Boolean, ) { if (!mediaFlags.isPersistentSsCardEnabled()) return @@ -259,7 +259,9 @@ constructor( } override fun onPlaybackStateChanged(state: PlaybackState?) { - processState(state, dispatchEvents = true, currentResumption = resumption) + bgExecutor.execute { + processState(state, dispatchEvents = true, currentResumption = resumption) + } } override fun onSessionDestroyed() { @@ -276,6 +278,7 @@ constructor( } } + @WorkerThread private fun processState( state: PlaybackState?, dispatchEvents: Boolean, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt index a0fb0bf25c7b..72650ea89dcb 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt @@ -16,6 +16,7 @@ package com.android.systemui.media.controls.ui.controller +import android.annotation.WorkerThread import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -41,6 +42,7 @@ import com.android.internal.logging.InstanceId import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.Dumpable +import com.android.systemui.Flags.mediaControlsUmoInflationInBackground import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background @@ -137,7 +139,7 @@ constructor( private val activityStarter: ActivityStarter, private val systemClock: SystemClock, @Main private val mainDispatcher: CoroutineDispatcher, - @Main executor: DelayableExecutor, + @Main private val uiExecutor: DelayableExecutor, @Background private val bgExecutor: Executor, @Background private val backgroundDispatcher: CoroutineDispatcher, private val mediaManager: MediaDataManager, @@ -227,7 +229,7 @@ constructor( private var carouselLocale: Locale? = null private val animationScaleObserver: ContentObserver = - object : ContentObserver(executor, 0) { + object : ContentObserver(uiExecutor, 0) { override fun onChange(selfChange: Boolean) { if (!SceneContainerFlag.isEnabled) { MediaPlayerData.players().forEach { it.updateAnimatorDurationScale() } @@ -350,7 +352,7 @@ constructor( MediaCarouselScrollHandler( mediaCarousel, pageIndicator, - executor, + uiExecutor, this::onSwipeToDismiss, this::updatePageIndicatorLocation, this::updateSeekbarListening, @@ -458,7 +460,17 @@ constructor( isSsReactivated: Boolean, ) { debugLogger.logMediaLoaded(key, data.active) - if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated)) { + val onUiExecutionEnd = + if (mediaControlsUmoInflationInBackground()) { + Runnable { + if (immediately) { + updateHostVisibility() + } + } + } else { + null + } + if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated, onUiExecutionEnd)) { // Log card received if a new resumable media card is added MediaPlayerData.getMediaPlayer(key)?.let { logSmartspaceCardReported( @@ -980,6 +992,7 @@ constructor( oldKey: String?, data: MediaData, isSsReactivated: Boolean, + onUiExecutionEnd: Runnable? = null, ): Boolean = traceSection("MediaCarouselController#addOrUpdatePlayer") { MediaPlayerData.moveIfExists(oldKey, key) @@ -987,76 +1000,119 @@ constructor( val curVisibleMediaKey = MediaPlayerData.visiblePlayerKeys() .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) - if (existingPlayer == null) { - val newPlayer = mediaControlPanelFactory.get() - if (SceneContainerFlag.isEnabled) { - newPlayer.mediaViewController.widthInSceneContainerPx = widthInSceneContainerPx - newPlayer.mediaViewController.heightInSceneContainerPx = - heightInSceneContainerPx - } - newPlayer.attachPlayer( - MediaViewHolder.create(LayoutInflater.from(context), mediaContent) - ) - newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions - val lp = - LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT, - ) - newPlayer.mediaViewHolder?.player?.setLayoutParams(lp) - newPlayer.bindPlayer(data, key) - newPlayer.setListening( - mediaCarouselScrollHandler.visibleToUser && currentlyExpanded - ) - MediaPlayerData.addMediaPlayer( - key, - data, - newPlayer, - systemClock, - isSsReactivated, - debugLogger, - ) - updateViewControllerToState(newPlayer.mediaViewController, noAnimation = true) - // Media data added from a recommendation card should starts playing. - if ( - (shouldScrollToKey && data.isPlaying == true) || - (!shouldScrollToKey && data.active) - ) { - reorderAllPlayers(curVisibleMediaKey, key) + if (mediaControlsUmoInflationInBackground()) { + if (existingPlayer == null) { + bgExecutor.execute { + val mediaViewHolder = createMediaViewHolderInBg() + // Add the new player in the main thread. + uiExecutor.execute { + setupNewPlayer( + key, + data, + isSsReactivated, + curVisibleMediaKey, + mediaViewHolder, + ) + updatePageIndicator() + mediaCarouselScrollHandler.onPlayersChanged() + mediaFrame.requiresRemeasuring = true + onUiExecutionEnd?.run() + } + } } else { - needsReordering = true + updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer) + updatePageIndicator() + mediaCarouselScrollHandler.onPlayersChanged() + mediaFrame.requiresRemeasuring = true + onUiExecutionEnd?.run() } } else { - existingPlayer.bindPlayer(data, key) - MediaPlayerData.addMediaPlayer( - key, - data, - existingPlayer, - systemClock, - isSsReactivated, - debugLogger, - ) - val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String() - // In case of recommendations hits. - // Check the playing status of media player and the package name. - // To make sure we scroll to the right app's media player. - if ( - isReorderingAllowed || - shouldScrollToKey && - data.isPlaying == true && - packageName == data.packageName - ) { - reorderAllPlayers(curVisibleMediaKey, key) + if (existingPlayer == null) { + val mediaViewHolder = + MediaViewHolder.create(LayoutInflater.from(context), mediaContent) + setupNewPlayer(key, data, isSsReactivated, curVisibleMediaKey, mediaViewHolder) } else { - needsReordering = true + updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer) } + updatePageIndicator() + mediaCarouselScrollHandler.onPlayersChanged() + mediaFrame.requiresRemeasuring = true + onUiExecutionEnd?.run() } - updatePageIndicator() - mediaCarouselScrollHandler.onPlayersChanged() - mediaFrame.requiresRemeasuring = true return existingPlayer == null } + private fun updatePlayer( + key: String, + data: MediaData, + isSsReactivated: Boolean, + curVisibleMediaKey: MediaPlayerData.MediaSortKey?, + existingPlayer: MediaControlPanel, + ) { + existingPlayer.bindPlayer(data, key) + MediaPlayerData.addMediaPlayer( + key, + data, + existingPlayer, + systemClock, + isSsReactivated, + debugLogger, + ) + val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String() + // In case of recommendations hits. + // Check the playing status of media player and the package name. + // To make sure we scroll to the right app's media player. + if ( + isReorderingAllowed || + shouldScrollToKey && data.isPlaying == true && packageName == data.packageName + ) { + reorderAllPlayers(curVisibleMediaKey, key) + } else { + needsReordering = true + } + } + + private fun setupNewPlayer( + key: String, + data: MediaData, + isSsReactivated: Boolean, + curVisibleMediaKey: MediaPlayerData.MediaSortKey?, + mediaViewHolder: MediaViewHolder, + ) { + val newPlayer = mediaControlPanelFactory.get() + newPlayer.attachPlayer(mediaViewHolder) + newPlayer.mediaViewController.sizeChangedListener = + this@MediaCarouselController::updateCarouselDimensions + val lp = + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + newPlayer.mediaViewHolder?.player?.setLayoutParams(lp) + newPlayer.bindPlayer(data, key) + newPlayer.setListening(mediaCarouselScrollHandler.visibleToUser && currentlyExpanded) + MediaPlayerData.addMediaPlayer( + key, + data, + newPlayer, + systemClock, + isSsReactivated, + debugLogger, + ) + updateViewControllerToState(newPlayer.mediaViewController, noAnimation = true) + // Media data added from a recommendation card should starts playing. + if ((shouldScrollToKey && data.isPlaying == true) || (!shouldScrollToKey && data.active)) { + reorderAllPlayers(curVisibleMediaKey, key) + } else { + needsReordering = true + } + } + + @WorkerThread + private fun createMediaViewHolderInBg(): MediaViewHolder { + return MediaViewHolder.create(LayoutInflater.from(context), mediaContent) + } + private fun addSmartspaceMediaRecommendations( key: String, data: SmartspaceMediaData, @@ -1173,8 +1229,16 @@ constructor( val previousVisibleKey = MediaPlayerData.visiblePlayerKeys() .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) + val onUiExecutionEnd = Runnable { + if (recreateMedia) { + reorderAllPlayers(previousVisibleKey) + } + } - MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) -> + val mediaDataList = MediaPlayerData.mediaData() + // Do not loop through the original list of media data because the re-addition of media data + // is being executed in background thread. + mediaDataList.forEach { (key, data, isSsMediaRec) -> if (isSsMediaRec) { val smartspaceMediaData = MediaPlayerData.smartspaceMediaData removePlayer(key, dismissMediaData = false, dismissRecommendation = false) @@ -1185,6 +1249,7 @@ constructor( MediaPlayerData.shouldPrioritizeSs, ) } + onUiExecutionEnd.run() } else { val isSsReactivated = MediaPlayerData.isSsReactivated(key) if (recreateMedia) { @@ -1195,11 +1260,9 @@ constructor( oldKey = null, data = data, isSsReactivated = isSsReactivated, + onUiExecutionEnd = onUiExecutionEnd, ) } - if (recreateMedia) { - reorderAllPlayers(previousVisibleKey) - } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManager.kt index 8660d12bcb85..782da4bd6a41 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManager.kt @@ -17,6 +17,7 @@ package com.android.systemui.media.controls.ui.controller import com.android.app.tracing.traceSection +import com.android.systemui.Flags.mediaControlsUmoInflationInBackground import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.controls.ui.view.MediaHostState import com.android.systemui.util.animation.MeasurementOutput @@ -71,23 +72,34 @@ class MediaHostStatesManager @Inject constructor() { */ fun updateCarouselDimensions( @MediaLocation location: Int, - hostState: MediaHostState + hostState: MediaHostState, ): MeasurementOutput = traceSection("MediaHostStatesManager#updateCarouselDimensions") { val result = MeasurementOutput(0, 0) + var changed = false for (controller in controllers) { val measurement = controller.getMeasurementsForState(hostState) measurement?.let { if (it.measuredHeight > result.measuredHeight) { result.measuredHeight = it.measuredHeight + changed = true } if (it.measuredWidth > result.measuredWidth) { result.measuredWidth = it.measuredWidth + changed = true } } } - carouselSizes[location] = result - return result + if (mediaControlsUmoInflationInBackground()) { + // Set carousel size if result measurements changed. This avoids setting carousel + // size when this method gets called before the addition of media view controllers + if (!carouselSizes.contains(location) || changed) { + carouselSizes[location] = result + } + } else { + carouselSizes[location] = result + } + return carouselSizes[location] ?: result } /** Add a callback to be called when a MediaState has updated */ diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt index 09a618110f21..5ddc3470da43 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt @@ -20,6 +20,7 @@ import android.graphics.Rect import android.util.ArraySet import android.view.View import android.view.View.OnAttachStateChangeListener +import com.android.systemui.Flags.mediaControlsUmoInflationInBackground import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.SmartspaceMediaData @@ -91,8 +92,10 @@ class MediaHost( data: MediaData, immediately: Boolean, receivedSmartspaceCardLatency: Int, - isSsReactivated: Boolean + isSsReactivated: Boolean, ) { + if (mediaControlsUmoInflationInBackground()) return + if (immediately) { updateViewVisibility() } @@ -101,7 +104,7 @@ class MediaHost( override fun onSmartspaceMediaDataLoaded( key: String, data: SmartspaceMediaData, - shouldPrioritize: Boolean + shouldPrioritize: Boolean, ) { updateViewVisibility() } @@ -171,7 +174,7 @@ class MediaHost( input.widthMeasureSpec = View.MeasureSpec.makeMeasureSpec( View.MeasureSpec.getSize(input.widthMeasureSpec), - View.MeasureSpec.EXACTLY + View.MeasureSpec.EXACTLY, ) } // This will trigger a state change that ensures that we now have a state diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt index 228b57603bed..d413474fde90 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt @@ -54,6 +54,7 @@ import com.android.systemui.mediaprojection.MediaProjectionServiceHelper import com.android.systemui.mediaprojection.appselector.data.RecentTask import com.android.systemui.mediaprojection.appselector.view.MediaProjectionRecentsViewController import com.android.systemui.res.R +import com.android.systemui.shared.system.ActivityManagerWrapper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.AsyncActivityLauncher import java.lang.IllegalArgumentException @@ -62,9 +63,10 @@ import javax.inject.Inject class MediaProjectionAppSelectorActivity( private val componentFactory: MediaProjectionAppSelectorComponent.Factory, private val activityLauncher: AsyncActivityLauncher, + private val activityManager: ActivityManagerWrapper, /** This is used to override the dependency in a screenshot test */ @VisibleForTesting - private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)? + private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?, ) : ChooserActivity(), MediaProjectionAppSelectorView, @@ -74,8 +76,9 @@ class MediaProjectionAppSelectorActivity( @Inject constructor( componentFactory: MediaProjectionAppSelectorComponent.Factory, - activityLauncher: AsyncActivityLauncher - ) : this(componentFactory, activityLauncher, listControllerFactory = null) + activityLauncher: AsyncActivityLauncher, + activityManager: ActivityManagerWrapper, + ) : this(componentFactory, activityLauncher, activityManager, listControllerFactory = null) private val lifecycleRegistry = LifecycleRegistry(this) override val lifecycle = lifecycleRegistry @@ -100,7 +103,7 @@ class MediaProjectionAppSelectorActivity( callingPackage = callingPackage, view = this, resultHandler = this, - isFirstStart = savedInstanceState == null + isFirstStart = savedInstanceState == null, ) component.lifecycleObservers.forEach { lifecycle.addObserver(it) } @@ -113,7 +116,7 @@ class MediaProjectionAppSelectorActivity( intent.configureChooserIntent( resources, component.hostUserHandle, - component.personalProfileUserHandle + component.personalProfileUserHandle, ) reviewGrantedConsentRequired = @@ -180,7 +183,13 @@ class MediaProjectionAppSelectorActivity( // is created and ready to be captured. val activityStarted = activityLauncher.startActivityAsUser(intent, userHandle, activityOptions.toBundle()) { - returnSelectedApp(launchCookie, taskId = -1) + if (targetInfo.resolvedComponentName == callingActivity) { + // If attempting to launch the app used to launch the MediaProjection, then + // provide the task id since the launch cookie won't match the existing task + returnSelectedApp(launchCookie, taskId = activityManager.runningTask.taskId) + } else { + returnSelectedApp(launchCookie, taskId = -1) + } } // Rely on the ActivityManager to pop up a dialog regarding app suspension @@ -213,7 +222,7 @@ class MediaProjectionAppSelectorActivity( MediaProjectionServiceHelper.setReviewedConsentIfNeeded( RECORD_CANCEL, reviewGrantedConsentRequired, - /* projection= */ null + /* projection= */ null, ) if (isFinishing) { // Only log dismissed when actually finishing, and not when changing configuration. @@ -246,7 +255,7 @@ class MediaProjectionAppSelectorActivity( val resultReceiver = intent.getParcelableExtra( EXTRA_CAPTURE_REGION_RESULT_RECEIVER, - ResultReceiver::class.java + ResultReceiver::class.java, ) as ResultReceiver val captureRegion = MediaProjectionCaptureTarget(launchCookie, taskId) val data = Bundle().apply { putParcelable(KEY_CAPTURE_TARGET, captureRegion) } @@ -260,8 +269,8 @@ class MediaProjectionAppSelectorActivity( val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION) val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder) - projection.setLaunchCookie(launchCookie) - projection.setTaskId(taskId) + projection.launchCookie = launchCookie + projection.taskId = taskId val intent = Intent() intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder()) @@ -270,7 +279,7 @@ class MediaProjectionAppSelectorActivity( MediaProjectionServiceHelper.setReviewedConsentIfNeeded( RECORD_CONTENT_TASK, reviewGrantedConsentRequired, - projection + projection, ) } @@ -457,7 +466,7 @@ class MediaProjectionAppSelectorActivity( */ private class RecyclerViewExpandingAccessibilityDelegate( rdl: ResolverDrawerLayout, - view: RecyclerView + view: RecyclerView, ) : RecyclerViewAccessibilityDelegate(view) { private val delegate = AppListAccessibilityDelegate(rdl) @@ -465,7 +474,7 @@ class MediaProjectionAppSelectorActivity( override fun onRequestSendAccessibilityEvent( host: ViewGroup, child: View, - event: AccessibilityEvent + event: AccessibilityEvent, ): Boolean { super.onRequestSendAccessibilityEvent(host, child, event) return delegate.onRequestSendAccessibilityEvent(host, child, event) 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 8351597f35de..c3729c0dcdfd 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java @@ -68,12 +68,12 @@ import com.android.systemui.res.R; import com.android.systemui.statusbar.phone.AlertDialogWithDelegate; import com.android.systemui.statusbar.phone.SystemUIDialog; +import dagger.Lazy; + import java.util.function.Consumer; import javax.inject.Inject; -import dagger.Lazy; - public class MediaProjectionPermissionActivity extends Activity { private static final String TAG = "MediaProjectionPermissionActivity"; private static final float MAX_APP_NAME_SIZE_PX = 500f; @@ -132,8 +132,7 @@ public class MediaProjectionPermissionActivity extends Activity { mPackageName = launchingIntent.getStringExtra( EXTRA_PACKAGE_REUSING_GRANTED_CONSENT); } else { - setResult(RESULT_CANCELED); - finish(RECORD_CANCEL, /* projection= */ null); + finishAsCancelled(); return; } } @@ -145,8 +144,7 @@ public class MediaProjectionPermissionActivity extends Activity { mUid = aInfo.uid; } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Unable to look up package name", e); - setResult(RESULT_CANCELED); - finish(RECORD_CANCEL, /* projection= */ null); + finishAsCancelled(); return; } @@ -176,15 +174,13 @@ public class MediaProjectionPermissionActivity extends Activity { } } catch (RemoteException e) { Log.e(TAG, "Error checking projection permissions", e); - setResult(RESULT_CANCELED); - finish(RECORD_CANCEL, /* projection= */ null); + finishAsCancelled(); return; } if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) { if (showScreenCaptureDisabledDialogIfNeeded()) { - setResult(RESULT_CANCELED); - finish(RECORD_CANCEL, /* projection= */ null); + finishAsCancelled(); return; } } @@ -346,6 +342,21 @@ public class MediaProjectionPermissionActivity extends Activity { private void requestDeviceUnlock() { mKeyguardManager.requestDismissKeyguard(this, new KeyguardManager.KeyguardDismissCallback() { + + @Override + public void onDismissError() { + if (com.android.systemui.Flags.mediaProjectionDialogBehindLockscreen()) { + finishAsCancelled(); + } + } + + @Override + public void onDismissCancelled() { + if (com.android.systemui.Flags.mediaProjectionDialogBehindLockscreen()) { + finishAsCancelled(); + } + } + @Override public void onDismissSucceeded() { mDialog.show(); @@ -386,8 +397,7 @@ public class MediaProjectionPermissionActivity extends Activity { } } catch (RemoteException e) { Log.e(TAG, "Error granting projection permission", e); - setResult(RESULT_CANCELED); - finish(RECORD_CANCEL, /* projection= */ null); + finishAsCancelled(); } finally { if (mDialog != null) { mDialog.dismiss(); @@ -436,6 +446,14 @@ public class MediaProjectionPermissionActivity extends Activity { } } + /** + * Finishes this activity and cancel the projection request. + */ + private void finishAsCancelled() { + setResult(RESULT_CANCELED); + finish(RECORD_CANCEL, /* projection= */ null); + } + @Nullable private MediaProjectionConfig getMediaProjectionConfig() { Intent intent = getIntent(); diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt index 5be225c718ea..0e5404164ba1 100644 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt @@ -16,12 +16,19 @@ package com.android.systemui.notifications.ui.viewmodel +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.scene.domain.interactor.SceneInteractor -import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch /** * Models UI state used to render the content of the notifications shade overlay. @@ -34,13 +41,40 @@ class NotificationsShadeOverlayContentViewModel constructor( val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, - private val sceneInteractor: SceneInteractor, -) { + val sceneInteractor: SceneInteractor, + private val shadeInteractor: ShadeInteractor, +) : ExclusiveActivatable() { + + override suspend fun onActivated(): Nothing { + coroutineScope { + launch { + sceneInteractor.currentScene.collect { currentScene -> + when (currentScene) { + // TODO(b/369513770): The ShadeSession should be preserved in this scenario. + Scenes.Bouncer -> + shadeInteractor.collapseNotificationsShade( + loggingReason = "bouncer shown while shade is open" + ) + } + } + } + + launch { + shadeInteractor.isShadeTouchable + .distinctUntilChanged() + .filter { !it } + .collect { + shadeInteractor.collapseNotificationsShade( + loggingReason = "device became non-interactive" + ) + } + } + } + awaitCancellation() + } + fun onScrimClicked() { - sceneInteractor.hideOverlay( - overlay = Overlays.NotificationsShade, - loggingReason = "Shade scrim clicked", - ) + shadeInteractor.collapseNotificationsShade(loggingReason = "shade scrim clicked") } @AssistedFactory 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 ba0d9384c7a4..66ac01ab95a0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -180,6 +180,7 @@ constructor( qqsMediaHost.init(MediaHierarchyManager.LOCATION_QQS) qsMediaHost.init(MediaHierarchyManager.LOCATION_QS) setListenerCollections() + lifecycleScope.launch { viewModel.activate() } } override fun onCreateView( @@ -331,7 +332,7 @@ constructor( } override fun setOverscrolling(overscrolling: Boolean) { - viewModel.stackScrollerOverscrollingValue = overscrolling + viewModel.isStackScrollerOverscrolling = overscrolling } override fun setExpanded(qsExpanded: Boolean) { @@ -410,11 +411,11 @@ constructor( qsTransitionFraction: Float, qsSquishinessFraction: Float, ) { - super.setTransitionToFullShadeProgress( - isTransitioningToFullShade, - qsTransitionFraction, - qsSquishinessFraction, - ) + viewModel.isTransitioningToFullShade = isTransitioningToFullShade + viewModel.lockscreenToShadeProgressValue = qsTransitionFraction + if (isTransitioningToFullShade) { + viewModel.squishinessFractionValue = qsSquishinessFraction + } } override fun setFancyClipping( 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 7300ee1053ff..2d4e358414d5 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 @@ -24,16 +24,19 @@ import androidx.lifecycle.LifecycleCoroutineScope import com.android.systemui.Dumpable import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.FooterActionsController +import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel.QSExpansionState import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel +import com.android.systemui.qs.panels.domain.interactor.TileSquishinessInteractor import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.transition.LargeScreenShadeInterpolator import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository -import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.util.LargeScreenUtils import com.android.systemui.util.asIndenting import com.android.systemui.util.printSection @@ -50,6 +53,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -61,13 +65,14 @@ constructor( private val footerActionsViewModelFactory: FooterActionsViewModel.Factory, private val footerActionsController: FooterActionsController, private val sysuiStatusBarStateController: SysuiStatusBarStateController, - private val keyguardBypassController: KeyguardBypassController, + private val deviceEntryInteractor: DeviceEntryInteractor, private val disableFlagsRepository: DisableFlagsRepository, private val largeScreenShadeInterpolator: LargeScreenShadeInterpolator, private val configurationInteractor: ConfigurationInteractor, private val largeScreenHeaderHelper: LargeScreenHeaderHelper, + private val squishinessInteractor: TileSquishinessInteractor, @Assisted private val lifecycleScope: LifecycleCoroutineScope, -) : Dumpable { +) : Dumpable, ExclusiveActivatable() { val footerActionsViewModel = footerActionsViewModelFactory.create(lifecycleScope).also { lifecycleScope.launch { footerActionsController.init() } @@ -110,7 +115,7 @@ constructor( _panelFraction.value = value } - private val _squishinessFraction = MutableStateFlow(0f) + private val _squishinessFraction = MutableStateFlow(1f) var squishinessFractionValue: Float get() = _squishinessFraction.value set(value) { @@ -131,7 +136,7 @@ constructor( private val _headerAnimating = MutableStateFlow(false) private val _stackScrollerOverscrolling = MutableStateFlow(false) - var stackScrollerOverscrollingValue: Boolean + var isStackScrollerOverscrolling: Boolean get() = _stackScrollerOverscrolling.value set(value) { _stackScrollerOverscrolling.value = value @@ -150,8 +155,6 @@ constructor( disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled(), ) - private val _showCollapsedOnKeyguard = MutableStateFlow(false) - private val _keyguardAndExpanded = MutableStateFlow(false) /** @@ -177,21 +180,65 @@ constructor( awaitClose { sysuiStatusBarStateController.removeCallback(callback) } } + .onStart { emit(sysuiStatusBarStateController.state) } .stateIn( lifecycleScope, SharingStarted.WhileSubscribed(), sysuiStatusBarStateController.state, ) + private val isKeyguardState = + statusBarState + .map { it == StatusBarState.KEYGUARD } + .stateIn( + lifecycleScope, + SharingStarted.WhileSubscribed(), + statusBarState.value == StatusBarState.KEYGUARD, + ) + private val _viewHeight = MutableStateFlow(0) private val _headerTranslation = MutableStateFlow(0f) private val _inSplitShade = MutableStateFlow(false) + var isInSplitShade: Boolean + get() = _inSplitShade.value + set(value) { + _inSplitShade.value = value + } private val _transitioningToFullShade = MutableStateFlow(false) + var isTransitioningToFullShade: Boolean + get() = _transitioningToFullShade.value + set(value) { + _transitioningToFullShade.value = value + } - private val _lockscreenToShadeProgress = MutableStateFlow(false) + private val isBypassEnabled = deviceEntryInteractor.isBypassEnabled + + private val showCollapsedOnKeyguard = + combine( + isBypassEnabled, + _transitioningToFullShade, + _inSplitShade, + ::calculateShowCollapsedOnKeyguard, + ) + .stateIn( + lifecycleScope, + SharingStarted.WhileSubscribed(), + calculateShowCollapsedOnKeyguard( + isBypassEnabled.value, + isTransitioningToFullShade, + isInSplitShade, + ), + ) + + private val _lockscreenToShadeProgress = MutableStateFlow(0.0f) + var lockscreenToShadeProgressValue: Float + get() = _lockscreenToShadeProgress.value + set(value) { + _lockscreenToShadeProgress.value = value + } private val _overscrolling = MutableStateFlow(false) @@ -212,12 +259,32 @@ constructor( _heightOverride.value = value } + private val forceQS = + combine( + _qsExpanded, + _stackScrollerOverscrolling, + isKeyguardState, + showCollapsedOnKeyguard, + ::calculateForceQs, + ) + .stateIn( + lifecycleScope, + SharingStarted.WhileSubscribed(), + calculateForceQs( + isQSExpanded, + isStackScrollerOverscrolling, + isKeyguardState.value, + showCollapsedOnKeyguard.value, + ), + ) + val expansionState: StateFlow<QSExpansionState> = - combine(_stackScrollerOverscrolling, _qsExpanded, _qsExpansion) { args: Array<Any> -> - val expansion = args[2] as Float - QSExpansionState(expansion.coerceIn(0f, 1f)) - } - .stateIn(lifecycleScope, SharingStarted.WhileSubscribed(), QSExpansionState(0f)) + combine(_qsExpansion, forceQS, ::calculateExpansionState) + .stateIn( + lifecycleScope, + SharingStarted.WhileSubscribed(), + calculateExpansionState(_qsExpansion.value, forceQS.value), + ) /** * Accessibility action for collapsing/expanding QS. The provided runnable is responsible for @@ -225,6 +292,16 @@ constructor( */ var collapseExpandAccessibilityAction: Runnable? = null + override suspend fun onActivated(): Nothing { + hydrateSquishinessInteractor() + } + + private suspend fun hydrateSquishinessInteractor(): Nothing { + _squishinessFraction.collect { + squishinessInteractor.setSquishinessValue(it.constrainSquishiness()) + } + } + override fun dump(pw: PrintWriter, args: Array<out String>) { pw.asIndenting().run { printSection("Quick Settings state") { @@ -238,13 +315,17 @@ constructor( println("panelExpansionFraction", panelExpansionFractionValue) println("squishinessFraction", squishinessFractionValue) println("expansionState", expansionState.value) + println("forceQS", forceQS.value) } printSection("Shade state") { - println("stackOverscrolling", stackScrollerOverscrollingValue) + println("stackOverscrolling", isStackScrollerOverscrolling) println("statusBarState", StatusBarState.toString(statusBarState.value)) + println("isKeyguardState", isKeyguardState.value) println("isSmallScreen", isSmallScreenValue) println("heightOverride", "${heightOverrideValue}px") println("qqsHeaderHeight", "${qqsHeaderHeight.value}px") + println("isSplitShade", isInSplitShade) + println("showCollapsedOnKeyguard", showCollapsedOnKeyguard.value) } } } @@ -257,3 +338,35 @@ constructor( // In the future, this will have other relevant elements like squishiness. data class QSExpansionState(@FloatRange(0.0, 1.0) val progress: Float) } + +private fun Float.constrainSquishiness(): Float { + return (0.1f + this * 0.9f).coerceIn(0f, 1f) +} + +// Helper methods for combining flows. + +private fun calculateExpansionState(expansion: Float, forceQs: Boolean): QSExpansionState { + return if (forceQs) { + QSExpansionState(1f) + } else { + QSExpansionState(expansion.coerceIn(0f, 1f)) + } +} + +private fun calculateForceQs( + isQSExpanded: Boolean, + isStackOverScrolling: Boolean, + isKeyguardShowing: Boolean, + shouldShowCollapsedOnKeyguard: Boolean, +): Boolean { + return (isQSExpanded || isStackOverScrolling) && + (isKeyguardShowing && !shouldShowCollapsedOnKeyguard) +} + +private fun calculateShowCollapsedOnKeyguard( + isBypassEnabled: Boolean, + isTransitioningToFullShade: Boolean, + isInSplitShade: Boolean, +): Boolean { + return isBypassEnabled || (isTransitioningToFullShade && !isInSplitShade) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt index 278352c6f69b..ead38f3f9b52 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt @@ -33,6 +33,7 @@ import com.android.systemui.log.core.LogLevel.VERBOSE import com.android.systemui.log.dagger.QSConfigLog import com.android.systemui.log.dagger.QSLog import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.plugins.qs.QSTile.State import com.android.systemui.statusbar.StatusBarState import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject @@ -57,6 +58,7 @@ constructor( fun d(@CompileTimeConstant msg: String, arg: Any) { buffer.log(TAG, DEBUG, { str1 = arg.toString() }, { "$msg: $str1" }) } + fun i(@CompileTimeConstant msg: String, arg: Any) { buffer.log(TAG, INFO, { str1 = arg.toString() }, { "$msg: $str1" }) } @@ -73,7 +75,19 @@ constructor( str1 = tileSpec str2 = reason }, - { "[$str1] Tile destroyed. Reason: $str2" } + { "[$str1] Tile destroyed. Reason: $str2" }, + ) + } + + fun logStateChanged(tileSpec: String, state: State) { + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + str2 = state.toString() + }, + { "[$str1] Tile state=$str2" }, ) } @@ -85,7 +99,7 @@ constructor( bool1 = listening str1 = tileSpec }, - { "[$str1] Tile listening=$bool1" } + { "[$str1] Tile listening=$bool1" }, ) } @@ -98,7 +112,7 @@ constructor( str1 = containerName str2 = allSpecs }, - { "Tiles listening=$bool1 in $str1. $str2" } + { "Tiles listening=$bool1 in $str1. $str2" }, ) } @@ -112,7 +126,7 @@ constructor( str2 = StatusBarState.toString(statusBarState) str3 = toStateString(state) }, - { "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3" } + { "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3" }, ) } @@ -124,7 +138,7 @@ constructor( str1 = tileSpec int1 = eventId }, - { "[$str1][$int1] Tile handling click." } + { "[$str1][$int1] Tile handling click." }, ) } @@ -138,7 +152,7 @@ constructor( str2 = StatusBarState.toString(statusBarState) str3 = toStateString(state) }, - { "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3" } + { "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3" }, ) } @@ -150,7 +164,7 @@ constructor( str1 = tileSpec int1 = eventId }, - { "[$str1][$int1] Tile handling secondary click." } + { "[$str1][$int1] Tile handling secondary click." }, ) } @@ -164,7 +178,7 @@ constructor( str2 = StatusBarState.toString(statusBarState) str3 = toStateString(state) }, - { "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3" } + { "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3" }, ) } @@ -176,7 +190,7 @@ constructor( str1 = tileSpec int1 = eventId }, - { "[$str1][$int1] Tile handling long click." } + { "[$str1][$int1] Tile handling long click." }, ) } @@ -189,7 +203,7 @@ constructor( int1 = lastType str2 = callback }, - { "[$str1] mLastTileState=$int1, Callback=$str2." } + { "[$str1] mLastTileState=$int1, Callback=$str2." }, ) } @@ -198,7 +212,7 @@ constructor( tileSpec: String, state: Int, disabledByPolicy: Boolean, - color: Int + color: Int, ) { // This method is added to further debug b/250618218 which has only been observed from the // InternetTile, so we are only logging the background color change for the InternetTile @@ -215,7 +229,7 @@ constructor( bool1 = disabledByPolicy int2 = color }, - { "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2." } + { "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2." }, ) } @@ -229,7 +243,7 @@ constructor( str3 = state.icon?.toString() int1 = state.state }, - { "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." } + { "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." }, ) } @@ -241,7 +255,7 @@ constructor( str1 = containerName bool1 = expanded }, - { "$str1 expanded=$bool1" } + { "$str1 expanded=$bool1" }, ) } @@ -253,7 +267,7 @@ constructor( str1 = containerName int1 = orientation }, - { "onViewAttached: $str1 orientation $int1" } + { "onViewAttached: $str1 orientation $int1" }, ) } @@ -265,7 +279,7 @@ constructor( str1 = containerName int1 = orientation }, - { "onViewDetached: $str1 orientation $int1" } + { "onViewDetached: $str1 orientation $int1" }, ) } @@ -276,7 +290,7 @@ constructor( newShouldUseSplitShade: Boolean, oldScreenLayout: Int, newScreenLayout: Int, - containerName: String + containerName: String, ) { configChangedBuffer.log( TAG, @@ -297,7 +311,7 @@ constructor( "screen layout=${toScreenLayoutString(long1.toInt())} " + "(was ${toScreenLayoutString(long2.toInt())}), " + "splitShade=$bool2 (was $bool1)" - } + }, ) } @@ -305,7 +319,7 @@ constructor( after: Boolean, before: Boolean, force: Boolean, - containerName: String + containerName: String, ) { buffer.log( TAG, @@ -316,7 +330,7 @@ constructor( bool2 = before bool3 = force }, - { "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3" } + { "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3" }, ) } @@ -328,7 +342,7 @@ constructor( int1 = tilesPerPageCount int2 = totalTilesCount }, - { "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]" } + { "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]" }, ) } @@ -340,7 +354,7 @@ constructor( str1 = tileName int1 = pageIndex }, - { "Adding $str1 to page number $int1" } + { "Adding $str1 to page number $int1" }, ) } @@ -361,7 +375,7 @@ constructor( str1 = viewName str2 = toVisibilityString(visibility) }, - { "$str1 visibility: $str2" } + { "$str1 visibility: $str2" }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepository.kt new file mode 100644 index 000000000000..76ba9af2f475 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepository.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.qs.panels.data.repository + +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +@SysUISingleton +class TileSquishinessRepository @Inject constructor() { + private val _squishiness = MutableStateFlow(1f) + val squishiness = _squishiness.asStateFlow() + + fun setSquishinessValue(value: Float) { + _squishiness.value = value + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractor.kt new file mode 100644 index 000000000000..4fdbc7647c78 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractor.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.qs.panels.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.qs.panels.data.repository.TileSquishinessRepository +import javax.inject.Inject + +@SysUISingleton +class TileSquishinessInteractor +@Inject +constructor(private val repository: TileSquishinessRepository) { + val squishiness = repository.squishiness + + fun setSquishinessValue(value: Float) { + repository.setSquishinessValue(value) + } +} 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 8998a7f5d815..a645b51404e7 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,6 +41,7 @@ fun SceneScope.QuickQuickSettings( val sizedTiles by viewModel.tileViewModels.collectAsStateWithLifecycle(initialValue = emptyList()) val tiles = sizedTiles.fastMap { it.tile } + val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle() DisposableEffect(tiles) { val token = Any() @@ -62,6 +63,7 @@ fun SceneScope.QuickQuickSettings( tile = it.tile, iconOnly = it.isIcon, modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)), + squishiness = { squishiness }, ) } } 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 8c2fb252d13c..bf4c113e3ae9 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 @@ -73,6 +73,7 @@ fun LargeTileContent( secondaryLabel: String?, icon: Icon, colors: TileColors, + squishiness: () -> Float, accessibilityUiState: AccessibilityUiState? = null, toggleClickSupported: Boolean = false, iconShape: Shape = RoundedCornerShape(CommonTileDefaults.InactiveCornerRadius), @@ -89,6 +90,7 @@ fun LargeTileContent( modifier = Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClickSupported) { Modifier.clip(iconShape) + .verticalSquish(squishiness) .background(colors.iconBackground, { 1f }) .combinedClickable( onClick = onClick, 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 e6edba513189..3ba49add530e 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 @@ -33,6 +33,7 @@ import com.android.systemui.qs.panels.ui.compose.rememberEditListState import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel +import com.android.systemui.qs.panels.ui.viewmodel.TileSquishinessViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.shared.ui.ElementKeys.toElementKey @@ -45,6 +46,7 @@ class InfiniteGridLayout constructor( private val iconTilesViewModel: IconTilesViewModel, private val gridSizeViewModel: FixedColumnsSizeViewModel, + private val squishinessViewModel: TileSquishinessViewModel, ) : PaginatableGridLayout { @Composable @@ -60,6 +62,7 @@ constructor( } val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle() val sizedTiles = tiles.map { SizedTileImpl(it, it.spec.width()) } + val squishiness by squishinessViewModel.squishiness.collectAsStateWithLifecycle() VerticalSpannedGrid( columns = columns, @@ -72,6 +75,7 @@ constructor( tile = it.tile, iconOnly = iconTilesViewModel.isIconTile(it.tile.spec), modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)), + squishiness = { squishiness }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/SquishTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/SquishTile.kt new file mode 100644 index 000000000000..ada1ef4de15d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/SquishTile.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.qs.panels.ui.compose.infinitegrid + +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layout +import kotlin.math.roundToInt + +/** + * Modifier to squish the vertical bounds of a composable (usually a QS tile). + * + * It will squish the vertical bounds of the inner composable node by the value returned by + * [squishiness] on the measure/layout pass. + * + * The squished composable will be center aligned. + */ +fun Modifier.verticalSquish(squishiness: () -> Float): Modifier { + return layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + val actualHeight = placeable.height + val squishedHeight = actualHeight * squishiness() + // Center the content by moving it UP (squishedHeight < actualHeight) + val scroll = (squishedHeight - actualHeight) / 2 + + layout(placeable.width, squishedHeight.roundToInt()) { + placeable.place(0, scroll.roundToInt()) + } + } +} 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 afcbed6db53b..4bd5b2d68c4c 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 @@ -98,7 +98,12 @@ fun TileLazyGrid( } @Composable -fun Tile(tile: TileViewModel, iconOnly: Boolean, modifier: Modifier) { +fun Tile( + tile: TileViewModel, + iconOnly: Boolean, + squishiness: () -> Float, + modifier: Modifier = Modifier, +) { val state by tile.state.collectAsStateWithLifecycle(tile.currentState) val resources = resources() val uiState = remember(state, resources) { state.toUiState(resources) } @@ -119,6 +124,7 @@ fun Tile(tile: TileViewModel, iconOnly: Boolean, modifier: Modifier) { onClick = tile::onClick, onLongClick = tile::onLongClick, uiState = uiState, + squishiness = squishiness, modifier = modifier, ) { expandable -> val icon = getTileIcon(icon = uiState.icon) @@ -144,6 +150,7 @@ fun Tile(tile: TileViewModel, iconOnly: Boolean, modifier: Modifier) { }, onLongClick = { tile.onLongClick(expandable) }, accessibilityUiState = uiState.accessibilityUiState, + squishiness = squishiness, ) } } @@ -155,12 +162,17 @@ private fun TileContainer( shape: Shape, iconOnly: Boolean, uiState: TileUiState, + squishiness: () -> Float, modifier: Modifier = Modifier, onClick: (Expandable) -> Unit = {}, onLongClick: (Expandable) -> Unit = {}, content: @Composable BoxScope.(Expandable) -> Unit, ) { - Expandable(color = color, shape = shape, modifier = modifier.clip(shape)) { + Expandable( + color = color, + shape = shape, + modifier = modifier.clip(shape).verticalSquish(squishiness), + ) { val longPressLabel = longPressLabel() Box( modifier = 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 eee905f9f894..88e3019ba163 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 @@ -42,6 +42,7 @@ constructor( tilesInteractor: CurrentTilesInteractor, fixedColumnsSizeViewModel: FixedColumnsSizeViewModel, quickQuickSettingsRowInteractor: QuickQuickSettingsRowInteractor, + val squishinessViewModel: TileSquishinessViewModel, private val iconTilesViewModel: IconTilesViewModel, @Application private val applicationScope: CoroutineScope, ) { @@ -52,7 +53,7 @@ constructor( quickQuickSettingsRowInteractor.rows.stateIn( applicationScope, SharingStarted.WhileSubscribed(), - quickQuickSettingsRowInteractor.defaultRows + quickQuickSettingsRowInteractor.defaultRows, ) val tileViewModels: StateFlow<List<SizedTile<TileViewModel>>> = @@ -60,12 +61,7 @@ constructor( .flatMapLatest { columns -> tilesInteractor.currentTiles.combine(rows, ::Pair).mapLatest { (tiles, rows) -> tiles - .map { - SizedTileImpl( - TileViewModel(it.tile, it.spec), - it.spec.width, - ) - } + .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width) } .let { splitInRowsSequence(it, columns).take(rows).toList().flatten() } } } @@ -73,15 +69,10 @@ constructor( applicationScope, SharingStarted.WhileSubscribed(), tilesInteractor.currentTiles.value - .map { - SizedTileImpl( - TileViewModel(it.tile, it.spec), - it.spec.width, - ) - } + .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width) } .let { splitInRowsSequence(it, columns.value).take(rows.value).toList().flatten() - } + }, ) private val TileSpec.width: Int diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModel.kt new file mode 100644 index 000000000000..0c4d5de0edf9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModel.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.qs.panels.ui.viewmodel + +import com.android.systemui.qs.panels.domain.interactor.TileSquishinessInteractor +import javax.inject.Inject + +/** View model to track the squishiness of tiles. */ +class TileSquishinessViewModel +@Inject +constructor(tileSquishinessInteractor: TileSquishinessInteractor) { + val squishiness = tileSquishinessInteractor.squishiness +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java index 5ea8c2183295..a4f3c7aa2652 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java @@ -14,6 +14,7 @@ package com.android.systemui.qs.tileimpl; +import static com.android.systemui.Flags.qsNewTiles; import static com.android.systemui.Flags.removeUpdateListenerInQsIconViewImpl; import android.animation.Animator; @@ -66,12 +67,22 @@ public class QSIconViewImpl extends QSIconView { private ValueAnimator mColorAnimator = new ValueAnimator(); + private int mColorUnavailable; + private int mColorInactive; + private int mColorActive; + public QSIconViewImpl(Context context) { super(context); final Resources res = context.getResources(); mIconSizePx = res.getDimensionPixelSize(R.dimen.qs_icon_size); + if (qsNewTiles()) { // pre-load icon tint colors + mColorUnavailable = Utils.getColorAttrDefaultColor(context, R.attr.outline); + mColorInactive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactiveVariant); + mColorActive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive); + } + mIcon = createIcon(); addView(mIcon); mColorAnimator.setDuration(QS_ANIM_LENGTH); @@ -195,7 +206,11 @@ public class QSIconViewImpl extends QSIconView { } protected int getColor(QSTile.State state) { - return getIconColorForState(getContext(), state); + if (qsNewTiles()) { + return getCachedIconColorForState(state); + } else { + return getIconColorForState(getContext(), state); + } } private void animateGrayScale(int fromColor, int toColor, ImageView iv, @@ -267,6 +282,19 @@ public class QSIconViewImpl extends QSIconView { } } + private int getCachedIconColorForState(QSTile.State state) { + if (state.disabledByPolicy || state.state == Tile.STATE_UNAVAILABLE) { + return mColorUnavailable; + } else if (state.state == Tile.STATE_INACTIVE) { + return mColorInactive; + } else if (state.state == Tile.STATE_ACTIVE) { + return mColorActive; + } else { + Log.e("QSIconView", "Invalid state " + state); + return 0; + } + } + private static class EndRunnableAnimatorListener extends AnimatorListenerAdapter { private Runnable mRunnable; diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index 4f3ea8331a17..18b1f071f44e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -643,7 +643,6 @@ constructor( } // HANDLE STATE CHANGES RELATED METHODS - protected open fun handleStateChanged(state: QSTile.State) { val allowAnimations = animationsEnabled() isClickable = state.state != Tile.STATE_UNAVAILABLE diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java index 5ea9e6ae0a70..301ab2bcdd65 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java @@ -311,10 +311,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi mConfig = MobileMappings.Config.readConfig(mContext); mTelephonyManager = mTelephonyManager.createForSubscriptionId(mDefaultDataSubId); mSubIdTelephonyManagerMap.put(mDefaultDataSubId, mTelephonyManager); - InternetTelephonyCallback telephonyCallback = - new InternetTelephonyCallback(mDefaultDataSubId); - mSubIdTelephonyCallbackMap.put(mDefaultDataSubId, telephonyCallback); - mTelephonyManager.registerTelephonyCallback(mExecutor, telephonyCallback); + registerInternetTelephonyCallback(mTelephonyManager, mDefaultDataSubId); // Listen the connectivity changes mConnectivityManager.registerDefaultNetworkCallback(mConnectivityManagerNetworkCallback); mCanConfigWifi = canConfigWifi; @@ -346,6 +343,23 @@ public class InternetDialogController implements AccessPointController.AccessPoi mCallback = null; } + /** + * This is to generate and register the new callback to Telephony for uncached subscription id, + * then cache it. Telephony also cached this callback into + * {@link com.android.server.TelephonyRegistry}, so if subscription id and callback were cached + * already, it shall do nothing to avoid registering redundant callback to Telephony. + */ + private void registerInternetTelephonyCallback( + TelephonyManager telephonyManager, int subId) { + if (mSubIdTelephonyCallbackMap.containsKey(subId)) { + // Avoid to generate and register unnecessary callback to Telephony. + return; + } + InternetTelephonyCallback telephonyCallback = new InternetTelephonyCallback(subId); + mSubIdTelephonyCallbackMap.put(subId, telephonyCallback); + telephonyManager.registerTelephonyCallback(mExecutor, telephonyCallback); + } + boolean isAirplaneModeEnabled() { return mGlobalSettings.getInt(Settings.Global.AIRPLANE_MODE_ON, 0) != 0; } @@ -673,9 +687,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi int subId = subInfo.getSubscriptionId(); if (mSubIdTelephonyManagerMap.get(subId) == null) { TelephonyManager secondaryTm = mTelephonyManager.createForSubscriptionId(subId); - InternetTelephonyCallback telephonyCallback = new InternetTelephonyCallback(subId); - secondaryTm.registerTelephonyCallback(mExecutor, telephonyCallback); - mSubIdTelephonyCallbackMap.put(subId, telephonyCallback); + registerInternetTelephonyCallback(secondaryTm, subId); mSubIdTelephonyManagerMap.put(subId, secondaryTm); } return subId; @@ -1351,6 +1363,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi if (DEBUG) { Log.d(TAG, "DDS: defaultDataSubId:" + defaultDataSubId); } + if (SubscriptionManager.isUsableSubscriptionId(defaultDataSubId)) { // clean up old defaultDataSubId TelephonyCallback oldCallback = mSubIdTelephonyCallbackMap.get(mDefaultDataSubId); @@ -1366,9 +1379,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi // create for new defaultDataSubId mTelephonyManager = mTelephonyManager.createForSubscriptionId(defaultDataSubId); mSubIdTelephonyManagerMap.put(defaultDataSubId, mTelephonyManager); - InternetTelephonyCallback newCallback = new InternetTelephonyCallback(defaultDataSubId); - mSubIdTelephonyCallbackMap.put(defaultDataSubId, newCallback); - mTelephonyManager.registerTelephonyCallback(mHandler::post, newCallback); + registerInternetTelephonyCallback(mTelephonyManager, defaultDataSubId); mCallback.onSubscriptionsChanged(defaultDataSubId); } mDefaultDataSubId = defaultDataSubId; diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt index 8965ef2bc493..bb0b9b7084fa 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt @@ -18,7 +18,9 @@ package com.android.systemui.qs.tiles.impl.internet.domain import android.content.Context import android.content.res.Resources +import android.os.Handler import android.widget.Switch +import com.android.settingslib.graph.SignalDrawable import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text.Companion.loadText @@ -28,6 +30,7 @@ import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileMode import com.android.systemui.qs.tiles.viewmodel.QSTileConfig import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.res.R +import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel import javax.inject.Inject /** Maps [InternetTileModel] to [QSTileState]. */ @@ -37,6 +40,7 @@ constructor( @Main private val resources: Resources, private val theme: Resources.Theme, private val context: Context, + @Main private val handler: Handler, ) : QSTileDataToStateMapper<InternetTileModel> { override fun map(config: QSTileConfig, data: InternetTileModel): QSTileState = @@ -44,25 +48,42 @@ constructor( label = resources.getString(R.string.quick_settings_internet_label) expandedAccessibilityClass = Switch::class - if (data.secondaryLabel != null) { - secondaryLabel = data.secondaryLabel.loadText(context) - } else { - secondaryLabel = data.secondaryTitle - } + secondaryLabel = + if (data.secondaryLabel != null) { + data.secondaryLabel.loadText(context) + } else { + data.secondaryTitle + } stateDescription = data.stateDescription.loadContentDescription(context) contentDescription = data.contentDescription.loadContentDescription(context) - iconRes = data.iconId - if (data.icon != null) { - this.icon = { data.icon } - } else if (data.iconId != null) { - val loadedIcon = - Icon.Loaded( - resources.getDrawable(data.iconId!!, theme), - contentDescription = null - ) - this.icon = { loadedIcon } + when (val dataIcon = data.icon) { + is InternetTileIconModel.ResourceId -> { + iconRes = dataIcon.resId + icon = { + Icon.Loaded( + resources.getDrawable(dataIcon.resId, theme), + contentDescription = null, + ) + } + } + + is InternetTileIconModel.Cellular -> { + val signalDrawable = SignalDrawable(context, handler) + signalDrawable.setLevel(dataIcon.level) + icon = { Icon.Loaded(signalDrawable, contentDescription = null) } + } + + is InternetTileIconModel.Satellite -> { + iconRes = dataIcon.resourceIcon.res // level is inferred from res + icon = { + Icon.Loaded( + resources.getDrawable(dataIcon.resourceIcon.res, theme), + contentDescription = null, + ) + } + } } sideViewIcon = QSTileState.SideViewIcon.Chevron @@ -75,7 +96,7 @@ constructor( setOf( QSTileState.UserAction.CLICK, QSTileState.UserAction.TOGGLE_CLICK, - QSTileState.UserAction.LONG_CLICK + QSTileState.UserAction.LONG_CLICK, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt index 204ead3fe29c..6fe3979fa446 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt @@ -20,13 +20,10 @@ import android.annotation.StringRes import android.content.Context import android.os.UserHandle import android.text.Html -import com.android.settingslib.graph.SignalDrawable import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription -import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel @@ -36,12 +33,12 @@ import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteracto import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository +import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon import com.android.systemui.utils.coroutines.flow.mapLatestConflated import javax.inject.Inject -import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -51,7 +48,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.withContext @OptIn(ExperimentalCoroutinesApi::class) /** Observes internet state changes providing the [InternetTileModel]. */ @@ -59,7 +55,6 @@ class InternetTileDataInteractor @Inject constructor( private val context: Context, - @Main private val mainCoroutineContext: CoroutineContext, @Application private val scope: CoroutineScope, airplaneModeRepository: AirplaneModeRepository, private val connectivityRepository: ConnectivityRepository, @@ -79,8 +74,7 @@ constructor( flowOf( InternetTileModel.Active( secondaryTitle = secondary, - iconId = wifiIcon.icon.res, - icon = Icon.Loaded(context.getDrawable(wifiIcon.icon.res)!!, null), + icon = InternetTileIconModel.ResourceId(wifiIcon.icon.res), stateDescription = wifiIcon.contentDescription, contentDescription = ContentDescription.Loaded("$internetLabel,$secondary"), ) @@ -116,11 +110,10 @@ constructor( if (it == null) { notConnectedFlow } else { - combine( - it.networkName, - it.signalLevelIcon, - mobileDataContentName, - ) { networkNameModel, signalIcon, dataContentDescription -> + combine(it.networkName, it.signalLevelIcon, mobileDataContentName) { + networkNameModel, + signalIcon, + dataContentDescription -> Triple(networkNameModel, signalIcon, dataContentDescription) } .mapLatestConflated { (networkNameModel, signalIcon, dataContentDescription) -> @@ -129,17 +122,12 @@ constructor( val secondary = mobileDataContentConcat( networkNameModel.name, - dataContentDescription + dataContentDescription, ) - val drawable = - withContext(mainCoroutineContext) { SignalDrawable(context) } - drawable.setLevel(signalIcon.level) - val loadedIcon = Icon.Loaded(drawable, null) - InternetTileModel.Active( secondaryTitle = secondary, - icon = loadedIcon, + icon = InternetTileIconModel.Cellular(signalIcon.level), stateDescription = ContentDescription.Loaded(secondary.toString()), contentDescription = ContentDescription.Loaded(internetLabel), @@ -150,9 +138,10 @@ constructor( signalIcon.icon.contentDescription.loadContentDescription( context ) + InternetTileModel.Active( secondaryTitle = secondary, - iconId = signalIcon.icon.res, + icon = InternetTileIconModel.Satellite(signalIcon.icon), stateDescription = ContentDescription.Loaded(secondary), contentDescription = ContentDescription.Loaded(internetLabel), ) @@ -164,7 +153,7 @@ constructor( private fun mobileDataContentConcat( networkName: String?, - dataContentDescription: CharSequence? + dataContentDescription: CharSequence?, ): CharSequence { if (dataContentDescription == null) { return networkName ?: "" @@ -177,9 +166,9 @@ constructor( context.getString( R.string.mobile_carrier_text_format, networkName, - dataContentDescription + dataContentDescription, ), - 0 + 0, ) } @@ -199,7 +188,7 @@ constructor( flowOf( InternetTileModel.Active( secondaryLabel = secondary?.toText(), - iconId = it.res, + icon = InternetTileIconModel.ResourceId(it.res), stateDescription = null, contentDescription = secondary, ) @@ -208,16 +197,18 @@ constructor( } private val notConnectedFlow: StateFlow<InternetTileModel> = - combine( - wifiInteractor.areNetworksAvailable, - airplaneModeRepository.isAirplaneMode, - ) { networksAvailable, isAirplaneMode -> + combine(wifiInteractor.areNetworksAvailable, airplaneModeRepository.isAirplaneMode) { + networksAvailable, + isAirplaneMode -> when { isAirplaneMode -> { val secondary = context.getString(R.string.status_bar_airplane) InternetTileModel.Inactive( secondaryTitle = secondary, - iconId = R.drawable.ic_qs_no_internet_unavailable, + icon = + InternetTileIconModel.ResourceId( + R.drawable.ic_qs_no_internet_unavailable + ), stateDescription = null, contentDescription = ContentDescription.Loaded(secondary), ) @@ -227,10 +218,13 @@ constructor( context.getString(R.string.quick_settings_networks_available) InternetTileModel.Inactive( secondaryTitle = secondary, - iconId = R.drawable.ic_qs_no_internet_available, + icon = + InternetTileIconModel.ResourceId( + R.drawable.ic_qs_no_internet_available + ), stateDescription = null, contentDescription = - ContentDescription.Loaded("$internetLabel,$secondary") + ContentDescription.Loaded("$internetLabel,$secondary"), ) } else -> { @@ -248,7 +242,7 @@ constructor( */ override fun tileData( user: UserHandle, - triggers: Flow<DataUpdateTrigger> + triggers: Flow<DataUpdateTrigger>, ): Flow<InternetTileModel> = connectivityRepository.defaultConnections.flatMapLatest { when { @@ -265,7 +259,7 @@ constructor( val NOT_CONNECTED_NETWORKS_UNAVAILABLE = InternetTileModel.Inactive( secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable), - iconId = R.drawable.ic_qs_no_internet_unavailable, + icon = InternetTileIconModel.ResourceId(R.drawable.ic_qs_no_internet_unavailable), stateDescription = null, contentDescription = ContentDescription.Resource(R.string.quick_settings_networks_unavailable), diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt index ece904611782..15b4e472eec7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt @@ -17,23 +17,21 @@ package com.android.systemui.qs.tiles.impl.internet.domain.model import com.android.systemui.common.shared.model.ContentDescription -import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text +import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel /** Model describing the state that the QS Internet tile should be in. */ sealed interface InternetTileModel { val secondaryTitle: CharSequence? val secondaryLabel: Text? - val iconId: Int? - val icon: Icon? + val icon: InternetTileIconModel val stateDescription: ContentDescription? val contentDescription: ContentDescription? data class Active( override val secondaryTitle: CharSequence? = null, override val secondaryLabel: Text? = null, - override val iconId: Int? = null, - override val icon: Icon? = null, + override val icon: InternetTileIconModel = InternetTileIconModel.Cellular(1), override val stateDescription: ContentDescription? = null, override val contentDescription: ContentDescription? = null, ) : InternetTileModel @@ -41,8 +39,7 @@ sealed interface InternetTileModel { data class Inactive( override val secondaryTitle: CharSequence? = null, override val secondaryLabel: Text? = null, - override val iconId: Int? = null, - override val icon: Icon? = null, + override val icon: InternetTileIconModel = InternetTileIconModel.Cellular(1), override val stateDescription: ContentDescription? = null, override val contentDescription: ContentDescription? = null, ) : InternetTileModel diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt index 3b97d820e6a8..afb9a788ec24 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt @@ -16,11 +16,18 @@ package com.android.systemui.qs.ui.viewmodel +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.scene.domain.interactor.SceneInteractor -import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch /** * Models UI state used to render the content of the quick settings shade overlay. @@ -31,15 +38,43 @@ import dagger.assisted.AssistedInject class QuickSettingsShadeOverlayContentViewModel @AssistedInject constructor( + val shadeInteractor: ShadeInteractor, val sceneInteractor: SceneInteractor, val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, val quickSettingsContainerViewModel: QuickSettingsContainerViewModel, -) { +) : ExclusiveActivatable() { + + override suspend fun onActivated(): Nothing { + coroutineScope { + launch { + sceneInteractor.currentScene.collect { currentScene -> + when (currentScene) { + // TODO(b/369513770): The ShadeSession should be preserved in this scenario. + Scenes.Bouncer -> + shadeInteractor.collapseQuickSettingsShade( + loggingReason = "bouncer shown while shade is open" + ) + } + } + } + + launch { + shadeInteractor.isShadeTouchable + .distinctUntilChanged() + .filter { !it } + .collect { + shadeInteractor.collapseQuickSettingsShade( + loggingReason = "device became non-interactive" + ) + } + } + } + + awaitCancellation() + } + fun onScrimClicked() { - sceneInteractor.hideOverlay( - overlay = Overlays.QuickSettingsShade, - loggingReason = "Shade scrim clicked", - ) + shadeInteractor.collapseQuickSettingsShade(loggingReason = "shade scrim clicked") } @AssistedFactory diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index ac49e91c777c..559c2637ed4f 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -248,7 +248,8 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } else if (action == ACTION_UP) { // Gesture was too short to be picked up by scene container touch // handling; programmatically start the transition to the shade. - mShadeInteractor.get().expandNotificationShade("short launcher swipe"); + mShadeInteractor.get() + .expandNotificationsShade("short launcher swipe", null); } } event.recycle(); @@ -265,7 +266,8 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis mSceneInteractor.get().onRemoteUserInputStarted( "trackpad swipe"); } else if (action == ACTION_UP) { - mShadeInteractor.get().expandNotificationShade("short trackpad swipe"); + mShadeInteractor.get() + .expandNotificationsShade("short trackpad swipe", null); } mStatusBarWinController.getWindowRootView().dispatchTouchEvent(event); } else { diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt index a5f4a8959569..4d2bc91aa52a 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt @@ -61,11 +61,11 @@ constructor( uiEventLogger, notificationManager, userContextProvider, - keyguardDismissUtil + keyguardDismissUtil, ) { - private val commandHandler = - IssueRecordingServiceCommandHandler( + private val session = + IssueRecordingServiceSession( bgExecutor, dialogTransitionAnimator, panelInteractor, @@ -86,7 +86,7 @@ constructor( Log.d(getTag(), "handling action: ${intent?.action}") when (intent?.action) { ACTION_START -> { - commandHandler.handleStartCommand() + session.start() if (!issueRecordingState.recordScreen) { // If we don't want to record the screen, the ACTION_SHOW_START_NOTIF action // will circumvent the RecordingService's screen recording start code. @@ -94,12 +94,12 @@ constructor( } } ACTION_STOP, - ACTION_STOP_NOTIF -> commandHandler.handleStopCommand(contentResolver) + ACTION_STOP_NOTIF -> session.stop(contentResolver) ACTION_SHARE -> { - commandHandler.handleShareCommand( + session.share( intent.getIntExtra(EXTRA_NOTIFICATION_ID, mNotificationId), intent.getParcelableExtra(EXTRA_PATH, Uri::class.java), - this + this, ) // Unlike all other actions, action_share has different behavior for the screen // recording qs tile than it does for the record issue qs tile. Return sticky to diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandler.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceSession.kt index 32de0f353502..e4d3e6cae502 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceSession.kt @@ -34,9 +34,11 @@ private const val DISABLED = 0 /** * This class exists to unit test the business logic encapsulated in IssueRecordingService. Android * specifically calls out that there is no supported way to test IntentServices here: - * https://developer.android.com/training/testing/other-components/services + * https://developer.android.com/training/testing/other-components/services, and mentions that the + * best way to add unit tests, is to introduce a separate class containing the business logic of + * that service, and test the functionality via that class. */ -class IssueRecordingServiceCommandHandler( +class IssueRecordingServiceSession( private val bgExecutor: Executor, private val dialogTransitionAnimator: DialogTransitionAnimator, private val panelInteractor: PanelInteractor, @@ -47,12 +49,12 @@ class IssueRecordingServiceCommandHandler( private val userContextProvider: UserContextProvider, ) { - fun handleStartCommand() { + fun start() { bgExecutor.execute { traceurMessageSender.startTracing(issueRecordingState.traceConfig) } issueRecordingState.isRecording = true } - fun handleStopCommand(contentResolver: ContentResolver) { + fun stop(contentResolver: ContentResolver) { bgExecutor.execute { if (issueRecordingState.traceConfig.longTrace) { Settings.Global.putInt(contentResolver, NOTIFY_SESSION_ENDED_SETTING, DISABLED) @@ -62,12 +64,12 @@ class IssueRecordingServiceCommandHandler( issueRecordingState.isRecording = false } - fun handleShareCommand(notificationId: Int, screenRecording: Uri?, context: Context) { + fun share(notificationId: Int, screenRecording: Uri?, context: Context) { bgExecutor.execute { notificationManager.cancelAsUser( null, notificationId, - UserHandle(userContextProvider.userContext.userId) + UserHandle(userContextProvider.userContext.userId), ) if (issueRecordingState.takeBugreport) { diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt index b9f57f2f31d5..3c6d858092af 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt @@ -32,4 +32,7 @@ object TransitionKeys { * normal collapse would. */ val SlightlyFasterShadeCollapse = TransitionKey("SlightlyFasterShadeCollapse") + + /** Reference to a content transition that should happen instantly, i.e. without animation. */ + val Instant = TransitionKey("Instant") } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index 361226a4df18..6c99282bdcdd 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -22,8 +22,8 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.scene.domain.interactor.SceneInteractor -import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.shared.model.TransitionKeys.Instant import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse import com.android.systemui.shade.ShadeController.ShadeVisibilityListener import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -80,18 +80,25 @@ constructor( } } + @Deprecated("Deprecated in Java") override fun isShadeEnabled() = shadeInteractor.isShadeEnabled.value + @Deprecated("Deprecated in Java") override fun isShadeFullyOpen(): Boolean = shadeInteractor.isAnyFullyExpanded.value + @Deprecated("Deprecated in Java") override fun isExpandingOrCollapsing(): Boolean = shadeInteractor.isUserInteracting.value + @Deprecated("Deprecated in Java") override fun instantExpandShade() { // Do nothing } override fun instantCollapseShade() { - sceneInteractor.snapToScene(SceneFamilies.Home, "hide shade") + shadeInteractor.collapseNotificationsShade( + loggingReason = "ShadeControllerSceneImpl.instantCollapseShade", + transitionKey = Instant, + ) } override fun animateCollapseShade( @@ -122,16 +129,17 @@ constructor( } } + @Deprecated("Deprecated in Java") override fun collapseWithDuration(animationDuration: Int) { // TODO(b/300258424) inline this. The only caller uses the default duration. animateCollapseShade() } private fun animateCollapseShadeInternal() { - sceneInteractor.changeScene( - SceneFamilies.Home, // TODO(b/336581871): add sceneState? - "ShadeController.animateCollapseShade", - SlightlyFasterShadeCollapse, + // TODO(b/336581871): add sceneState? + shadeInteractor.collapseEitherShade( + loggingReason = "ShadeController.animateCollapseShade", + transitionKey = SlightlyFasterShadeCollapse, ) } @@ -140,6 +148,7 @@ constructor( animateCollapseShade() } + @Deprecated("Deprecated in Java") override fun closeShadeIfOpen(): Boolean { if (shadeInteractor.isAnyExpanded.value) { commandQueue.animateCollapsePanels( @@ -155,6 +164,7 @@ constructor( animateCollapseShadeForcedDelayed() } + @Deprecated("Deprecated in Java") override fun collapseShade(animate: Boolean) { if (animate) { animateCollapseShade() @@ -163,13 +173,14 @@ constructor( } } + @Deprecated("Deprecated in Java") override fun collapseOnMainThread() { // TODO if this works with delegation alone, we can deprecate and delete collapseShade() } override fun expandToNotifications() { - shadeInteractor.expandNotificationShade("ShadeController.animateExpandShade") + shadeInteractor.expandNotificationsShade("ShadeController.animateExpandShade") } override fun expandToQs() { @@ -193,14 +204,17 @@ constructor( } } + @Deprecated("Deprecated in Java") override fun postAnimateCollapseShade() { animateCollapseShade() } + @Deprecated("Deprecated in Java") override fun postAnimateForceCollapseShade() { animateCollapseShadeForced() } + @Deprecated("Deprecated in Java") override fun postAnimateExpandQs() { expandToQs() } @@ -214,18 +228,23 @@ constructor( } } + @Deprecated("Deprecated in Java") override fun makeExpandedInvisible() { // Do nothing } + @Deprecated("Deprecated in Java") override fun makeExpandedVisible(force: Boolean) { // Do nothing } + @Deprecated("Deprecated in Java") override fun isExpandedVisible(): Boolean { - return sceneInteractor.currentScene.value != Scenes.Gone + return sceneInteractor.currentScene.value != Scenes.Gone || + sceneInteractor.currentOverlays.value.isNotEmpty() } + @Deprecated("Deprecated in Java") override fun onStatusBarTouch(event: MotionEvent) { // The only call to this doesn't happen with MigrateClocksToBlueprint.isEnabled enabled throw UnsupportedOperationException() diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index b046c50b05d3..a3f2c64f6909 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -17,6 +17,7 @@ package com.android.systemui.shade.domain.interactor import androidx.annotation.FloatRange +import com.android.compose.animation.scene.TransitionKey import com.android.systemui.shade.shared.model.ShadeMode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -27,19 +28,22 @@ import kotlinx.coroutines.flow.stateIn /** Business logic for shade interactions. */ interface ShadeInteractor : BaseShadeInteractor { - /** Emits true if the shade is currently allowed and false otherwise. */ + /** Emits true if the Notifications shade is currently allowed and false otherwise. */ val isShadeEnabled: StateFlow<Boolean> - /** Emits true if QS is currently allowed and false otherwise. */ + /** Emits true if QS shade is currently allowed and false otherwise. */ val isQsEnabled: StateFlow<Boolean> - /** Whether either the shade or QS is fully expanded. */ + /** Whether either the Notifications shade or QS shade is fully expanded. */ val isAnyFullyExpanded: StateFlow<Boolean> - /** Whether the Shade is fully expanded. */ + /** Whether the Notifications Shade is fully expanded. */ val isShadeFullyExpanded: Flow<Boolean> - /** Whether the Shade is fully collapsed. */ + /** Whether Notifications Shade is expanded a non-zero amount. */ + val isShadeAnyExpanded: StateFlow<Boolean> + + /** Whether the Notifications Shade is fully collapsed. */ val isShadeFullyCollapsed: Flow<Boolean> /** @@ -102,7 +106,7 @@ interface BaseShadeInteractor { */ val isAnyExpanded: StateFlow<Boolean> - /** The amount [0-1] that the shade has been opened. */ + /** The amount [0-1] that the Notifications Shade has been opened. */ val shadeExpansion: StateFlow<Float> /** @@ -111,7 +115,7 @@ interface BaseShadeInteractor { */ val qsExpansion: StateFlow<Float> - /** Whether Quick Settings is expanded a non-zero amount. */ + /** Whether Quick Settings Shade is expanded a non-zero amount. */ val isQsExpanded: StateFlow<Boolean> /** @@ -142,16 +146,38 @@ interface BaseShadeInteractor { val isUserInteractingWithQs: Flow<Boolean> /** - * Triggers the expansion (opening) of the notification shade. If the notification shade is - * already open, this has no effect. + * Triggers the expansion (opening) of the notifications shade. If it is already expanded, this + * has no effect. + */ + fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey? = null) + + /** + * Triggers the expansion (opening) of the quick settings shade. If it is already expanded, this + * has no effect. + */ + fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey? = null) + + /** + * Triggers the collapse (closing) of the notifications shade. If it is already collapsed, this + * has no effect. + */ + fun collapseNotificationsShade(loggingReason: String, transitionKey: TransitionKey? = null) + + /** + * Triggers the collapse (closing) of the quick settings shade. If it is already collapsed, this + * has no effect. */ - fun expandNotificationShade(loggingReason: String) + fun collapseQuickSettingsShade( + loggingReason: String, + transitionKey: TransitionKey? = null, + bypassNotificationsShade: Boolean = false, + ) /** - * Triggers the expansion (opening) of the quick settings shade. If the quick settings shade is - * already open, this has no effect. + * Triggers the collapse (closing) of the notifications shade or quick settings shade, whichever + * is open. If both are already collapsed, this has no effect. */ - fun expandQuickSettingsShade(loggingReason: String) + fun collapseEitherShade(loggingReason: String, transitionKey: TransitionKey? = null) } fun createAnyExpansionFlow( diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt index fb1482890b87..322fca39a1df 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt @@ -16,6 +16,7 @@ package com.android.systemui.shade.domain.interactor +import com.android.compose.animation.scene.TransitionKey import com.android.systemui.dagger.SysUISingleton import com.android.systemui.shade.shared.model.ShadeMode import javax.inject.Inject @@ -31,6 +32,7 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor { override val isShadeEnabled: StateFlow<Boolean> = inactiveFlowBoolean override val isQsEnabled: StateFlow<Boolean> = inactiveFlowBoolean override val shadeExpansion: StateFlow<Float> = inactiveFlowFloat + override val isShadeAnyExpanded: StateFlow<Boolean> = inactiveFlowBoolean override val qsExpansion: StateFlow<Float> = inactiveFlowFloat override val isQsExpanded: StateFlow<Boolean> = inactiveFlowBoolean override val isQsBypassingShade: Flow<Boolean> = inactiveFlowBoolean @@ -50,7 +52,17 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor { override fun getTopEdgeSplitFraction(): Float = 0.5f - override fun expandNotificationShade(loggingReason: String) {} + override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {} - override fun expandQuickSettingsShade(loggingReason: String) {} + override fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey?) {} + + override fun collapseNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {} + + override fun collapseQuickSettingsShade( + loggingReason: String, + transitionKey: TransitionKey?, + bypassNotificationsShade: Boolean, + ) {} + + override fun collapseEitherShade(loggingReason: String, transitionKey: TransitionKey?) {} } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt index 3eab02ad30d5..949d2aa36bf3 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt @@ -78,12 +78,16 @@ constructor( override val isShadeFullyExpanded: Flow<Boolean> = baseShadeInteractor.shadeExpansion.map { it >= 1f }.distinctUntilChanged() + override val isShadeAnyExpanded: StateFlow<Boolean> = + baseShadeInteractor.shadeExpansion + .map { it > 0 } + .stateIn(scope, SharingStarted.Eagerly, false) + override val isShadeFullyCollapsed: Flow<Boolean> = baseShadeInteractor.shadeExpansion.map { it <= 0f }.distinctUntilChanged() override val isUserInteracting: StateFlow<Boolean> = combine(isUserInteractingWithShade, isUserInteractingWithQs) { shade, qs -> shade || qs } - .distinctUntilChanged() .stateIn(scope, SharingStarted.Eagerly, false) override val isShadeTouchable: Flow<Boolean> = diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt index df094864a71b..0902c3936661 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt @@ -17,6 +17,7 @@ package com.android.systemui.shade.domain.interactor import com.android.app.tracing.FlowTracing.traceAsCounter +import com.android.compose.animation.scene.TransitionKey import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardRepository @@ -111,18 +112,40 @@ constructor( override val isUserInteractingWithQs: Flow<Boolean> = userInteractingFlow(repository.legacyQsTracking, repository.qsExpansion) - override fun expandNotificationShade(loggingReason: String) { + override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) { throw UnsupportedOperationException( "expandNotificationShade() is not supported in legacy shade" ) } - override fun expandQuickSettingsShade(loggingReason: String) { + override fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey?) { throw UnsupportedOperationException( "expandQuickSettingsShade() is not supported in legacy shade" ) } + override fun collapseNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) { + throw UnsupportedOperationException( + "collapseNotificationShade() is not supported in legacy shade" + ) + } + + override fun collapseQuickSettingsShade( + loggingReason: String, + transitionKey: TransitionKey?, + bypassNotificationsShade: Boolean, + ) { + throw UnsupportedOperationException( + "collapseQuickSettingsShade() is not supported in legacy shade" + ) + } + + override fun collapseEitherShade(loggingReason: String, transitionKey: TransitionKey?) { + throw UnsupportedOperationException( + "collapseEitherShade() is not supported in legacy shade" + ) + } + /** * Return a flow for whether a user is interacting with an expandable shade component using * tracking and expansion flows. NOTE: expansion must be a `StateFlow` to guarantee that diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt index 81bf712f21e5..765810810bb8 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt @@ -21,12 +21,16 @@ import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.OverlayKey import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.TransitionKey import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.shared.model.TransitionKeys.Instant +import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import javax.inject.Inject @@ -133,43 +137,120 @@ constructor( } } - override fun expandNotificationShade(loggingReason: String) { + override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) { if (shadeModeInteractor.isDualShade) { if (Overlays.QuickSettingsShade in sceneInteractor.currentOverlays.value) { sceneInteractor.replaceOverlay( from = Overlays.QuickSettingsShade, to = Overlays.NotificationsShade, loggingReason = loggingReason, + transitionKey = transitionKey, ) } else { sceneInteractor.showOverlay( overlay = Overlays.NotificationsShade, loggingReason = loggingReason, + transitionKey = transitionKey, ) } } else { - sceneInteractor.changeScene(toScene = Scenes.Shade, loggingReason = loggingReason) + sceneInteractor.changeScene( + toScene = Scenes.Shade, + loggingReason = loggingReason, + transitionKey = + transitionKey ?: ToSplitShade.takeIf { shadeModeInteractor.isSplitShade }, + ) } } - override fun expandQuickSettingsShade(loggingReason: String) { + override fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey?) { if (shadeModeInteractor.isDualShade) { if (Overlays.NotificationsShade in sceneInteractor.currentOverlays.value) { sceneInteractor.replaceOverlay( from = Overlays.NotificationsShade, to = Overlays.QuickSettingsShade, loggingReason = loggingReason, + transitionKey = transitionKey, ) } else { sceneInteractor.showOverlay( overlay = Overlays.QuickSettingsShade, loggingReason = loggingReason, + transitionKey = transitionKey, ) } } else { + val isSplitShade = shadeModeInteractor.isSplitShade + sceneInteractor.changeScene( + toScene = if (isSplitShade) Scenes.Shade else Scenes.QuickSettings, + loggingReason = loggingReason, + transitionKey = transitionKey ?: ToSplitShade.takeIf { isSplitShade }, + ) + } + } + + override fun collapseNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) { + if (shadeModeInteractor.isDualShade) { + // TODO(b/356596436): Hide without animation if transitionKey is Instant. + sceneInteractor.hideOverlay( + overlay = Overlays.NotificationsShade, + loggingReason = loggingReason, + transitionKey = transitionKey, + ) + } else if (transitionKey == Instant) { + // TODO(b/356596436): Define instant transition instead of snapToScene(). + sceneInteractor.snapToScene(toScene = SceneFamilies.Home, loggingReason = loggingReason) + } else { sceneInteractor.changeScene( - toScene = Scenes.QuickSettings, + toScene = SceneFamilies.Home, + loggingReason = loggingReason, + transitionKey = + transitionKey ?: ToSplitShade.takeIf { shadeModeInteractor.isSplitShade }, + ) + } + } + + override fun collapseQuickSettingsShade( + loggingReason: String, + transitionKey: TransitionKey?, + bypassNotificationsShade: Boolean, + ) { + if (shadeModeInteractor.isDualShade) { + // TODO(b/356596436): Hide without animation if transitionKey is Instant. + sceneInteractor.hideOverlay( + overlay = Overlays.QuickSettingsShade, + loggingReason = loggingReason, + transitionKey = transitionKey, + ) + return + } + + val isSplitShade = shadeModeInteractor.isSplitShade + val targetScene = + if (bypassNotificationsShade || isSplitShade) SceneFamilies.Home else Scenes.Shade + if (transitionKey == Instant) { + // TODO(b/356596436): Define instant transition instead of snapToScene(). + sceneInteractor.snapToScene(toScene = targetScene, loggingReason = loggingReason) + } else { + sceneInteractor.changeScene( + toScene = targetScene, + loggingReason = loggingReason, + transitionKey = transitionKey ?: ToSplitShade.takeIf { isSplitShade }, + ) + } + } + + override fun collapseEitherShade(loggingReason: String, transitionKey: TransitionKey?) { + // Note: The notifications shade and QS shade may be both partially expanded simultaneously, + // so we don't use an 'else' clause here. + if (shadeExpansion.value > 0) { + collapseNotificationsShade(loggingReason = loggingReason, transitionKey = transitionKey) + } + if (isQsExpanded.value) { + collapseQuickSettingsShade( loggingReason = loggingReason, + transitionKey = transitionKey, + bypassNotificationsShade = true, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt index 0fb379017be9..ea76ac4b0f83 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt @@ -21,10 +21,9 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.scene.domain.interactor.SceneInteractor -import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.shared.model.TransitionKeys.Instant import com.android.systemui.shade.data.repository.ShadeRepository -import com.android.systemui.shade.shared.model.ShadeMode import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -48,7 +47,7 @@ constructor( @Deprecated("Use ShadeInteractor instead") override fun expandToNotifications() { - shadeInteractor.expandNotificationShade( + shadeInteractor.expandNotificationsShade( loggingReason = "ShadeLockscreenInteractorImpl.expandToNotifications" ) } @@ -71,17 +70,11 @@ constructor( } override fun resetViews(animate: Boolean) { - val loggingReason = "ShadeLockscreenInteractorImpl.resetViews" // The existing comment to the only call to this claims it only calls it to collapse QS - if (shadeInteractor.shadeMode.value == ShadeMode.Dual) { - // TODO(b/356596436): Hide without animation if !animate. - sceneInteractor.hideOverlay( - overlay = Overlays.QuickSettingsShade, - loggingReason = loggingReason, - ) - } else { - shadeInteractor.expandNotificationShade(loggingReason) - } + shadeInteractor.collapseQuickSettingsShade( + loggingReason = "ShadeLockscreenInteractorImpl.resetViews", + transitionKey = Instant.takeIf { !animate }, + ) } @Deprecated("Not supported by scenes") @@ -93,7 +86,7 @@ constructor( backgroundScope.launch { delay(delay) withContext(mainDispatcher) { - shadeInteractor.expandNotificationShade( + shadeInteractor.expandNotificationsShade( "ShadeLockscreenInteractorImpl.transitionToExpandedShade" ) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt index caa45137ed98..c838c378965f 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt @@ -55,6 +55,10 @@ interface ShadeModeInteractor { val isDualShade: Boolean get() = shadeMode.value is ShadeMode.Dual + /** Convenience shortcut for querying whether the current [shadeMode] is [ShadeMode.Split]. */ + val isSplitShade: Boolean + get() = shadeMode.value is ShadeMode.Split + /** * The fraction between [0..1] (i.e., percentage) of screen width to consider the threshold * between "top-left" and "top-right" for the purposes of dual-shade invocation. diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt index a154e91feca1..bd4ed5b45dc7 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt @@ -29,9 +29,7 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.privacy.PrivacyItem import com.android.systemui.res.R -import com.android.systemui.scene.domain.interactor.SceneInteractor -import com.android.systemui.scene.shared.model.SceneFamilies -import com.android.systemui.scene.shared.model.TransitionKeys +import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse import com.android.systemui.shade.domain.interactor.PrivacyChipInteractor import com.android.systemui.shade.domain.interactor.ShadeHeaderClockInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -55,9 +53,8 @@ import kotlinx.coroutines.launch class ShadeHeaderViewModel @AssistedInject constructor( - private val context: Context, + context: Context, private val activityStarter: ActivityStarter, - private val sceneInteractor: SceneInteractor, private val shadeInteractor: ShadeInteractor, private val mobileIconsInteractor: MobileIconsInteractor, val mobileIconsViewModel: MobileIconsViewModel, @@ -120,7 +117,7 @@ constructor( map = { intent, _ -> intent.action == Intent.ACTION_TIMEZONE_CHANGED || intent.action == Intent.ACTION_LOCALE_CHANGED - } + }, ) .onEach { invalidateFormats -> updateDateTexts(invalidateFormats) } .launchIn(this) @@ -152,10 +149,9 @@ constructor( /** Notifies that the system icons container was clicked. */ fun onSystemIconContainerClicked() { - sceneInteractor.changeScene( - SceneFamilies.Home, - "ShadeHeaderViewModel.onSystemIconContainerClicked", - TransitionKeys.SlightlyFasterShadeCollapse, + shadeInteractor.collapseEitherShade( + loggingReason = "ShadeHeaderViewModel.onSystemIconContainerClicked", + transitionKey = SlightlyFasterShadeCollapse, ) } @@ -163,7 +159,7 @@ constructor( fun onShadeCarrierGroupClicked() { activityStarter.postStartActivityDismissingKeyguard( Intent(Settings.ACTION_WIRELESS_SETTINGS), - 0 + 0, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConntectedDisplays.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConntectedDisplays.kt new file mode 100644 index 000000000000..54a18f764406 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConntectedDisplays.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.core + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the status bar connected displays flag state. */ +@Suppress("NOTHING_TO_INLINE") +object StatusBarConnectedDisplays { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_STATUS_BAR_CONNECTED_DISPLAYS + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.statusBarConnectedDisplays() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is enabled. This will throw an exception if + * the flag is not enabled to ensure that the refactor author catches issues in testing. + * Caution!! Using this check incorrectly will cause crashes in nextfood builds! + */ + @JvmStatic + inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java deleted file mode 100644 index ec3c7d0d6de4..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.notification; - -import android.content.Intent; -import android.view.View; - -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; - -/** - * Component responsible for handling actions on a notification which cause activites to start. - * (e.g. clicking on a notification, tapping on the settings icon in the notification guts) - */ -public interface NotificationActivityStarter { - /** Called when the user clicks on the notification bubble icon. */ - void onNotificationBubbleIconClicked(NotificationEntry entry); - - /** Called when the user clicks on the surface of a notification. */ - void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row); - - /** Called when the user clicks on a button in the notification guts which fires an intent. */ - void startNotificationGutsIntent(Intent intent, int appUid, - ExpandableNotificationRow row); - - /** Called when the user clicks "Manage" or "History" in the Shade. */ - void startHistoryIntent(View view, boolean showHistory); - - /** Called when the user succeed to drop notification to proper target view. */ - void onDragSuccess(NotificationEntry entry); - - default boolean isCollapsingToShowActivityOverLockscreen() { - return false; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt new file mode 100644 index 000000000000..231a0b0b21cb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt @@ -0,0 +1,103 @@ +/* + * 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 + +import android.content.Intent +import android.provider.Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS +import android.provider.Settings.ACTION_NOTIFICATION_HISTORY +import android.provider.Settings.ACTION_NOTIFICATION_SETTINGS +import android.provider.Settings.ACTION_ZEN_MODE_SETTINGS +import android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID +import android.view.View +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow + +/** + * Component responsible for handling actions on a notification which cause activites to start. + * (e.g. clicking on a notification, tapping on the settings icon in the notification guts) + */ +interface NotificationActivityStarter { + + /** Called when the user clicks on the notification bubble icon. */ + fun onNotificationBubbleIconClicked(entry: NotificationEntry?) + + /** Called when the user clicks on the surface of a notification. */ + fun onNotificationClicked(entry: NotificationEntry?, row: ExpandableNotificationRow?) + + /** Called when the user clicks on a button in the notification guts which fires an intent. */ + fun startNotificationGutsIntent(intent: Intent?, appUid: Int, row: ExpandableNotificationRow?) + + /** + * Called when the user clicks "Manage" or "History" in the Shade. Prefer using + * [startSettingsIntent] instead. + */ + fun startHistoryIntent(view: View?, showHistory: Boolean) + + /** + * Called to open a settings intent from a launchable view (such as the "Manage" or "History" + * button in the shade, or the "No notifications" text). + * + * @param view the view to perform the launch animation from (must extend [LaunchableView]) + * @param intentInfo information about the (settings) intent to be launched + */ + fun startSettingsIntent(view: View, intentInfo: SettingsIntent) + + /** Called when the user succeed to drop notification to proper target view. */ + fun onDragSuccess(entry: NotificationEntry?) + + val isCollapsingToShowActivityOverLockscreen: Boolean + get() = false + + /** + * Information about a settings intent to be launched. + * + * If the [targetIntent] is T and [backStack] is [A, B, C], the stack will look like + * [A, B, C, T]. + */ + data class SettingsIntent( + var targetIntent: Intent, + var backStack: List<Intent> = emptyList(), + var cujType: Int? = null, + ) { + // Utility factory methods for known intents + companion object { + fun forNotificationSettings(cujType: Int? = null) = + SettingsIntent( + targetIntent = Intent(ACTION_NOTIFICATION_SETTINGS), + cujType = cujType, + ) + + fun forNotificationHistory(cujType: Int? = null) = + SettingsIntent( + targetIntent = Intent(ACTION_NOTIFICATION_HISTORY), + backStack = listOf(Intent(ACTION_NOTIFICATION_SETTINGS)), + cujType = cujType, + ) + + fun forModesSettings(cujType: Int? = null) = + SettingsIntent(targetIntent = Intent(ACTION_ZEN_MODE_SETTINGS), cujType = cujType) + + fun forModeSettings(modeId: String, cujType: Int? = null) = + SettingsIntent( + targetIntent = + Intent(ACTION_AUTOMATIC_ZEN_RULE_SETTINGS) + .putExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID, modeId), + backStack = listOf(Intent(ACTION_ZEN_MODE_SETTINGS)), + cujType = cujType, + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt index b342722ebb09..b67092ca9348 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt @@ -22,7 +22,7 @@ import com.android.internal.annotations.VisibleForTesting import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NOTIFICATIONS_USE_PEOPLE_FILTERING import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.notification.collection.NotificationClassificationFlag -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING import com.android.systemui.statusbar.notification.stack.BUCKET_FOREGROUND_SERVICE @@ -54,7 +54,7 @@ constructor(val proxy: DeviceConfigProxy, val context: Context) { fun getNotificationBuckets(): IntArray { if ( PriorityPeopleSection.isEnabled || - NotificationMinimalismPrototype.isEnabled || + NotificationMinimalism.isEnabled || NotificationClassificationFlag.isEnabled ) { // We don't need this list to be adaptive, it can be the superset of all features. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt index a621b2a02c5d..4e63b920a73d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt @@ -35,7 +35,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN import com.android.systemui.util.asIndenting @@ -77,7 +77,7 @@ constructor( private var unseenFilterEnabled = false override fun attach(pipeline: NotifPipeline) { - if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) { + if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) { return } pipeline.addPromoter(unseenNotifPromoter) @@ -129,26 +129,25 @@ constructor( } } - private fun unseenFeatureEnabled(): Flow<Boolean> { - // TODO(b/330387368): create LOCK_SCREEN_NOTIFICATION_MINIMALISM setting to use here? - // Or should we actually just repurpose using the existing setting? - if (NotificationMinimalismPrototype.isEnabled) { - return flowOf(true) + private fun minimalismFeatureSettingEnabled(): Flow<Boolean> { + if (!NotificationMinimalism.isEnabled) { + return flowOf(false) } - return seenNotificationsInteractor.isLockScreenShowOnlyUnseenNotificationsEnabled() + return seenNotificationsInteractor.isLockScreenNotificationMinimalismEnabled() } private suspend fun trackUnseenFilterSettingChanges() { - unseenFeatureEnabled().collectLatest { isSettingEnabled -> + // Only filter the seen notifs when the lock screen minimalism feature settings is on. + minimalismFeatureSettingEnabled().collectLatest { isMinimalismSettingEnabled -> // update local field and invalidate if necessary - if (isSettingEnabled != unseenFilterEnabled) { - unseenFilterEnabled = isSettingEnabled + if (isMinimalismSettingEnabled != unseenFilterEnabled) { + unseenFilterEnabled = isMinimalismSettingEnabled unseenNotifications.clear() unseenNotifPromoter.invalidateList("unseen setting changed") } // if the setting is enabled, then start tracking and filtering unseen notifications - logger.logTrackingUnseen(isSettingEnabled) - if (isSettingEnabled) { + logger.logTrackingUnseen(isMinimalismSettingEnabled) + if (isMinimalismSettingEnabled) { trackSeenNotifications() } } @@ -178,7 +177,7 @@ constructor( } private fun pickOutTopUnseenNotifs(list: List<ListEntry>) { - if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return + if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return if (!unseenFilterEnabled) return // Only ever elevate a top unseen notification on keyguard, not even locked shade if (statusBarStateController.state != StatusBarState.KEYGUARD) { @@ -215,9 +214,9 @@ constructor( object : NotifPromoter(TAG) { override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean = when { - NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode() -> false + NotificationMinimalism.isUnexpectedlyInLegacyMode() -> false seenNotificationsInteractor.isTopOngoingNotification(child) -> true - !NotificationMinimalismPrototype.ungroupTopUnseen -> false + !NotificationMinimalism.ungroupTopUnseen -> false else -> seenNotificationsInteractor.isTopUnseenNotification(child) } } @@ -225,7 +224,7 @@ constructor( val topOngoingSectioner = object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) { override fun isInSection(entry: ListEntry): Boolean { - if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false + if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return false return entry.anyEntry { notificationEntry -> seenNotificationsInteractor.isTopOngoingNotification(notificationEntry) } @@ -235,7 +234,7 @@ constructor( val topUnseenSectioner = object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) { override fun isInSection(entry: ListEntry): Boolean { - if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false + if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return false return entry.anyEntry { notificationEntry -> seenNotificationsInteractor.isTopUnseenNotification(notificationEntry) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt index e44a77c30999..a4fa72942380 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt @@ -34,7 +34,10 @@ constructor( TAG, LogLevel.DEBUG, messageInitializer = { bool1 = trackingUnseen }, - messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." }, + messagePrinter = { + "${if (bool1) "Start" else "Stop"} " + + "tracking unseen notifications because of settings change." + }, ) fun logShadeVisible(numUnseen: Int) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java index 0bbde21ba6a5..82ce31bfee2d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java @@ -25,7 +25,6 @@ import android.util.ArrayMap; import androidx.annotation.NonNull; import com.android.internal.statusbar.IStatusBarService; -import com.android.systemui.Flags; import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.collection.NotifPipeline; @@ -61,8 +60,27 @@ public class MediaCoordinator implements Coordinator { return false; } - if (!Flags.notificationsBackgroundIcons()) { - inflateOrUpdateIcons(entry); + switch (mIconsState.getOrDefault(entry, STATE_ICONS_UNINFLATED)) { + case STATE_ICONS_UNINFLATED: + try { + mIconManager.createIcons(entry); + mIconsState.put(entry, STATE_ICONS_INFLATED); + } catch (InflationException e) { + reportInflationError(entry, e); + mIconsState.put(entry, STATE_ICONS_ERROR); + } + break; + case STATE_ICONS_INFLATED: + try { + mIconManager.updateIcons(entry, /* usingCache = */ false); + } catch (InflationException e) { + reportInflationError(entry, e); + mIconsState.put(entry, STATE_ICONS_ERROR); + } + break; + case STATE_ICONS_ERROR: + // do nothing + break; } return true; @@ -72,19 +90,7 @@ public class MediaCoordinator implements Coordinator { private final NotifCollectionListener mCollectionListener = new NotifCollectionListener() { @Override public void onEntryInit(@NonNull NotificationEntry entry) { - // We default to STATE_ICONS_UNINFLATED anyway, so there's no need to initialize it. - if (!Flags.notificationsBackgroundIcons()) { - mIconsState.put(entry, STATE_ICONS_UNINFLATED); - } - } - - @Override - public void onEntryAdded(@NonNull NotificationEntry entry) { - if (Flags.notificationsBackgroundIcons()) { - if (isMediaNotification(entry.getSbn())) { - inflateOrUpdateIcons(entry); - } - } + mIconsState.put(entry, STATE_ICONS_UNINFLATED); } @Override @@ -93,12 +99,6 @@ public class MediaCoordinator implements Coordinator { // The update may have fixed the inflation error, so give it another chance. mIconsState.put(entry, STATE_ICONS_UNINFLATED); } - - if (Flags.notificationsBackgroundIcons()) { - if (isMediaNotification(entry.getSbn())) { - inflateOrUpdateIcons(entry); - } - } } @Override @@ -107,31 +107,6 @@ public class MediaCoordinator implements Coordinator { } }; - private void inflateOrUpdateIcons(NotificationEntry entry) { - switch (mIconsState.getOrDefault(entry, STATE_ICONS_UNINFLATED)) { - case STATE_ICONS_UNINFLATED: - try { - mIconManager.createIcons(entry); - mIconsState.put(entry, STATE_ICONS_INFLATED); - } catch (InflationException e) { - reportInflationError(entry, e); - mIconsState.put(entry, STATE_ICONS_ERROR); - } - break; - case STATE_ICONS_INFLATED: - try { - mIconManager.updateIcons(entry, /* usingCache = */ false); - } catch (InflationException e) { - reportInflationError(entry, e); - mIconsState.put(entry, STATE_ICONS_ERROR); - } - break; - case STATE_ICONS_ERROR: - // do nothing - break; - } - } - private void reportInflationError(NotificationEntry entry, Exception e) { // This is the same logic as in PreparationCoordinator; it doesn't handle media // notifications when the media feature is enabled since they aren't displayed in the shade, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt index 73ce48b2324a..96c260bb0852 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt @@ -25,7 +25,7 @@ import com.android.systemui.statusbar.notification.collection.SortBySectionTimeF import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection import javax.inject.Inject @@ -88,11 +88,10 @@ constructor( mCoordinators.add(hideLocallyDismissedNotifsCoordinator) mCoordinators.add(hideNotifsForOtherUsersCoordinator) mCoordinators.add(keyguardCoordinator) - if (NotificationMinimalismPrototype.isEnabled) { + if (NotificationMinimalism.isEnabled) { mCoordinators.add(lockScreenMinimalismCoordinator) - } else { - mCoordinators.add(unseenKeyguardCoordinator) } + mCoordinators.add(unseenKeyguardCoordinator) mCoordinators.add(rankingCoordinator) mCoordinators.add(colorizedFgsCoordinator) mCoordinators.add(deviceProvisionedCoordinator) @@ -125,11 +124,11 @@ constructor( } // Manually add Ordered Sections - if (NotificationMinimalismPrototype.isEnabled) { + if (NotificationMinimalism.isEnabled) { mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing } mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp - if (NotificationMinimalismPrototype.isEnabled) { + if (NotificationMinimalism.isEnabled) { mOrderedSections.add(lockScreenMinimalismCoordinator.topUnseenSectioner) // Top Unseen } mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt index bfea2ba6b839..cf1329c6b564 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt @@ -35,7 +35,6 @@ import com.android.systemui.statusbar.notification.collection.coordinator.dagger import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.headsUpEvents import com.android.systemui.util.asIndenting @@ -51,7 +50,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -87,7 +85,6 @@ constructor( private var unseenFilterEnabled = false override fun attach(pipeline: NotifPipeline) { - NotificationMinimalismPrototype.assertInLegacyMode() pipeline.addFinalizeFilter(unseenNotifFilter) pipeline.addCollectionListener(collectionListener) scope.launch { trackUnseenFilterSettingChanges() } @@ -253,10 +250,6 @@ constructor( } private fun unseenFeatureEnabled(): Flow<Boolean> { - if (NotificationMinimalismPrototype.isEnabled) { - // TODO(b/330387368): should this really just be turned off? If so, hide the setting. - return flowOf(false) - } return seenNotificationsInteractor.isLockScreenShowOnlyUnseenNotificationsEnabled() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt index fe59d732cceb..1fe32c9a873a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt @@ -45,17 +45,16 @@ private const val TAG = "RemoteInputCoordinator" /** * How long to wait before auto-dismissing a notification that was kept for active remote input, and - * has now sent a remote input. We auto-dismiss, because the app may not cannot cancel - * these given that they technically don't exist anymore. We wait a bit in case the app issues - * an update, and to also give the other lifetime extenders a beat to decide they want it. + * has now sent a remote input. We auto-dismiss, because the app may not cannot cancel these given + * that they technically don't exist anymore. We wait a bit in case the app issues an update, and to + * also give the other lifetime extenders a beat to decide they want it. */ private const val REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY: Long = 500 /** * How long to wait before releasing a lifetime extension when requested to do so due to a user - * interaction (such as tapping another action). - * We wait a bit in case the app issues an update in response to the action, but not too long or we - * risk appearing unresponsive to the user. + * interaction (such as tapping another action). We wait a bit in case the app issues an update in + * response to the action, but not too long or we risk appearing unresponsive to the user. */ private const val REMOTE_INPUT_EXTENDER_RELEASE_DELAY: Long = 200 @@ -63,22 +62,21 @@ private const val REMOTE_INPUT_EXTENDER_RELEASE_DELAY: Long = 200 private val DEBUG: Boolean by lazy { Log.isLoggable(TAG, Log.DEBUG) } @CoordinatorScope -class RemoteInputCoordinator @Inject constructor( +class RemoteInputCoordinator +@Inject +constructor( dumpManager: DumpManager, private val mRebuilder: RemoteInputNotificationRebuilder, private val mNotificationRemoteInputManager: NotificationRemoteInputManager, @Main private val mMainHandler: Handler, - private val mSmartReplyController: SmartReplyController + private val mSmartReplyController: SmartReplyController, ) : Coordinator, RemoteInputListener, Dumpable { @VisibleForTesting val mRemoteInputHistoryExtender = RemoteInputHistoryExtender() @VisibleForTesting val mSmartReplyHistoryExtender = SmartReplyHistoryExtender() @VisibleForTesting val mRemoteInputActiveExtender = RemoteInputActiveExtender() - private val mRemoteInputLifetimeExtenders = listOf( - mRemoteInputHistoryExtender, - mSmartReplyHistoryExtender, - mRemoteInputActiveExtender - ) + private val mRemoteInputLifetimeExtenders = + listOf(mRemoteInputHistoryExtender, mSmartReplyHistoryExtender, mRemoteInputActiveExtender) private lateinit var mNotifUpdater: InternalNotifUpdater @@ -93,9 +91,7 @@ class RemoteInputCoordinator @Inject constructor( if (lifetimeExtensionRefactor()) { pipeline.addNotificationLifetimeExtender(mRemoteInputActiveExtender) } else { - mRemoteInputLifetimeExtenders.forEach { - pipeline.addNotificationLifetimeExtender(it) - } + mRemoteInputLifetimeExtenders.forEach { pipeline.addNotificationLifetimeExtender(it) } } mNotifUpdater = pipeline.getInternalNotifUpdater(TAG) pipeline.addCollectionListener(mCollectionListener) @@ -105,64 +101,74 @@ class RemoteInputCoordinator @Inject constructor( * Listener that updates the appearance of the notification if it has been lifetime extended * by a a direct reply or a smart reply, and cancelled. */ - val mCollectionListener = object : NotifCollectionListener { - override fun onEntryUpdated(entry: NotificationEntry, fromSystem: Boolean) { - if (DEBUG) { - Log.d(TAG, "mCollectionListener.onEntryUpdated(entry=${entry.key}," + - " fromSystem=$fromSystem)") - } - if (fromSystem) { - if (lifetimeExtensionRefactor()) { - if ((entry.getSbn().getNotification().flags - and FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) { - if (mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory( - entry)) { - val newSbn = mRebuilder.rebuildForRemoteInputReply(entry) - entry.onRemoteInputInserted() - mNotifUpdater.onInternalNotificationUpdate(newSbn, - "Extending lifetime of notification with remote input") - } else if (mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory( - entry)) { - val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry) - mSmartReplyController.stopSending(entry) - mNotifUpdater.onInternalNotificationUpdate(newSbn, - "Extending lifetime of notification with smart reply") + val mCollectionListener = + object : NotifCollectionListener { + override fun onEntryUpdated(entry: NotificationEntry, fromSystem: Boolean) { + if (DEBUG) { + Log.d( + TAG, + "mCollectionListener.onEntryUpdated(entry=${entry.key}," + + " fromSystem=$fromSystem)", + ) + } + if (fromSystem) { + if (lifetimeExtensionRefactor()) { + if ( + (entry.getSbn().getNotification().flags and + FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0 + ) { + // If we've received an update from the system and the entry is marked + // as lifetime extended, that means system server has received a + // cancelation in response to a direct reply, and sent an update to + // let system ui know that it should rebuild the notification with + // that direct reply. + if ( + mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory( + entry + ) + ) { + val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry) + mSmartReplyController.stopSending(entry) + mNotifUpdater.onInternalNotificationUpdate( + newSbn, + "Extending lifetime of notification with smart reply", + ) + } else { + val newSbn = mRebuilder.rebuildForRemoteInputReply(entry) + entry.onRemoteInputInserted() + mNotifUpdater.onInternalNotificationUpdate( + newSbn, + "Extending lifetime of notification with remote input", + ) + } } else { - // The app may have re-cancelled a notification after it had already - // been lifetime extended. - // Rebuild the notification with the replies it already had to ensure - // those replies continue to be displayed. - val newSbn = mRebuilder.rebuildWithExistingReplies(entry) - mNotifUpdater.onInternalNotificationUpdate(newSbn, - "Extending lifetime of notification that has already been " + - "lifetime extended.") + // Notifications updated without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY + // should have their remote inputs list cleared. + entry.remoteInputs = null } } else { - // Notifications updated without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY - // should have their remote inputs list cleared. - entry.remoteInputs = null + // Mark smart replies as sent whenever a notification is updated by the app, + // otherwise the smart replies are never marked as sent. + mSmartReplyController.stopSending(entry) } - } else { - // Mark smart replies as sent whenever a notification is updated by the app, - // otherwise the smart replies are never marked as sent. - mSmartReplyController.stopSending(entry) } } - } - override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { - if (DEBUG) Log.d(TAG, "mCollectionListener.onEntryRemoved(entry=${entry.key})") - // We're removing the notification, the smart reply controller can forget about it. - // TODO(b/145659174): track 'sending' state on the entry to avoid having to clear it. - mSmartReplyController.stopSending(entry) - - // When we know the entry will not be lifetime extended, clean up the remote input view - // TODO: Share code with NotifCollection.cannotBeLifetimeExtended - if (reason == REASON_CANCEL || reason == REASON_CLICK) { - mNotificationRemoteInputManager.cleanUpRemoteInputForUserRemoval(entry) + override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { + if (DEBUG) Log.d(TAG, "mCollectionListener.onEntryRemoved(entry=${entry.key})") + // We're removing the notification, the smart reply controller can forget about it. + // TODO(b/145659174): track 'sending' state on the entry to avoid having to clear + // it. + mSmartReplyController.stopSending(entry) + + // When we know the entry will not be lifetime extended, clean up the remote input + // view + // TODO: Share code with NotifCollection.cannotBeLifetimeExtended + if (reason == REASON_CANCEL || reason == REASON_CLICK) { + mNotificationRemoteInputManager.cleanUpRemoteInputForUserRemoval(entry) + } } } - } override fun dump(pw: PrintWriter, args: Array<out String>) { mRemoteInputLifetimeExtenders.forEach { it.dump(pw, args) } @@ -183,22 +189,25 @@ class RemoteInputCoordinator @Inject constructor( // view it is already canceled, so we'll need to cancel it on the apps behalf // now that a reply has been sent. However, delay so that the app has time to posts an // update in the mean time, and to give another lifetime extender time to pick it up. - mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key, - REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY) + mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay( + entry.key, + REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY, + ) } private fun onSmartReplySent(entry: NotificationEntry, reply: CharSequence) { if (DEBUG) Log.d(TAG, "onSmartReplySent(entry=${entry.key})") val newSbn = mRebuilder.rebuildForSendingSmartReply(entry, reply) - mNotifUpdater.onInternalNotificationUpdate(newSbn, - "Adding smart reply spinner for sent") + mNotifUpdater.onInternalNotificationUpdate(newSbn, "Adding smart reply spinner for sent") // If we're extending for remote input being active, then from the apps point of // view it is already canceled, so we'll need to cancel it on the apps behalf // now that a reply has been sent. However, delay so that the app has time to posts an // update in the mean time, and to give another lifetime extender time to pick it up. - mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key, - REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY) + mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay( + entry.key, + REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY, + ) } override fun onPanelCollapsed() { @@ -208,19 +217,25 @@ class RemoteInputCoordinator @Inject constructor( override fun isNotificationKeptForRemoteInputHistory(key: String) = if (!lifetimeExtensionRefactor()) { mRemoteInputHistoryExtender.isExtending(key) || - mSmartReplyHistoryExtender.isExtending(key) + mSmartReplyHistoryExtender.isExtending(key) } else false override fun releaseNotificationIfKeptForRemoteInputHistory(entry: NotificationEntry) { if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entry.key})") if (!lifetimeExtensionRefactor()) { - mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(entry.key, - REMOTE_INPUT_EXTENDER_RELEASE_DELAY) - mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(entry.key, - REMOTE_INPUT_EXTENDER_RELEASE_DELAY) + mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay( + entry.key, + REMOTE_INPUT_EXTENDER_RELEASE_DELAY, + ) + mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay( + entry.key, + REMOTE_INPUT_EXTENDER_RELEASE_DELAY, + ) } - mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key, - REMOTE_INPUT_EXTENDER_RELEASE_DELAY) + mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay( + entry.key, + REMOTE_INPUT_EXTENDER_RELEASE_DELAY, + ) } override fun setRemoteInputController(remoteInputController: RemoteInputController) { @@ -229,32 +244,36 @@ class RemoteInputCoordinator @Inject constructor( @VisibleForTesting inner class RemoteInputHistoryExtender : - SelfTrackingLifetimeExtender(TAG, "RemoteInputHistory", DEBUG, mMainHandler) { + SelfTrackingLifetimeExtender(TAG, "RemoteInputHistory", DEBUG, mMainHandler) { override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean = - mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(entry) + mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(entry) override fun onStartedLifetimeExtension(entry: NotificationEntry) { val newSbn = mRebuilder.rebuildForRemoteInputReply(entry) entry.onRemoteInputInserted() - mNotifUpdater.onInternalNotificationUpdate(newSbn, - "Extending lifetime of notification with remote input") + mNotifUpdater.onInternalNotificationUpdate( + newSbn, + "Extending lifetime of notification with remote input", + ) // TODO: Check if the entry was removed due perhaps to an inflation exception? } } @VisibleForTesting inner class SmartReplyHistoryExtender : - SelfTrackingLifetimeExtender(TAG, "SmartReplyHistory", DEBUG, mMainHandler) { + SelfTrackingLifetimeExtender(TAG, "SmartReplyHistory", DEBUG, mMainHandler) { override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean = - mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(entry) + mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(entry) override fun onStartedLifetimeExtension(entry: NotificationEntry) { val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry) mSmartReplyController.stopSending(entry) - mNotifUpdater.onInternalNotificationUpdate(newSbn, - "Extending lifetime of notification with smart reply") + mNotifUpdater.onInternalNotificationUpdate( + newSbn, + "Extending lifetime of notification with smart reply", + ) // TODO: Check if the entry was removed due perhaps to an inflation exception? } @@ -266,9 +285,9 @@ class RemoteInputCoordinator @Inject constructor( @VisibleForTesting inner class RemoteInputActiveExtender : - SelfTrackingLifetimeExtender(TAG, "RemoteInputActive", DEBUG, mMainHandler) { + SelfTrackingLifetimeExtender(TAG, "RemoteInputActive", DEBUG, mMainHandler) { override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean = - mNotificationRemoteInputManager.isRemoteInputActive(entry) + mNotificationRemoteInputManager.isRemoteInputActive(entry) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java index 6d0148a24cf8..41419f31eb7a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java @@ -41,7 +41,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager; import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider; import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor; -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype; +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.kotlin.BooleanFlowOperators; @@ -170,7 +170,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { if (entry == null) { return false; } - boolean isTopUnseen = NotificationMinimalismPrototype.isEnabled() + boolean isTopUnseen = NotificationMinimalism.isEnabled() && (mSeenNotificationsInteractor.isTopUnseenNotification(entry) || mSeenNotificationsInteractor.isTopOngoingNotification(entry)); if (isTopUnseen || mHeadsUpManager.isHeadsUpEntry(entry.getKey())) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt index 29564326481f..1babe47559e5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt @@ -23,7 +23,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.util.printSection import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow @@ -57,25 +57,25 @@ constructor( /** Set the entry that is identified as the top ongoing notification. */ fun setTopOngoingNotification(entry: NotificationEntry?) { - if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return + if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return notificationListRepository.topOngoingNotificationKey.value = entry?.key } /** Determine if the given notification is the top ongoing notification. */ fun isTopOngoingNotification(entry: NotificationEntry?): Boolean = - if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false + if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) false else entry != null && notificationListRepository.topOngoingNotificationKey.value == entry.key /** Set the entry that is identified as the top unseen notification. */ fun setTopUnseenNotification(entry: NotificationEntry?) { - if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return + if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return notificationListRepository.topUnseenNotificationKey.value = entry?.key } /** Determine if the given notification is the top unseen notification. */ fun isTopUnseenNotification(entry: NotificationEntry?): Boolean = - if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false + if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) false else entry != null && notificationListRepository.topUnseenNotificationKey.value == entry.key fun dump(pw: IndentingPrintWriter) = @@ -120,4 +120,29 @@ constructor( // only track the most recent emission, if events are happening faster than they can be // consumed .conflate() + + fun isLockScreenNotificationMinimalismEnabled(): Flow<Boolean> = + secureSettings + // emit whenever the setting has changed + .observerFlow( + UserHandle.USER_ALL, + Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM, + ) + // perform a query immediately + .onStart { emit(Unit) } + // for each change, lookup the new value + .map { + secureSettings.getIntForUser( + name = Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM, + default = 1, + userHandle = UserHandle.USER_CURRENT, + ) == 1 + } + // don't emit anything if nothing has changed + .distinctUntilChanged() + // perform lookups on the bg thread pool + .flowOn(bgDispatcher) + // only track the most recent emission, if events are happening faster than they can be + // consumed + .conflate() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt new file mode 100644 index 000000000000..f1fc2751d11f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.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.notification.emptyshade.shared + +import android.app.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the modes_ui_empty_shade flag state. */ +@Suppress("NOTHING_TO_INLINE") +object ModesEmptyShadeFix { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_MODES_UI_EMPTY_SHADE + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.modesUiEmptyShade() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is not enabled to ensure that the refactor author catches issues in testing. + * Caution!! Using this check incorrectly will cause crashes in nextfood builds! + */ + @JvmStatic + inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java index 850e9447beea..73477da247f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java @@ -22,6 +22,7 @@ import android.annotation.StringRes; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Configuration; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; @@ -29,26 +30,71 @@ import android.widget.TextView; import androidx.annotation.NonNull; +import com.android.systemui.animation.LaunchableView; +import com.android.systemui.animation.LaunchableViewDelegate; import com.android.systemui.res.R; +import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix; import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; import com.android.systemui.statusbar.notification.stack.ExpandableViewState; -public class EmptyShadeView extends StackScrollerDecorView { +import kotlin.Unit; + +import java.util.Objects; + +public class EmptyShadeView extends StackScrollerDecorView implements LaunchableView { private TextView mEmptyText; private TextView mEmptyFooterText; - private @StringRes int mText = R.string.empty_shade_text; + private @StringRes int mTextId = R.string.empty_shade_text; + private String mTextString; - private @DrawableRes int mFooterIcon = R.drawable.ic_friction_lock_closed; - private @StringRes int mFooterText = R.string.unlock_to_see_notif_text; + private @DrawableRes int mFooterIcon; + private @StringRes int mFooterText; + // This view is initially gone in the xml. private @Visibility int mFooterVisibility = View.GONE; private int mSize; + private LaunchableViewDelegate mLaunchableViewDelegate = new LaunchableViewDelegate(this, + visibility -> { + super.setVisibility(visibility); + return Unit.INSTANCE; + }); + public EmptyShadeView(Context context, AttributeSet attrs) { super(context, attrs); mSize = getResources().getDimensionPixelSize( R.dimen.notifications_unseen_footer_icon_size); + if (ModesEmptyShadeFix.isEnabled()) { + mTextString = getContext().getString(R.string.empty_shade_text); + } else { + // These will be set by the binder when appropriate if ModesEmptyShadeFix is on. + mFooterIcon = R.drawable.ic_friction_lock_closed; + mFooterText = R.string.unlock_to_see_notif_text; + } + } + + @Override + public void setVisibility(int visibility) { + mLaunchableViewDelegate.setVisibility(visibility); + } + + @Override + public void setShouldBlockVisibilityChanges(boolean block) { + /* check if */ ModesEmptyShadeFix.isUnexpectedlyInLegacyMode(); + mLaunchableViewDelegate.setShouldBlockVisibilityChanges(block); + } + + @Override + public void onActivityLaunchAnimationEnd() { + /* check if */ ModesEmptyShadeFix.isUnexpectedlyInLegacyMode(); + } + + @Override + @NonNull + public Rect getPaddingForLaunchAnimation() { + /* check if */ ModesEmptyShadeFix.isUnexpectedlyInLegacyMode(); + return new Rect(); } @Override @@ -56,7 +102,11 @@ public class EmptyShadeView extends StackScrollerDecorView { super.onConfigurationChanged(newConfig); mSize = getResources().getDimensionPixelSize( R.dimen.notifications_unseen_footer_icon_size); - mEmptyText.setText(mText); + if (ModesEmptyShadeFix.isEnabled()) { + mEmptyText.setText(mTextString); + } else { + mEmptyText.setText(mTextId); + } mEmptyFooterText.setVisibility(mFooterVisibility); setFooterText(mFooterText); setFooterIcon(mFooterIcon); @@ -72,25 +122,45 @@ public class EmptyShadeView extends StackScrollerDecorView { return findViewById(R.id.no_notifications_footer); } + /** Update view colors. */ public void setTextColors(@ColorInt int onSurface, @ColorInt int onSurfaceVariant) { mEmptyText.setTextColor(onSurfaceVariant); mEmptyFooterText.setTextColor(onSurface); mEmptyFooterText.setCompoundDrawableTintList(ColorStateList.valueOf(onSurface)); } + /** Set the resource ID for the main text shown by the view. */ public void setText(@StringRes int text) { - mText = text; - mEmptyText.setText(mText); + ModesEmptyShadeFix.assertInLegacyMode(); + mTextId = text; + mEmptyText.setText(mTextId); } + /** Set the string for the main text shown by the view. */ + public void setText(String text) { + if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode() || Objects.equals(mTextString, text)) { + return; + } + mTextString = text; + mEmptyText.setText(text); + } + + /** Visibility for the footer (the additional icon+text shown below the main text). */ public void setFooterVisibility(@Visibility int visibility) { + if (ModesEmptyShadeFix.isEnabled() && mFooterVisibility == visibility) { + return; // nothing to change + } mFooterVisibility = visibility; setSecondaryVisible(/* visible = */ visibility == View.VISIBLE, /* animate = */false, /* onAnimationEnded = */ null); } + /** Text resource ID for the footer (the additional icon+text shown below the main text). */ public void setFooterText(@StringRes int text) { + if (ModesEmptyShadeFix.isEnabled() && mFooterText == text) { + return; // nothing to change + } mFooterText = text; if (text != 0) { mEmptyFooterText.setText(mFooterText); @@ -99,7 +169,11 @@ public class EmptyShadeView extends StackScrollerDecorView { } } + /** Icon resource ID for the footer (the additional icon+text shown below the main text). */ public void setFooterIcon(@DrawableRes int icon) { + if (ModesEmptyShadeFix.isEnabled() && mFooterIcon == icon) { + return; // nothing to change + } mFooterIcon = icon; Drawable drawable; if (icon == 0) { @@ -111,18 +185,24 @@ public class EmptyShadeView extends StackScrollerDecorView { mEmptyFooterText.setCompoundDrawablesRelative(drawable, null, null, null); } + /** Get resource ID for main text. */ @StringRes public int getTextResource() { - return mText; + ModesEmptyShadeFix.assertInLegacyMode(); + return mTextId; } + /** Get resource ID for footer text. */ @StringRes public int getFooterTextResource() { + ModesEmptyShadeFix.assertInLegacyMode(); return mFooterText; } + /** Get resource ID for footer icon. */ @DrawableRes public int getFooterIconResource() { + ModesEmptyShadeFix.assertInLegacyMode(); return mFooterIcon; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt new file mode 100644 index 000000000000..7f1b04358546 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.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.systemui.statusbar.notification.emptyshade.ui.viewbinder + +import android.view.View +import com.android.systemui.statusbar.notification.NotificationActivityStarter +import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView +import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch + +object EmptyShadeViewBinder { + suspend fun bind( + view: EmptyShadeView, + viewModel: EmptyShadeViewModel, + notificationActivityStarter: NotificationActivityStarter, + ) = coroutineScope { + launch { viewModel.text.collect { view.setText(it) } } + + launch { + viewModel.onClick.collect { settingsIntent -> + val onClickListener = { view: View -> + notificationActivityStarter.startSettingsIntent(view, settingsIntent) + } + view.setOnClickListener(onClickListener) + } + } + + launch { bindFooter(view, viewModel) } + } + + private suspend fun bindFooter(view: EmptyShadeView, viewModel: EmptyShadeViewModel) = + coroutineScope { + // Bind the resource IDs + view.setFooterText(viewModel.footer.messageId) + view.setFooterIcon(viewModel.footer.iconId) + + launch { + viewModel.footer.isVisible.collect { visible -> + view.setFooterVisibility(if (visible) View.VISIBLE else View.GONE) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt new file mode 100644 index 000000000000..8c8f200f78b7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt @@ -0,0 +1,146 @@ +/* + * 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.emptyshade.ui.viewmodel + +import android.content.Context +import android.icu.text.MessageFormat +import com.android.systemui.dump.DumpManager +import com.android.systemui.modes.shared.ModesUi +import com.android.systemui.res.R +import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor +import com.android.systemui.statusbar.notification.NotificationActivityStarter.SettingsIntent +import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor +import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix +import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor +import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterMessageViewModel +import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor +import com.android.systemui.util.kotlin.FlowDumperImpl +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import java.util.Locale +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +/** + * ViewModel for the empty shade (aka the "No notifications" text shown when there are no + * notifications. + */ +class EmptyShadeViewModel +@AssistedInject +constructor( + private val context: Context, + zenModeInteractor: ZenModeInteractor, + seenNotificationsInteractor: SeenNotificationsInteractor, + notificationSettingsInteractor: NotificationSettingsInteractor, + dumpManager: DumpManager, +) : FlowDumperImpl(dumpManager) { + val areNotificationsHiddenInShade: Flow<Boolean> by lazy { + if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { + flowOf(false) + } else { + zenModeInteractor.areNotificationsHiddenInShade.dumpWhileCollecting( + "areNotificationsHiddenInShade" + ) + } + } + + val hasFilteredOutSeenNotifications: StateFlow<Boolean> by lazy { + if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { + MutableStateFlow(false) + } else { + seenNotificationsInteractor.hasFilteredOutSeenNotifications.dumpValue( + "hasFilteredOutSeenNotifications" + ) + } + } + + val text: Flow<String> by lazy { + if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) { + flowOf(context.getString(R.string.empty_shade_text)) + } else { + // Note: Flag modes_ui_empty_shade includes two pieces: refactoring the empty shade to + // recommended architecture, and making it so it reacts to changes for the new Modes. + // The former does not depend on the modes flags being on, but the latter does. + if (ModesUi.isEnabled) { + zenModeInteractor.modesHidingNotifications.map { modes -> + // Create a string that is either "No notifications" if no modes are filtering + // them out, or something like "Notifications paused by SomeMode" otherwise. + val msgFormat = + MessageFormat( + context.getString(R.string.modes_suppressing_shade_text), + Locale.getDefault(), + ) + val count = modes.count() + val args: MutableMap<String, Any> = HashMap() + args["count"] = count + if (count >= 1) { + args["mode"] = modes[0].name + } + msgFormat.format(args) + } + } else { + areNotificationsHiddenInShade.map { areNotificationsHiddenInShade -> + if (areNotificationsHiddenInShade) { + context.getString(R.string.dnd_suppressing_shade_text) + } else { + context.getString(R.string.empty_shade_text) + } + } + } + } + } + + val footer: FooterMessageViewModel by lazy { + ModesEmptyShadeFix.assertInNewMode() + FooterMessageViewModel( + messageId = R.string.unlock_to_see_notif_text, + iconId = R.drawable.ic_friction_lock_closed, + isVisible = hasFilteredOutSeenNotifications, + ) + } + + val onClick: Flow<SettingsIntent> by lazy { + ModesEmptyShadeFix.assertInNewMode() + combine( + zenModeInteractor.modesHidingNotifications, + notificationSettingsInteractor.isNotificationHistoryEnabled, + ) { modes, isNotificationHistoryEnabled -> + if (modes.isNotEmpty()) { + if (modes.size == 1) { + SettingsIntent.forModeSettings(modes[0].id) + } else { + SettingsIntent.forModesSettings() + } + } else { + if (isNotificationHistoryEnabled) { + SettingsIntent.forNotificationHistory() + } else { + SettingsIntent.forNotificationSettings() + } + } + } + } + + @AssistedFactory + interface Factory { + fun create(): EmptyShadeViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt index 920541d101cf..22bec5a43230 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt @@ -19,6 +19,8 @@ package com.android.systemui.statusbar.notification.footer.ui.viewbinder import android.view.View import androidx.lifecycle.lifecycleScope import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.notification.NotificationActivityStarter +import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix import com.android.systemui.statusbar.notification.footer.ui.view.FooterView import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel import com.android.systemui.util.ui.isAnimating @@ -36,6 +38,7 @@ object FooterViewBinder { clearAllNotifications: View.OnClickListener, launchNotificationSettings: View.OnClickListener, launchNotificationHistory: View.OnClickListener, + notificationActivityStarter: NotificationActivityStarter, ): DisposableHandle { return footer.repeatWhenAttached { lifecycleScope.launch { @@ -45,6 +48,7 @@ object FooterViewBinder { clearAllNotifications, launchNotificationSettings, launchNotificationHistory, + notificationActivityStarter, ) } } @@ -56,6 +60,7 @@ object FooterViewBinder { clearAllNotifications: View.OnClickListener, launchNotificationSettings: View.OnClickListener, launchNotificationHistory: View.OnClickListener, + notificationActivityStarter: NotificationActivityStarter, ) = coroutineScope { launch { bindClearAllButton(footer, viewModel, clearAllNotifications) } launch { @@ -64,6 +69,7 @@ object FooterViewBinder { viewModel, launchNotificationSettings, launchNotificationHistory, + notificationActivityStarter, ) } launch { bindMessage(footer, viewModel) } @@ -113,13 +119,23 @@ object FooterViewBinder { viewModel: FooterViewModel, launchNotificationSettings: View.OnClickListener, launchNotificationHistory: View.OnClickListener, + notificationActivityStarter: NotificationActivityStarter, ) = coroutineScope { launch { - viewModel.manageButtonShouldLaunchHistory.collect { shouldLaunchHistory -> - if (shouldLaunchHistory) { - footer.setManageButtonClickListener(launchNotificationHistory) - } else { - footer.setManageButtonClickListener(launchNotificationSettings) + if (ModesEmptyShadeFix.isEnabled) { + viewModel.manageOrHistoryButtonClick.collect { settingsIntent -> + val onClickListener = { view: View -> + notificationActivityStarter.startSettingsIntent(view, settingsIntent) + } + footer.setManageButtonClickListener(onClickListener) + } + } else { + viewModel.manageButtonShouldLaunchHistory.collect { shouldLaunchHistory -> + if (shouldLaunchHistory) { + footer.setManageButtonClickListener(launchNotificationHistory) + } else { + footer.setManageButtonClickListener(launchNotificationSettings) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt index 90fb7285e939..a3f4cd225130 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt @@ -16,12 +16,17 @@ package com.android.systemui.statusbar.notification.footer.ui.viewmodel +import android.content.Intent +import android.provider.Settings +import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor +import com.android.systemui.statusbar.notification.NotificationActivityStarter.SettingsIntent import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor +import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.footer.ui.view.FooterView import com.android.systemui.util.kotlin.sample @@ -80,7 +85,7 @@ class FooterViewModel( combine( shadeInteractor.isShadeFullyExpanded, shadeInteractor.isShadeTouchable, - ::Pair + ::Pair, ) .onStart { emit(Pair(false, false)) } ) { clearAllButtonVisible, (isShadeFullyExpanded, animationsEnabled) -> @@ -93,8 +98,28 @@ class FooterViewModel( val manageButtonShouldLaunchHistory = notificationSettingsInteractor.isNotificationHistoryEnabled + // TODO(b/366003631): When inlining the flag, consider adding this to FooterButtonViewModel. + val manageOrHistoryButtonClick: Flow<SettingsIntent> by lazy { + if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) { + flowOf(SettingsIntent(Intent(Settings.ACTION_NOTIFICATION_SETTINGS))) + } else { + notificationSettingsInteractor.isNotificationHistoryEnabled.map { + isNotificationHistoryEnabled -> + if (isNotificationHistoryEnabled) { + SettingsIntent.forNotificationHistory( + cujType = InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON + ) + } else { + SettingsIntent.forNotificationSettings( + cujType = InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON + ) + } + } + } + } + private val manageOrHistoryButtonText: Flow<Int> = - manageButtonShouldLaunchHistory.map { shouldLaunchHistory -> + notificationSettingsInteractor.isNotificationHistoryEnabled.map { shouldLaunchHistory -> if (shouldLaunchHistory) R.string.manage_notifications_history_text else R.string.manage_notifications_text } @@ -128,7 +153,7 @@ object FooterViewModelModule { activeNotificationsInteractor.get(), notificationSettingsInteractor.get(), seenNotificationsInteractor.get(), - shadeInteractor.get() + shadeInteractor.get(), ) ) } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt index da29b0fd0dc7..ec5ebc3651ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt @@ -43,7 +43,7 @@ interface RichOngoingNotificationContentExtractor { entry: NotificationEntry, builder: Notification.Builder, systemUIContext: Context, - packageContext: Context + packageContext: Context, ): RichOngoingContentModel? } @@ -52,7 +52,7 @@ class NoOpRichOngoingNotificationContentExtractor : RichOngoingNotificationConte entry: NotificationEntry, builder: Notification.Builder, systemUIContext: Context, - packageContext: Context + packageContext: Context, ): RichOngoingContentModel? = null } @@ -68,7 +68,7 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : entry: NotificationEntry, builder: Notification.Builder, systemUIContext: Context, - packageContext: Context + packageContext: Context, ): RichOngoingContentModel? { val sbn = entry.sbn val notification = sbn.notification @@ -89,7 +89,7 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : null } } - } else if (builder.style is Notification.EnRouteStyle) { + } else if (builder.style is Notification.ProgressStyle) { parseEnRouteNotification(notification, icon) } else null } catch (e: Exception) { @@ -104,7 +104,7 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : */ private fun parseTimerNotification( notification: Notification, - icon: IconModel + icon: IconModel, ): TimerContentModel { // sortKey=1 0|↺7|RUNNING|▶16:21:58.523|Σ0:05:00|Δ0:00:03|⏳0:04:57 // sortKey=1 0|↺7|PAUSED|Σ0:05:00|Δ0:04:54|⏳0:00:06 @@ -132,7 +132,7 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : resumeIntent = notification.findStartIntent(), addMinuteAction = notification.findAddMinuteAction(), resetAction = notification.findResetAction(), - ) + ), ) } "RUNNING" -> { @@ -149,7 +149,7 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : pauseIntent = notification.findPauseIntent(), addMinuteAction = notification.findAddMinuteAction(), resetAction = notification.findResetAction(), - ) + ), ) } else -> error("unknown state ($state) in sortKey=$sortKey") @@ -192,7 +192,7 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : val localDateTime = LocalDateTime.of( LocalDate.now(), - LocalTime.of(hour.toInt(), minute.toInt(), second.toInt(), millis.toInt() * 1000000) + LocalTime.of(hour.toInt(), minute.toInt(), second.toInt(), millis.toInt() * 1000000), ) val offset = ZoneId.systemDefault().rules.getOffset(localDateTime) return localDateTime.toInstant(offset).toEpochMilli() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java index 291dc132686b..cd228e7872c1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java @@ -72,14 +72,24 @@ public abstract class StackScrollerDecorView extends ExpandableView { } /** + * See {@link #setVisible(boolean, boolean, Consumer)}. + */ + public void setVisible(boolean visible, boolean animate) { + setVisible(visible, animate, null /* onAnimationEnded */); + } + + /** * Make this view visible. If {@code false} is passed, the view will fade out its content * and set the view Visibility to GONE. If only the content should be changed, * {@link #setContentVisibleAnimated(boolean)} can be used. * * @param visible True if the contents should be visible. * @param animate True if we should fade to new visibility. + * @param onAnimationEnded Callback to run after visibility updates, takes a boolean as a + * parameter that represents whether the animation was cancelled. */ - public void setVisible(boolean visible, boolean animate) { + public void setVisible(boolean visible, boolean animate, + Consumer<Boolean> onAnimationEnded) { if (mIsVisible != visible) { mIsVisible = visible; if (animate) { @@ -90,10 +100,10 @@ public abstract class StackScrollerDecorView extends ExpandableView { } else { setWillBeGone(true); } - setContentVisible(visible, true /* animate */, null /* onAnimationEnded */); + setContentVisible(visible, true /* animate */, onAnimationEnded); } else { setVisibility(visible ? VISIBLE : GONE); - setContentVisible(visible, false /* animate */, null /* onAnimationEnded */); + setContentVisible(visible, false /* animate */, onAnimationEnded); setWillBeGone(false); notifyHeightChanged(false /* needsAnimation */); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalism.kt index 06f3db504aaf..70bb2722c678 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalism.kt @@ -17,23 +17,23 @@ package com.android.systemui.statusbar.notification.shared import android.os.SystemProperties -import com.android.systemui.Flags +import com.android.server.notification.Flags import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils /** Helper for reading or using the minimalism prototype flag state. */ @Suppress("NOTHING_TO_INLINE") -object NotificationMinimalismPrototype { - const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE +object NotificationMinimalism { + const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM /** A token used for dependency declaration */ val token: FlagToken get() = FlagToken(FLAG_NAME, isEnabled) - /** Is the heads-up cycling animation enabled */ + /** Is the notification minimalism enabled */ @JvmStatic inline val isEnabled - get() = Flags.notificationMinimalismPrototype() + get() = Flags.notificationMinimalism() /** * The prototype will (by default) use a promoter to ensure that the top unseen notification is diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java index 1431b28bf794..129d4cee9cdb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java @@ -88,6 +88,8 @@ public class AmbientState implements Dumpable { private ExpandableView mLastVisibleBackgroundChild; private float mCurrentScrollVelocity; private int mStatusBarState; + private boolean mShowingStackOnLockscreen; + private float mLockscreenStackFadeInProgress; private float mExpandingVelocity; private boolean mPanelTracking; private boolean mExpansionChanging; @@ -222,6 +224,7 @@ public class AmbientState implements Dumpable { * @param isSwipingUp Whether we are swiping up. */ public void setSwipingUp(boolean isSwipingUp) { + SceneContainerFlag.assertInLegacyMode(); if (!isSwipingUp && mIsSwipingUp) { // Just stopped swiping up. mIsFlingRequiredAfterLockScreenSwipeUp = true; @@ -240,6 +243,7 @@ public class AmbientState implements Dumpable { * @param isFlinging Whether we are flinging the shade open or closed. */ public void setFlinging(boolean isFlinging) { + SceneContainerFlag.assertInLegacyMode(); if (isOnKeyguard() && !isFlinging && mIsFlinging) { // Just stopped flinging. mIsFlingRequiredAfterLockScreenSwipeUp = false; @@ -624,6 +628,26 @@ public class AmbientState implements Dumpable { return mStatusBarState == StatusBarState.KEYGUARD; } + public boolean isShowingStackOnLockscreen() { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return false; + return mShowingStackOnLockscreen; + } + + public void setShowingStackOnLockscreen(boolean showingStackOnLockscreen) { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; + mShowingStackOnLockscreen = showingStackOnLockscreen; + } + + public float getLockscreenStackFadeInProgress() { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f; + return mLockscreenStackFadeInProgress; + } + + public void setLockscreenStackFadeInProgress(float lockscreenStackFadeInProgress) { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; + mLockscreenStackFadeInProgress = lockscreenStackFadeInProgress; + } + public void setStatusBarState(int statusBarState) { if (mStatusBarState != StatusBarState.KEYGUARD) { mIsFlingRequiredAfterLockScreenSwipeUp = false; @@ -695,6 +719,7 @@ public class AmbientState implements Dumpable { * @return Whether we need to do a fling down after swiping up on lockscreen. */ public boolean isFlingingAfterSwipeUpOnLockscreen() { + SceneContainerFlag.assertInLegacyMode(); return mIsFlinging && mIsFlingRequiredAfterLockScreenSwipeUp; } 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 cd3516dadbad..b466bf02387f 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 @@ -104,6 +104,7 @@ import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; +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.ui.view.FooterView; @@ -567,6 +568,7 @@ public class NotificationStackScrollLayout private boolean mHasFilteredOutSeenNotifications; @Nullable private SplitShadeStateController mSplitShadeStateController = null; private boolean mIsSmallLandscapeLockscreenEnabled = false; + private boolean mSuppressHeightUpdates; /** Pass splitShadeStateController to view and update split shade */ public void passSplitShadeStateController(SplitShadeStateController splitShadeStateController) { @@ -686,7 +688,9 @@ public class NotificationStackScrollLayout protected void onFinishInflate() { super.onFinishInflate(); - inflateEmptyShadeView(); + if (!ModesEmptyShadeFix.isEnabled()) { + inflateEmptyShadeView(); + } if (!FooterViewRefactor.isEnabled()) { inflateFooterView(); } @@ -729,7 +733,9 @@ public class NotificationStackScrollLayout inflateFooterView(); updateFooter(); } - inflateEmptyShadeView(); + if (!ModesEmptyShadeFix.isEnabled()) { + inflateEmptyShadeView(); + } mSectionsManager.reinflateViews(); } @@ -1453,9 +1459,13 @@ public class NotificationStackScrollLayout * 2) Swiping up on lockscreen or flinging down after swipe up */ private boolean shouldSkipHeightUpdate() { - return mAmbientState.isOnKeyguard() - && (mAmbientState.isSwipingUp() - || mAmbientState.isFlingingAfterSwipeUpOnLockscreen()); + if (SceneContainerFlag.isEnabled()) { + return mSuppressHeightUpdates; + } else { + return mAmbientState.isOnKeyguard() + && (mAmbientState.isSwipingUp() + || mAmbientState.isFlingingAfterSwipeUpOnLockscreen()); + } } /** @@ -4835,6 +4845,8 @@ public class NotificationStackScrollLayout /** Trigger an update for the empty shade resources and visibility. */ public void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade, boolean hasFilteredOutSeenNotifications) { + ModesEmptyShadeFix.assertInLegacyMode(); + mEmptyShadeView.setVisible(visible, mIsExpanded && mAnimationsEnabled); if (areNotificationsHiddenInShade) { @@ -4853,6 +4865,8 @@ public class NotificationStackScrollLayout @StringRes int newTextRes, @StringRes int newFooterTextRes, @DrawableRes int newFooterIconRes) { + ModesEmptyShadeFix.assertInLegacyMode(); + int oldTextRes = mEmptyShadeView.getTextResource(); if (oldTextRes != newTextRes) { mEmptyShadeView.setText(newTextRes); @@ -4874,6 +4888,9 @@ public class NotificationStackScrollLayout public boolean isEmptyShadeViewVisible() { SceneContainerFlag.assertInLegacyMode(); + if (mEmptyShadeView == null) { + return false; + } return mEmptyShadeView.isVisible(); } @@ -5330,6 +5347,19 @@ public class NotificationStackScrollLayout updateDismissBehavior(); } + @Override + public void setShowingStackOnLockscreen(boolean showingStackOnLockscreen) { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; + mAmbientState.setShowingStackOnLockscreen(showingStackOnLockscreen); + } + + @Override + public void setAlphaForLockscreenFadeIn(float alphaForLockscreenFadeIn) { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; + mAmbientState.setLockscreenStackFadeInProgress(alphaForLockscreenFadeIn); + requestChildrenUpdate(); + } + void setUpcomingStatusBarState(int upcomingStatusBarState) { FooterViewRefactor.assertInLegacyMode(); mUpcomingStatusBarState = upcomingStatusBarState; @@ -5361,7 +5391,7 @@ public class NotificationStackScrollLayout public float getOpeningHeight() { SceneContainerFlag.assertInLegacyMode(); - if (mEmptyShadeView.getVisibility() == GONE) { + if (mEmptyShadeView == null || mEmptyShadeView.getVisibility() == GONE) { return getMinExpansionHeight(); } else { return FooterViewRefactor.isEnabled() ? getAppearEndPosition() @@ -5374,6 +5404,7 @@ public class NotificationStackScrollLayout } public void setPanelFlinging(boolean flinging) { + SceneContainerFlag.assertInLegacyMode(); mAmbientState.setFlinging(flinging); if (!flinging) { // re-calculate the stack height which was frozen while flinging @@ -5381,6 +5412,12 @@ public class NotificationStackScrollLayout } } + @Override + public void suppressHeightUpdates(boolean suppress) { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; + mSuppressHeightUpdates = suppress; + } + public void setHeadsUpGoingAwayAnimationsAllowed(boolean headsUpGoingAwayAnimationsAllowed) { mHeadsUpGoingAwayAnimationsAllowed = headsUpGoingAwayAnimationsAllowed; } @@ -5710,6 +5747,8 @@ public class NotificationStackScrollLayout } private void inflateEmptyShadeView() { + ModesEmptyShadeFix.assertInLegacyMode(); + EmptyShadeView oldView = mEmptyShadeView; EmptyShadeView view = (EmptyShadeView) LayoutInflater.from(mContext).inflate( R.layout.status_bar_no_notifications, this, false); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index dad6894a43ce..7b02d0cebfb3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -380,7 +380,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { new StatusBarStateController.StateListener() { @Override public void onStatePreChange(int oldState, int newState) { - if (oldState == StatusBarState.SHADE_LOCKED + if (!SceneContainerFlag.isEnabled() && oldState == StatusBarState.SHADE_LOCKED && newState == KEYGUARD) { mView.requestAnimateEverything(); } @@ -1439,6 +1439,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public void setPanelFlinging(boolean flinging) { + SceneContainerFlag.assertInLegacyMode(); mView.setPanelFlinging(flinging); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt index 06222fdb2761..3bc549543ef2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt @@ -29,7 +29,7 @@ import com.android.systemui.statusbar.StatusBarState.KEYGUARD import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.Compile import com.android.systemui.util.children @@ -74,7 +74,7 @@ constructor( /** Whether we allow keyguard to show less important notifications above the shelf. */ private val limitLockScreenToOneImportant - get() = NotificationMinimalismPrototype.isEnabled + get() = NotificationMinimalism.isEnabled /** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */ private var dividerHeight by notNull<Float>() @@ -406,7 +406,7 @@ constructor( fun updateResources() { maxKeyguardNotifications = infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count)) - maxNotificationsExcludesMedia = NotificationMinimalismPrototype.isEnabled + maxNotificationsExcludesMedia = NotificationMinimalism.isEnabled dividerHeight = max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat()) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 9c0fd0e844b4..e0b0ccd9e840 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -148,12 +148,18 @@ public class StackScrollAlgorithm { if (isHunGoingToShade) { // Keep 100% opacity for heads up notification going to shade. viewState.setAlpha(1f); - } else if (ambientState.isOnKeyguard()) { + } else if ((!SceneContainerFlag.isEnabled() && ambientState.isOnKeyguard()) + || ambientState.isShowingStackOnLockscreen()) { // Adjust alpha for wakeup to lockscreen. if (view.isHeadsUpState()) { // Pulsing HUN should be visible on AOD and stay visible during // AOD=>lockscreen transition viewState.setAlpha(1f - ambientState.getHideAmount()); + } else if (SceneContainerFlag.isEnabled()) { + // Take into account scene container-specific Lockscreen fade-in progress + float fadeAlpha = ambientState.getLockscreenStackFadeInProgress(); + float dozeAlpha = 1f - ambientState.getDozeAmount(); + viewState.setAlpha(Math.min(dozeAlpha, fadeAlpha)); } else { // Normal notifications are hidden on AOD and should fade in during // AOD=>lockscreen transition diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt index f6722a4ccff0..c0f1a5619140 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt @@ -31,6 +31,9 @@ class NotificationPlaceholderRepository @Inject constructor() { /** The alpha of the shade in order to show brightness. */ val alphaForBrightnessMirror = MutableStateFlow(1f) + /** The alpha of the Notification Stack for lockscreen fade-in */ + val alphaForLockscreenFadeIn = MutableStateFlow(0f) + /** * The bounds of the notification shade scrim / container in the current scene. * diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt index 756cd87970a4..32e092bcdf4d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt @@ -56,10 +56,9 @@ constructor( /** The rounding of the notification stack. */ val shadeScrimRounding: Flow<ShadeScrimRounding> = - combine( - shadeInteractor.shadeMode, - isExpandingFromHeadsUp, - ) { shadeMode, isExpandingFromHeadsUp -> + combine(shadeInteractor.shadeMode, isExpandingFromHeadsUp) { + shadeMode, + isExpandingFromHeadsUp -> ShadeScrimRounding( isTopRounded = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp), isBottomRounded = shadeMode != ShadeMode.Single, @@ -71,6 +70,10 @@ constructor( val alphaForBrightnessMirror: StateFlow<Float> = placeholderRepository.alphaForBrightnessMirror.asStateFlow() + /** The alpha of the Notification Stack for lockscreen fade-in */ + val alphaForLockscreenFadeIn: StateFlow<Float> = + placeholderRepository.alphaForLockscreenFadeIn.asStateFlow() + /** The height of the keyguard's available space bounds */ val constrainedAvailableSpace: StateFlow<Int> = placeholderRepository.constrainedAvailableSpace.asStateFlow() @@ -99,7 +102,7 @@ constructor( val shouldCloseGuts: Flow<Boolean> = combine( sceneInteractor.isSceneContainerUserInputOngoing, - viewHeightRepository.isCurrentGestureInGuts + viewHeightRepository.isCurrentGestureInGuts, ) { isUserInputOngoing, isCurrentGestureInGuts -> isUserInputOngoing && !isCurrentGestureInGuts } @@ -109,6 +112,11 @@ constructor( placeholderRepository.alphaForBrightnessMirror.value = alpha } + /** Sets the alpha to apply to the NSSL for fade-in on lockscreen */ + fun setAlphaForLockscreenFadeIn(alpha: Float) { + placeholderRepository.alphaForLockscreenFadeIn.value = alpha + } + /** Sets the position of the notification stack in the current scene. */ fun setShadeScrimBounds(bounds: ShadeScrimBounds?) { check(bounds == null || bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt index 41c02934efa6..dbe81c10e2fd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt @@ -89,6 +89,12 @@ interface NotificationScrollView { /** sets the current QS expand fraction */ fun setQsExpandFraction(expandFraction: Float) + /** set whether we are idle on the lockscreen scene */ + fun setShowingStackOnLockscreen(showingStackOnLockscreen: Boolean) + + /** set the alpha from 0-1f of stack fade-in on lockscreen */ + fun setAlphaForLockscreenFadeIn(alphaForLockscreenFadeIn: Float) + /** Sets whether the view is displayed in doze mode. */ fun setDozing(dozing: Boolean) @@ -118,4 +124,7 @@ interface NotificationScrollView { /** @see addHeadsUpHeightChangedListener */ fun removeHeadsUpHeightChangedListener(runnable: Runnable) + + /** Sets whether updates to the stack are are suppressed. */ + fun suppressHeightUpdates(suppress: Boolean) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt index dc9615c25ada..ebae235f88d6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewbinder import android.view.LayoutInflater +import android.view.View import androidx.lifecycle.lifecycleScope import com.android.app.tracing.TraceUtils.traceAsync import com.android.internal.logging.MetricsLogger @@ -25,6 +26,7 @@ import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.lifecycle.repeatWhenAttachedToWindow import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -32,6 +34,10 @@ import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.notification.NotificationActivityStarter import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController import com.android.systemui.statusbar.notification.dagger.SilentHeader +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.emptyshade.ui.viewbinder.EmptyShadeViewBinder +import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.footer.ui.view.FooterView import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder @@ -49,6 +55,7 @@ import com.android.systemui.statusbar.notification.ui.viewbinder.HeadsUpNotifica import com.android.systemui.util.kotlin.awaitCancellationThenDispose import com.android.systemui.util.kotlin.getOrNull import com.android.systemui.util.ui.isAnimating +import com.android.systemui.util.ui.stopAnimating import com.android.systemui.util.ui.value import java.util.Optional import javax.inject.Inject @@ -84,7 +91,7 @@ constructor( fun bindWhileAttached( view: NotificationStackScrollLayout, - viewController: NotificationStackScrollLayoutController + viewController: NotificationStackScrollLayoutController, ) { val shelf = LayoutInflater.from(view.context) @@ -103,7 +110,13 @@ constructor( val hasNonClearableSilentNotifications: StateFlow<Boolean> = viewModel.hasNonClearableSilentNotifications.stateIn(this) launch { reinflateAndBindFooter(view, hasNonClearableSilentNotifications) } - launch { bindEmptyShade(view) } + launch { + if (ModesEmptyShadeFix.isEnabled) { + reinflateAndBindEmptyShade(view) + } else { + bindEmptyShadeLegacy(viewModel.emptyShadeViewFactory.create(), view) + } + } launch { bindSilentHeaderClickListener(view, hasNonClearableSilentNotifications) } @@ -121,17 +134,12 @@ constructor( } private suspend fun bindShelf(shelf: NotificationShelf) { - NotificationShelfViewBinder.bind( - shelf, - viewModel.shelf, - falsingManager, - nicBinder, - ) + NotificationShelfViewBinder.bind(shelf, viewModel.shelf, falsingManager, nicBinder) } private suspend fun reinflateAndBindFooter( parentView: NotificationStackScrollLayout, - hasNonClearableSilentNotifications: StateFlow<Boolean> + hasNonClearableSilentNotifications: StateFlow<Boolean>, ) { viewModel.footer.getOrNull()?.let { footerViewModel -> // The footer needs to be re-inflated every time the theme or the font size changes. @@ -149,7 +157,7 @@ constructor( footerView, footerViewModel, parentView, - hasNonClearableSilentNotifications + hasNonClearableSilentNotifications, ) } } @@ -163,13 +171,13 @@ constructor( footerView: FooterView, footerViewModel: FooterViewModel, parentView: NotificationStackScrollLayout, - hasNonClearableSilentNotifications: StateFlow<Boolean> + hasNonClearableSilentNotifications: StateFlow<Boolean>, ): Unit = coroutineScope { val disposableHandle = FooterViewBinder.bindWhileAttached( footerView, footerViewModel, - clearAllNotifications = { + { clearAllNotifications( parentView, // Hide the silent section header (if present) if there will be @@ -177,16 +185,9 @@ constructor( hideSilentSection = !hasNonClearableSilentNotifications.value, ) }, - launchNotificationSettings = { view -> - notificationActivityStarter - .get() - .startHistoryIntent(view, /* showHistory= */ false) - }, - launchNotificationHistory = { view -> - notificationActivityStarter - .get() - .startHistoryIntent(view, /* showHistory= */ true) - }, + launchNotificationSettings, + launchNotificationHistory, + notificationActivityStarter.get(), ) if (SceneContainerFlag.isEnabled) { launch { @@ -194,7 +195,9 @@ constructor( footerView.setVisible( /* visible = */ animatedVisibility.value, /* animate = */ animatedVisibility.isAnimating, - ) + ) { + animatedVisibility.stopAnimating() + } } } } else { @@ -211,20 +214,70 @@ constructor( disposableHandle.awaitCancellationThenDispose() } - private suspend fun bindEmptyShade(parentView: NotificationStackScrollLayout) { + private val launchNotificationSettings: (View) -> Unit = { view: View -> + notificationActivityStarter.get().startHistoryIntent(view, /* showHistory= */ false) + } + + private val launchNotificationHistory: (View) -> Unit = { view -> + notificationActivityStarter.get().startHistoryIntent(view, /* showHistory= */ true) + } + + private suspend fun reinflateAndBindEmptyShade(parentView: NotificationStackScrollLayout) { + ModesEmptyShadeFix.assertInNewMode() + // The empty shade needs to be re-inflated every time the theme or the font size + // changes. + configuration + .inflateLayout<EmptyShadeView>( + R.layout.status_bar_no_notifications, + parentView, + attachToRoot = false, + ) + .flowOn(backgroundDispatcher) + .collectLatest { emptyShadeView: EmptyShadeView -> + traceAsync("bind EmptyShadeView") { + parentView.setEmptyShadeView(emptyShadeView) + bindEmptyShade(emptyShadeView, viewModel.emptyShadeViewFactory.create()) + } + } + } + + private suspend fun bindEmptyShadeLegacy( + emptyShadeViewModel: EmptyShadeViewModel, + parentView: NotificationStackScrollLayout, + ) { + ModesEmptyShadeFix.assertInLegacyMode() combine( viewModel.shouldShowEmptyShadeView, - viewModel.areNotificationsHiddenInShade, - viewModel.hasFilteredOutSeenNotifications, - ::Triple + emptyShadeViewModel.areNotificationsHiddenInShade, + emptyShadeViewModel.hasFilteredOutSeenNotifications, + ::Triple, ) .collect { (shouldShow, areNotifsHidden, hasFilteredNotifs) -> - parentView.updateEmptyShadeView( - shouldShow, - areNotifsHidden, - hasFilteredNotifs, + parentView.updateEmptyShadeView(shouldShow, areNotifsHidden, hasFilteredNotifs) + } + } + + private suspend fun bindEmptyShade( + emptyShadeView: EmptyShadeView, + emptyShadeViewModel: EmptyShadeViewModel, + ): Unit = coroutineScope { + ModesEmptyShadeFix.assertInNewMode() + launch { + emptyShadeView.repeatWhenAttachedToWindow { + EmptyShadeViewBinder.bind( + emptyShadeView, + emptyShadeViewModel, + notificationActivityStarter.get(), ) } + } + launch { + viewModel.shouldShowEmptyShadeViewAnimated.collect { shouldShow -> + emptyShadeView.setVisible(shouldShow.value, shouldShow.isAnimating) { + shouldShow.stopAnimating() + } + } + } } private suspend fun bindSilentHeaderClickListener( @@ -261,7 +314,7 @@ constructor( private fun clearSilentNotifications( view: NotificationStackScrollLayout, closeShade: Boolean, - hideSilentSection: Boolean + hideSilentSection: Boolean, ) { view.clearSilentNotifications(closeShade, hideSilentSection) } @@ -270,11 +323,7 @@ constructor( if (NotificationsLiveDataStoreRefactor.isEnabled) { viewModel.logger.getOrNull()?.let { viewModel -> loggerOptional.getOrNull()?.let { logger -> - NotificationStatsLoggerBinder.bindLogger( - view, - logger, - viewModel, - ) + NotificationStatsLoggerBinder.bindLogger(view, logger, viewModel) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt index 2e37dead8787..87d70ba12012 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt @@ -88,6 +88,14 @@ constructor( viewModel.expandFraction.collect { view.setExpandFraction(it.coerceIn(0f, 1f)) } } launch { viewModel.qsExpandFraction.collect { view.setQsExpandFraction(it) } } + launch { + viewModel.isShowingStackOnLockscreen.collect { + view.setShowingStackOnLockscreen(it) + } + } + launch { + viewModel.alphaForLockscreenFadeIn.collect { view.setAlphaForLockscreenFadeIn(it) } + } launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } } launch { viewModel.isDozing.collect { isDozing -> view.setDozing(isDozing) } } launch { @@ -103,6 +111,7 @@ constructor( launch { viewModel.shouldCloseGuts.filter { it }.collect { view.closeGutsOnSceneTouch() } } + launch { viewModel.suppressHeightUpdates.collect { view.suppressHeightUpdates(it) } } launchAndDispose { view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt index 4e2a46d78a5d..935e2a37b13c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt @@ -23,14 +23,14 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor -import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor +import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix +import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackInteractor import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor -import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor import com.android.systemui.util.kotlin.FlowDumperImpl import com.android.systemui.util.kotlin.combine import com.android.systemui.util.kotlin.sample @@ -48,22 +48,24 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart -/** ViewModel for the list of notifications. */ +/** + * ViewModel for the list of notifications, including child elements like the Clear all/Manage + * button at the bottom (the footer) and the "No notifications" text (the empty shade). + */ class NotificationListViewModel @Inject constructor( val shelf: NotificationShelfViewModel, val hideListViewModel: HideListViewModel, val footer: Optional<FooterViewModel>, + val emptyShadeViewFactory: EmptyShadeViewModel.Factory, val logger: Optional<NotificationLoggerViewModel>, activeNotificationsInteractor: ActiveNotificationsInteractor, notificationStackInteractor: NotificationStackInteractor, private val headsUpNotificationInteractor: HeadsUpNotificationInteractor, remoteInputInteractor: RemoteInputInteractor, - seenNotificationsInteractor: SeenNotificationsInteractor, shadeInteractor: ShadeInteractor, userSetupInteractor: UserSetupInteractor, - zenModeInteractor: ZenModeInteractor, @Background bgDispatcher: CoroutineDispatcher, dumpManager: DumpManager, ) : FlowDumperImpl(dumpManager) { @@ -90,6 +92,7 @@ constructor( } val shouldShowEmptyShadeView: Flow<Boolean> by lazy { + ModesEmptyShadeFix.assertInLegacyMode() if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { flowOf(false) } else { @@ -114,6 +117,45 @@ constructor( } } + val shouldShowEmptyShadeViewAnimated: Flow<AnimatedValue<Boolean>> by lazy { + if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) { + flowOf(AnimatedValue.NotAnimating(false)) + } else { + combine( + activeNotificationsInteractor.areAnyNotificationsPresent, + shadeInteractor.isQsFullscreen, + notificationStackInteractor.isShowingOnLockscreen, + ) { hasNotifications, isQsFullScreen, isShowingOnLockscreen -> + when { + hasNotifications -> false + isQsFullScreen -> false + // Do not show the empty shade if the lockscreen is visible (including AOD + // b/228790482 and bouncer b/267060171), except if the shade is opened on + // top. + isShowingOnLockscreen -> false + else -> true + } + } + .distinctUntilChanged() + .sample( + // TODO(b/322167853): This check is currently duplicated in FooterViewModel + // but instead it should be a field in ShadeAnimationInteractor. + combine( + shadeInteractor.isShadeFullyExpanded, + shadeInteractor.isShadeTouchable, + ::Pair, + ) + .onStart { emit(Pair(false, false)) } + ) { visible, (isShadeFullyExpanded, animationsEnabled) -> + val shouldAnimate = isShadeFullyExpanded && animationsEnabled + AnimatableEvent(visible, shouldAnimate) + } + .toAnimatedValueFlow() + .dumpWhileCollecting("shouldShowEmptyShadeViewAnimated") + .flowOn(bgDispatcher) + } + } + /** * Whether the footer should not be visible for the user, even if it's present in the list (as * per [shouldIncludeFooterView] below). @@ -154,7 +196,7 @@ constructor( userSetupInteractor.isUserSetUp, notificationStackInteractor.isShowingOnLockscreen, shadeInteractor.isQsFullscreen, - remoteInputInteractor.isRemoteInputActive + remoteInputInteractor.isRemoteInputActive, ) { hasNotifications, isUserSetUp, @@ -193,7 +235,7 @@ constructor( combine( shadeInteractor.isShadeFullyExpanded, shadeInteractor.isShadeTouchable, - ::Pair + ::Pair, ) .onStart { emit(Pair(false, false)) } ) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) -> @@ -263,7 +305,7 @@ constructor( combine( shadeInteractor.isShadeFullyExpanded, shadeInteractor.isShadeTouchable, - ::Pair + ::Pair, ) .onStart { emit(Pair(false, false)) } ) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) -> @@ -283,29 +325,7 @@ constructor( enum class VisibilityChange(val visible: Boolean, val canAnimate: Boolean) { DISAPPEAR_WITHOUT_ANIMATION(visible = false, canAnimate = false), DISAPPEAR_WITH_ANIMATION(visible = false, canAnimate = true), - APPEAR_WITH_ANIMATION(visible = true, canAnimate = true) - } - - // TODO(b/308591475): This should be tracked separately by the empty shade. - val areNotificationsHiddenInShade: Flow<Boolean> by lazy { - if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { - flowOf(false) - } else { - zenModeInteractor.areNotificationsHiddenInShade.dumpWhileCollecting( - "areNotificationsHiddenInShade" - ) - } - } - - // TODO(b/308591475): This should be tracked separately by the empty shade. - val hasFilteredOutSeenNotifications: Flow<Boolean> by lazy { - if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { - flowOf(false) - } else { - seenNotificationsInteractor.hasFilteredOutSeenNotifications.dumpWhileCollecting( - "hasFilteredOutSeenNotifications" - ) - } + APPEAR_WITH_ANIMATION(visible = true, canAnimate = true), } val hasClearableAlertingNotifications: Flow<Boolean> by lazy { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLockscreenScrimViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLockscreenScrimViewModel.kt new file mode 100644 index 000000000000..84aa997cf0e1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLockscreenScrimViewModel.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.stack.ui.viewmodel + +import com.android.systemui.dump.DumpManager +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor +import com.android.systemui.util.kotlin.ActivatableFlowDumper +import com.android.systemui.util.kotlin.ActivatableFlowDumperImpl +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +class NotificationLockscreenScrimViewModel +@AssistedInject +constructor( + dumpManager: DumpManager, + shadeInteractor: ShadeInteractor, + private val stackAppearanceInteractor: NotificationStackAppearanceInteractor, +) : + ActivatableFlowDumper by ActivatableFlowDumperImpl(dumpManager, "NotificationScrollViewModel"), + ExclusiveActivatable() { + + val shadeMode = shadeInteractor.shadeMode + + /** Sets the alpha to apply to the NSSL for fade-in on lockscreen */ + fun setAlphaForLockscreenFadeIn(alpha: Float) { + stackAppearanceInteractor.setAlphaForLockscreenFadeIn(alpha) + } + + override suspend fun onActivated(): Nothing { + activateFlowDumper() + } + + @AssistedFactory + interface Factory { + fun create(): NotificationLockscreenScrimViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt index 5b2e02d446cf..c9eaec7c5b85 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.compose.animation.scene.ContentKey +import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.ObservableTransitionState.Idle import com.android.compose.animation.scene.ObservableTransitionState.Transition import com.android.compose.animation.scene.ObservableTransitionState.Transition.ChangeScene @@ -48,6 +49,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */ @@ -82,8 +84,13 @@ constructor( private fun fullyExpandedDuringSceneChange(change: ChangeScene): Boolean { // The lockscreen stack is visible during all transitions away from the lockscreen, so keep // the stack expanded until those transitions finish. - return (expandedInScene(change.fromScene) && expandedInScene(change.toScene)) || - change.isBetween({ it == Scenes.Lockscreen }, { true }) + return if (change.isFrom({ it == Scenes.Lockscreen }, to = { true })) { + true + } else if (change.isFrom({ it == Scenes.Shade }, to = { it == Scenes.Lockscreen })) { + false + } else { + (expandedInScene(change.fromScene) && expandedInScene(change.toScene)) + } } private fun expandFractionDuringSceneChange( @@ -93,7 +100,10 @@ constructor( ): Float { return if (fullyExpandedDuringSceneChange(change)) { 1f - } else if (change.isBetween({ it == Scenes.Gone }, { it == Scenes.Shade })) { + } else if ( + change.isBetween({ it == Scenes.Gone }, { it == Scenes.Shade }) || + change.isFrom({ it == Scenes.Shade }, to = { it == Scenes.Lockscreen }) + ) { shadeExpansion } else if (change.isBetween({ it == Scenes.Gone }, { it == Scenes.QuickSettings })) { // during QS expansion, increase fraction at same rate as scrim alpha, @@ -121,6 +131,14 @@ constructor( } } + /** Are notification stack height updates suppressed? */ + val suppressHeightUpdates: Flow<Boolean> = + sceneInteractor.transitionState.map { transition: ObservableTransitionState -> + transition is Transition && + transition.fromContent == Scenes.Lockscreen && + (transition.toContent == Scenes.Bouncer || transition.toContent == Scenes.Gone) + } + /** * The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning * from Gone to Shade scenes, and remain at 1 when in Lockscreen or Shade scenes and while @@ -178,6 +196,18 @@ constructor( .distinctUntilChanged() .dumpWhileCollecting("shouldResetStackTop") + /** Whether the Notification Stack is visibly on the lockscreen scene. */ + val isShowingStackOnLockscreen: Flow<Boolean> = + sceneInteractor.transitionState + .mapNotNull { state -> + state.isIdle(Scenes.Lockscreen) || + state.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade) + } + .distinctUntilChanged() + + /** The alpha of the Notification Stack for lockscreen fade-in */ + val alphaForLockscreenFadeIn = stackAppearanceInteractor.alphaForLockscreenFadeIn + private operator fun SceneKey.contains(scene: SceneKey) = sceneInteractor.isSceneInFamily(scene, this) @@ -298,3 +328,6 @@ constructor( private fun ChangeScene.isBetween(a: (SceneKey) -> Boolean, b: (SceneKey) -> Boolean): Boolean = (a(fromScene) && b(toScene)) || (b(fromScene) && a(toScene)) + +private fun ChangeScene.isFrom(from: (SceneKey) -> Boolean, to: (SceneKey) -> Boolean): Boolean = + from(fromScene) && to(toScene) 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 57be62932e59..0ad22e0b6dc9 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 @@ -61,6 +61,7 @@ import com.android.systemui.keyguard.ui.viewmodel.LockscreenToPrimaryBouncerTran import com.android.systemui.keyguard.ui.viewmodel.OccludedToAodTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludedToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.OffToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor @@ -132,6 +133,7 @@ constructor( private val occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel, private val occludedToGoneTransitionViewModel: OccludedToGoneTransitionViewModel, private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel, + private val offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel, private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel, private val primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel, @@ -444,6 +446,7 @@ constructor( occludedToAodTransitionViewModel.lockscreenAlpha, occludedToGoneTransitionViewModel.notificationAlpha(viewState), occludedToLockscreenTransitionViewModel.lockscreenAlpha, + offToLockscreenTransitionViewModel.lockscreenAlpha, primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState), glanceableHubToLockscreenTransitionViewModel.keyguardAlpha, lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 0a6e7f59e24e..93db2db918b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -20,6 +20,7 @@ import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED import static android.service.notification.NotificationListenerService.REASON_CLICK; import static com.android.systemui.statusbar.phone.CentralSurfaces.getActivityOptions; +import static com.android.systemui.util.kotlin.NullabilityKt.expectNotNull; import android.app.ActivityManager; import android.app.ActivityOptions; @@ -43,6 +44,7 @@ import android.text.TextUtils; import android.util.EventLog; import android.view.View; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; @@ -74,6 +76,7 @@ import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorCon import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; +import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowDragController; import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback; @@ -110,6 +113,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit boolean showOverTheLockScreen); } + private final static String TAG = "StatusBarNotificationActivityStarter"; + private final Context mContext; private final int mDisplayId; @@ -227,6 +232,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit */ @Override public void onNotificationBubbleIconClicked(NotificationEntry entry) { + expectNotNull(TAG, "entry", entry); Runnable action = () -> { mBubblesManagerOptional.ifPresent(bubblesManager -> bubblesManager.onUserChangedBubble(entry, !entry.isBubble())); @@ -249,10 +255,12 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit * Called when a notification is clicked. * * @param entry notification that was clicked - * @param row row for that notification + * @param row row for that notification */ @Override public void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row) { + expectNotNull(TAG, "entry", entry); + expectNotNull(TAG, "row", row); mLogger.logStartingActivityFromClick(entry, row.isHeadsUpState(), mKeyguardStateController.isVisible(), mNotificationShadeWindowController.getPanelExpanded()); @@ -435,6 +443,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit */ @Override public void onDragSuccess(NotificationEntry entry) { + expectNotNull(TAG, "entry", entry); // this method is not responsible for intent sending. // will focus follow operation only after drag-and-drop that notification. final NotificationVisibility nv = mVisibilityProvider.obtain(entry, true); @@ -527,6 +536,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit @Override public void startNotificationGutsIntent(final Intent intent, final int appUid, ExpandableNotificationRow row) { + expectNotNull(TAG, "intent", intent); + expectNotNull(TAG, "row", row); boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override @@ -547,8 +558,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit (adapter) -> TaskStackBuilder.create(mContext) .addNextIntentWithParentStack(intent) .startActivities(getActivityOptions( - mDisplayId, - adapter), + mDisplayId, + adapter), new UserHandle(UserHandle.getUserId(appUid)))); }); return true; @@ -565,6 +576,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit @Override public void startHistoryIntent(View view, boolean showHistory) { + ModesEmptyShadeFix.assertInLegacyMode(); boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override @@ -585,14 +597,14 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit ); ActivityTransitionAnimator.Controller animationController = viewController == null ? null - : new StatusBarTransitionAnimatorController( - viewController, - mShadeAnimationInteractor, - mShadeController, - mNotificationShadeWindowController, - mCommandQueue, - mDisplayId, - true /* isActivityIntent */); + : new StatusBarTransitionAnimatorController( + viewController, + mShadeAnimationInteractor, + mShadeController, + mNotificationShadeWindowController, + mCommandQueue, + mDisplayId, + true /* isActivityIntent */); mActivityTransitionAnimator.startIntentWithAnimation( animationController, animate, intent.getPackage(), @@ -612,6 +624,51 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit false /* afterKeyguardGone */); } + @Override + public void startSettingsIntent(@NonNull View view, @NonNull SettingsIntent intentInfo) { + boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); + ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { + @Override + public boolean onDismiss() { + AsyncTask.execute(() -> { + TaskStackBuilder tsb = TaskStackBuilder.create(mContext); + for (Intent intent : intentInfo.getBackStack()) { + tsb.addNextIntent(intent); + } + tsb.addNextIntent(intentInfo.getTargetIntent()); + + ActivityTransitionAnimator.Controller viewController = + ActivityTransitionAnimator.Controller.fromView(view, + intentInfo.getCujType()); + ActivityTransitionAnimator.Controller animationController = + viewController == null ? null + : new StatusBarTransitionAnimatorController( + viewController, + mShadeAnimationInteractor, + mShadeController, + mNotificationShadeWindowController, + mCommandQueue, + mDisplayId, + true /* isActivityIntent */); + + mActivityTransitionAnimator.startIntentWithAnimation( + animationController, animate, intentInfo.getTargetIntent().getPackage(), + (adapter) -> tsb.startActivities( + getActivityOptions(mDisplayId, adapter), + mUserTracker.getUserHandle())); + }); + return true; + } + + @Override + public boolean willRunAnimationOnKeyguard() { + return animate; + } + }; + mActivityStarter.dismissKeyguardThenExecute(onDismissAction, null, + false /* afterKeyguardGone */); + } + private void removeHunAfterClick(ExpandableNotificationRow row) { String key = row.getEntry().getSbn().getKey(); if (mHeadsUpManager != null && mHeadsUpManager.isHeadsUpEntry(key)) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileIconModel.kt new file mode 100644 index 000000000000..f8958e0d002f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/InternetTileIconModel.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.pipeline.shared.ui.model + +import com.android.systemui.common.shared.model.Icon + +sealed interface InternetTileIconModel { + data class ResourceId(val resId: Int) : InternetTileIconModel + + data class Cellular(val level: Int) : InternetTileIconModel + + data class Satellite(val resourceIcon: Icon.Resource) : InternetTileIconModel +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt index ba45942177a2..daba1099c49d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt @@ -20,6 +20,7 @@ import android.content.Context import android.provider.Settings import android.provider.Settings.Secure.ZEN_DURATION_FOREVER import android.provider.Settings.Secure.ZEN_DURATION_PROMPT +import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST import android.util.Log import androidx.concurrent.futures.await import com.android.settingslib.notification.data.repository.ZenModeRepository @@ -29,6 +30,7 @@ import com.android.settingslib.notification.modes.ZenMode import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.modes.shared.ModesUi import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository +import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes @@ -39,6 +41,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow 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 @@ -54,8 +57,8 @@ constructor( private val notificationSettingsRepository: NotificationSettingsRepository, @Background private val bgDispatcher: CoroutineDispatcher, private val iconLoader: ZenIconLoader, - private val deviceProvisioningRepository: DeviceProvisioningRepository, - private val userSetupRepository: UserSetupRepository, + deviceProvisioningRepository: DeviceProvisioningRepository, + userSetupRepository: UserSetupRepository, ) { val isZenAvailable: Flow<Boolean> = combine( @@ -126,6 +129,25 @@ constructor( val mainActiveMode: Flow<ZenModeInfo?> = activeModes.map { a -> a.mainMode }.distinctUntilChanged() + val modesHidingNotifications: Flow<List<ZenMode>> by lazy { + if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode() || !ModesUi.isEnabled) { + flowOf(listOf()) + } else { + modes + .map { modes -> + modes.filter { mode -> + mode.isActive && + !mode.policy.isVisualEffectAllowed( + /* effect = */ VISUAL_EFFECT_NOTIFICATION_LIST, + /* defaultVal = */ true, + ) + } + } + .flowOn(bgDispatcher) + .distinctUntilChanged() + } + } + suspend fun getModeIcon(mode: ZenMode): ZenIcon { return iconLoader.getIcon(context, mode).await() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt index e1dcc524c486..a1d5cbea62f9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt @@ -19,17 +19,21 @@ package com.android.systemui.statusbar.policy.ui.dialog import android.content.Intent import android.provider.Settings import android.util.Log +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.paneTitle import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.android.compose.PlatformButton import com.android.compose.PlatformOutlinedButton +import com.android.compose.theme.PlatformTheme import com.android.internal.annotations.VisibleForTesting import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.animation.DialogCuj @@ -74,7 +78,7 @@ constructor( currentDialog?.dismiss() } - currentDialog = sysuiDialogFactory.create() { ModesDialogContent(it) } + currentDialog = sysuiDialogFactory.create { ModesDialogContent(it) } currentDialog ?.lifecycle ?.addObserver( @@ -91,28 +95,40 @@ constructor( @Composable private fun ModesDialogContent(dialog: SystemUIDialog) { - AlertDialogContent( - modifier = Modifier.semantics { - testTagsAsResourceId = true - }, - title = { - Text( - modifier = Modifier.testTag("modes_title"), - text = stringResource(R.string.zen_modes_dialog_title) - ) - }, - content = { ModeTileGrid(viewModel.get()) }, - neutralButton = { - PlatformOutlinedButton(onClick = { openSettings(dialog) }) { - Text(stringResource(R.string.zen_modes_dialog_settings)) - } - }, - positiveButton = { - PlatformButton(onClick = { dialog.dismiss() }) { - Text(stringResource(R.string.zen_modes_dialog_done)) - } - }, - ) + // TODO(b/369376884): The composable does correctly update when the theme changes + // while the dialog is open, but the background (which we don't control here) + // doesn't, which causes us to show things like white text on a white background. + // as a workaround, we remember the original theme and keep it on recomposition. + val isCurrentlyInDarkTheme = isSystemInDarkTheme() + val cachedDarkTheme = remember { isCurrentlyInDarkTheme } + PlatformTheme(isDarkTheme = cachedDarkTheme) { + AlertDialogContent( + modifier = + Modifier.semantics { + testTagsAsResourceId = true + paneTitle = dialog.context.getString( + R.string.accessibility_desc_quick_settings + ) + }, + title = { + Text( + modifier = Modifier.testTag("modes_title"), + text = stringResource(R.string.zen_modes_dialog_title), + ) + }, + content = { ModeTileGrid(viewModel.get()) }, + neutralButton = { + PlatformOutlinedButton(onClick = { openSettings(dialog) }) { + Text(stringResource(R.string.zen_modes_dialog_settings)) + } + }, + positiveButton = { + PlatformButton(onClick = { dialog.dismiss() }) { + Text(stringResource(R.string.zen_modes_dialog_done)) + } + }, + ) + } } @VisibleForTesting @@ -128,8 +144,8 @@ constructor( } activityStarter.startActivity( ZEN_MODE_SETTINGS_INTENT, - true /* dismissShade */, - animationController + /* dismissShade= */ true, + animationController, ) } @@ -163,7 +179,7 @@ constructor( Log.w( TAG, "Cannot launch from dialog, the dialog is not present. " + - "Will launch activity without animating." + "Will launch activity without animating.", ) } @@ -172,11 +188,7 @@ constructor( if (animationController == null) { currentDialog?.dismiss() } - activityStarter.startActivity( - intent, - true, /* dismissShade */ - animationController, - ) + activityStarter.startActivity(intent, /* dismissShade= */ true, animationController) } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt index d03b2e717398..e1f7bd59005c 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt @@ -29,6 +29,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.theme.PlatformTheme import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger.TutorialContext +import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialMetricsLogger import com.android.systemui.touchpad.tutorial.ui.composable.BackGestureTutorialScreen import com.android.systemui.touchpad.tutorial.ui.composable.HomeGestureTutorialScreen import com.android.systemui.touchpad.tutorial.ui.composable.RecentAppsGestureTutorialScreen @@ -45,6 +46,7 @@ class TouchpadTutorialActivity constructor( private val viewModelFactory: TouchpadTutorialViewModel.Factory, private val logger: InputDeviceTutorialLogger, + private val metricsLogger: KeyboardTouchpadTutorialMetricsLogger, ) : ComponentActivity() { private val vm by viewModels<TouchpadTutorialViewModel>(factoryProducer = { viewModelFactory }) @@ -57,6 +59,7 @@ constructor( } // required to handle 3+ fingers on touchpad window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) + metricsLogger.logPeripheralTutorialLaunchedFromSettings() logger.logOpenTutorial(TutorialContext.TOUCHPAD_TUTORIAL) } @@ -85,7 +88,7 @@ fun TouchpadTutorialScreen(vm: TouchpadTutorialViewModel, closeTutorial: () -> U onBackTutorialClicked = { vm.goTo(BACK_GESTURE) }, onHomeTutorialClicked = { vm.goTo(HOME_GESTURE) }, onRecentAppsTutorialClicked = { vm.goTo(RECENT_APPS_GESTURE) }, - onDoneButtonClicked = closeTutorial + onDoneButtonClicked = closeTutorial, ) BACK_GESTURE -> BackGestureTutorialScreen( diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/nullability.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Nullability.kt index 298dacde8128..1c760bedff58 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/nullability.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Nullability.kt @@ -16,6 +16,7 @@ package com.android.systemui.util.kotlin +import android.util.Log import java.util.Optional /** @@ -28,3 +29,14 @@ inline fun <T : Any, R> transform(value: T?, block: (T) -> R): R? = value?.let(b */ @Suppress("NOTHING_TO_INLINE") inline fun <T> Optional<T>.getOrNull(): T? = orElse(null) + +/** + * Utility method to check if a value that is technically nullable is actually null. If it is null, + * this will crash development builds (but just log on production/droidfood builds). It can be used + * as a first step to verify if a nullable value can be made non-nullable instead. + */ +fun <T> expectNotNull(logTag: String, name: String, nullable: T?) { + if (nullable == null) { + Log.wtf(logTag, "Expected value of $name to not be null.") + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java index 24f3a29e64ee..24f3a29e64ee 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java index aa8c6b7a8a5f..e160ff17a6ed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.content.res.Configuration; @@ -643,6 +644,46 @@ public class TouchMonitorTest extends SysuiTestCase { environment.verifyInputSessionDispose(); } + @Test + public void testSessionPopAfterDestroy() { + final TouchHandler touchHandler = createTouchHandler(); + + final Environment environment = new Environment(Stream.of(touchHandler) + .collect(Collectors.toCollection(HashSet::new)), mKosmos); + + final InputEvent initialEvent = Mockito.mock(InputEvent.class); + environment.publishInputEvent(initialEvent); + + // Ensure session started + final InputChannelCompat.InputEventListener eventListener = + registerInputEventListener(touchHandler); + + // First event will be missed since we register after the execution loop, + final InputEvent event = Mockito.mock(InputEvent.class); + environment.publishInputEvent(event); + verify(eventListener).onInputEvent(eq(event)); + + final ArgumentCaptor<TouchHandler.TouchSession> touchSessionArgumentCaptor = + ArgumentCaptor.forClass(TouchHandler.TouchSession.class); + + verify(touchHandler).onSessionStart(touchSessionArgumentCaptor.capture()); + + environment.updateLifecycle(Lifecycle.State.DESTROYED); + + // Check to make sure the input session is now disposed. + environment.verifyInputSessionDispose(); + + clearInvocations(environment.mInputFactory); + + // Pop the session + touchSessionArgumentCaptor.getValue().pop(); + + environment.executeAll(); + + // Ensure no input sessions were created due to the session reset. + verifyNoMoreInteractions(environment.mInputFactory); + } + @Test public void testPilfering() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt index 72163e4d7710..72163e4d7710 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index e2a6a5508992..e2a6a5508992 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index b0810a9edf6b..6608542980b0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -100,6 +100,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.flags.SystemPropertiesHelper; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionBootInteractor; import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.log.SessionTracker; import com.android.systemui.navigationbar.NavigationModeController; @@ -199,6 +200,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private @Mock ShadeWindowLogger mShadeWindowLogger; private @Mock SelectedUserInteractor mSelectedUserInteractor; private @Mock KeyguardInteractor mKeyguardInteractor; + private @Mock KeyguardTransitionBootInteractor mKeyguardTransitionBootInteractor; private @Captor ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateControllerCallback; private @Captor ArgumentCaptor<KeyguardUpdateMonitorCallback> @@ -1294,6 +1296,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { () -> mock(WindowManagerLockscreenVisibilityManager.class), mSelectedUserInteractor, mKeyguardInteractor, + mKeyguardTransitionBootInteractor, mock(WindowManagerOcclusionManager.class)); mViewMediator.start(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt index 8a5af09f52ed..ad5eeabf83d2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt @@ -65,7 +65,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { flowOf(Scenes.Lockscreen), progress, false, - flowOf(false) + flowOf(false), ) private val goneToLs = @@ -75,7 +75,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { flowOf(Scenes.Lockscreen), progress, false, - flowOf(false) + flowOf(false), ) @Before @@ -84,7 +84,8 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { kosmos.sceneContainerRepository.setTransitionState(sceneTransitions) testScope.launch { kosmos.realKeyguardTransitionRepository.emitInitialStepsFromOff( - KeyguardState.LOCKSCREEN + KeyguardState.LOCKSCREEN, + testSetup = true, ) } } @@ -105,11 +106,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { ) progress.value = 0.4f - assertTransition( - step = currentStep!!, - state = TransitionState.RUNNING, - progress = 0.4f, - ) + assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f) sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone) assertTransition( @@ -142,11 +139,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { ) progress.value = 0.4f - assertTransition( - step = currentStep!!, - state = TransitionState.RUNNING, - progress = 0.4f, - ) + assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f) sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen) @@ -191,7 +184,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { from = KeyguardState.LOCKSCREEN, to = KeyguardState.AOD, animator = null, - modeOnCanceled = TransitionModeOnCanceled.RESET + modeOnCanceled = TransitionModeOnCanceled.RESET, ) ) sceneTransitions.value = lsToGone @@ -205,11 +198,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { ) progress.value = 0.4f - assertTransition( - step = currentStep!!, - state = TransitionState.RUNNING, - progress = 0.4f, - ) + assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f) sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen) @@ -257,11 +246,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { ) progress.value = 0.4f - assertTransition( - step = currentStep!!, - state = TransitionState.RUNNING, - progress = 0.4f, - ) + assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f) sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen) @@ -297,7 +282,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { flowOf(Scenes.Lockscreen), progress, false, - flowOf(false) + flowOf(false), ) assertTransition( @@ -330,11 +315,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { ) progress.value = 0.4f - assertTransition( - step = currentStep!!, - state = TransitionState.RUNNING, - progress = 0.4f, - ) + assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f) sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone) val stepM3 = allSteps[allSteps.size - 3] @@ -393,7 +374,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { flowOf(Scenes.Lockscreen), progress, false, - flowOf(false) + flowOf(false), ) assertTransition( @@ -466,7 +447,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { flowOf(Scenes.Lockscreen), progress, false, - flowOf(false) + flowOf(false), ) assertTransition( @@ -523,7 +504,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { flowOf(Scenes.Lockscreen), progress, false, - flowOf(false) + flowOf(false), ) assertTransition( @@ -577,11 +558,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { ) progress.value = 0.4f - assertTransition( - step = currentStep!!, - state = TransitionState.RUNNING, - progress = 0.4f, - ) + assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f) kosmos.realKeyguardTransitionRepository.startTransition( TransitionInfo( @@ -589,7 +566,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { from = KeyguardState.LOCKSCREEN, to = KeyguardState.AOD, animator = null, - modeOnCanceled = TransitionModeOnCanceled.RESET + modeOnCanceled = TransitionModeOnCanceled.RESET, ) ) @@ -641,11 +618,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { ) progress.value = 0.4f - assertTransition( - step = currentStep!!, - state = TransitionState.RUNNING, - progress = 0.4f, - ) + assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f) kosmos.realKeyguardTransitionRepository.startTransition( TransitionInfo( @@ -653,7 +626,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { from = KeyguardState.LOCKSCREEN, to = KeyguardState.AOD, animator = null, - modeOnCanceled = TransitionModeOnCanceled.RESET + modeOnCanceled = TransitionModeOnCanceled.RESET, ) ) @@ -702,11 +675,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { ) progress.value = 0.4f - assertTransition( - step = currentStep!!, - state = TransitionState.RUNNING, - progress = 0.4f, - ) + assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f) kosmos.realKeyguardTransitionRepository.startTransition( TransitionInfo( @@ -714,7 +683,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { from = KeyguardState.LOCKSCREEN, to = KeyguardState.AOD, animator = null, - modeOnCanceled = TransitionModeOnCanceled.RESET + modeOnCanceled = TransitionModeOnCanceled.RESET, ) ) @@ -736,7 +705,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { flowOf(Scenes.Lockscreen), progress, false, - flowOf(false) + flowOf(false), ) assertTransition( @@ -777,7 +746,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { from = KeyguardState.LOCKSCREEN, to = KeyguardState.AOD, animator = null, - modeOnCanceled = TransitionModeOnCanceled.RESET + modeOnCanceled = TransitionModeOnCanceled.RESET, ) ) @@ -799,7 +768,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { flowOf(Scenes.Lockscreen), progress, false, - flowOf(false) + flowOf(false), ) allSteps[allSteps.size - 3] @@ -858,7 +827,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { from = KeyguardState.LOCKSCREEN, to = KeyguardState.AOD, animator = null, - modeOnCanceled = TransitionModeOnCanceled.RESET + modeOnCanceled = TransitionModeOnCanceled.RESET, ) ) @@ -880,7 +849,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { flowOf(Scenes.Lockscreen), progress, false, - flowOf(false) + flowOf(false), ) assertTransition( @@ -959,7 +928,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { from = KeyguardState.LOCKSCREEN, to = KeyguardState.AOD, animator = null, - modeOnCanceled = TransitionModeOnCanceled.RESET + modeOnCanceled = TransitionModeOnCanceled.RESET, ) ) @@ -977,7 +946,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { from = KeyguardState.AOD, to = KeyguardState.DOZING, animator = null, - modeOnCanceled = TransitionModeOnCanceled.RESET + modeOnCanceled = TransitionModeOnCanceled.RESET, ) ) @@ -995,7 +964,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { from = KeyguardState.DOZING, to = KeyguardState.OCCLUDED, animator = null, - modeOnCanceled = TransitionModeOnCanceled.RESET + modeOnCanceled = TransitionModeOnCanceled.RESET, ) ) @@ -1017,7 +986,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { flowOf(Scenes.Lockscreen), progress, false, - flowOf(false) + flowOf(false), ) assertTransition( @@ -1077,7 +1046,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { from = KeyguardState.LOCKSCREEN, to = KeyguardState.AOD, animator = null, - modeOnCanceled = TransitionModeOnCanceled.RESET + modeOnCanceled = TransitionModeOnCanceled.RESET, ) ) @@ -1092,7 +1061,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { kosmos.realKeyguardTransitionRepository.updateTransition( ktfUuid!!, 1f, - TransitionState.FINISHED + TransitionState.FINISHED, ) assertTransition( @@ -1110,7 +1079,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { flowOf(Scenes.Lockscreen), progress, false, - flowOf(false) + flowOf(false), ) assertTransition( @@ -1171,7 +1140,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { flowOf(Scenes.Lockscreen), progress, false, - flowOf(false) + flowOf(false), ) assertTransition( @@ -1235,7 +1204,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { flowOf(Scenes.Lockscreen), progress, false, - flowOf(false) + flowOf(false), ) assertTransition( @@ -1291,7 +1260,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { flowOf(Scenes.Lockscreen), progress, false, - flowOf(false) + flowOf(false), ) assertTransition( @@ -1308,7 +1277,7 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { from: KeyguardState? = null, to: KeyguardState? = null, state: TransitionState? = null, - progress: Float? = null + progress: Float? = null, ) { if (from != null) assertThat(step.from).isEqualTo(from) if (to != null) assertThat(step.to).isEqualTo(to) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt index d32d8cc4bd51..fb376ce3ca40 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt @@ -1890,7 +1890,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa // Callback gets an updated state val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build() - stateCallbackCaptor.value.invoke(KEY, state) + onStateUpdated(KEY, state) // Listener is notified of updated state verify(listener) @@ -1911,7 +1911,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa // No media added with this key - stateCallbackCaptor.value.invoke(KEY, state) + onStateUpdated(KEY, state) verify(listener, never()) .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean()) } @@ -1928,7 +1928,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa val state = PlaybackState.Builder().build() // Then no changes are made - stateCallbackCaptor.value.invoke(KEY, state) + onStateUpdated(KEY, state) verify(listener, never()) .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean()) } @@ -1939,7 +1939,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa whenever(controller.playbackState).thenReturn(state) addNotificationAndLoad() - stateCallbackCaptor.value.invoke(KEY, state) + onStateUpdated(KEY, state) verify(listener) .onMediaDataLoaded( @@ -1983,7 +1983,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa backgroundExecutor.runAllReady() foregroundExecutor.runAllReady() - stateCallbackCaptor.value.invoke(PACKAGE_NAME, state) + onStateUpdated(PACKAGE_NAME, state) verify(listener) .onMediaDataLoaded( @@ -2008,7 +2008,7 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa .build() addNotificationAndLoad() - stateCallbackCaptor.value.invoke(KEY, state) + onStateUpdated(KEY, state) verify(listener) .onMediaDataLoaded( @@ -2518,4 +2518,10 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa eq(false), ) } + + private fun onStateUpdated(key: String, state: PlaybackState) { + stateCallbackCaptor.value.invoke(key, state) + backgroundExecutor.runAllReady() + foregroundExecutor.runAllReady() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt index 90af93292de1..7d364bd832f2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt @@ -1967,7 +1967,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { // Callback gets an updated state val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build() - stateCallbackCaptor.value.invoke(KEY, state) + testScope.onStateUpdated(KEY, state) // Listener is notified of updated state verify(listener) @@ -1988,7 +1988,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { // No media added with this key - stateCallbackCaptor.value.invoke(KEY, state) + testScope.onStateUpdated(KEY, state) verify(listener, never()) .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean()) } @@ -2005,7 +2005,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { val state = PlaybackState.Builder().build() // Then no changes are made - stateCallbackCaptor.value.invoke(KEY, state) + testScope.onStateUpdated(KEY, state) verify(listener, never()) .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean()) } @@ -2016,7 +2016,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { whenever(controller.playbackState).thenReturn(state) addNotificationAndLoad() - stateCallbackCaptor.value.invoke(KEY, state) + testScope.onStateUpdated(KEY, state) verify(listener) .onMediaDataLoaded( @@ -2059,7 +2059,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { backgroundExecutor.runAllReady() foregroundExecutor.runAllReady() - stateCallbackCaptor.value.invoke(PACKAGE_NAME, state) + testScope.onStateUpdated(PACKAGE_NAME, state) verify(listener) .onMediaDataLoaded( @@ -2084,7 +2084,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { .build() addNotificationAndLoad() - stateCallbackCaptor.value.invoke(KEY, state) + testScope.onStateUpdated(KEY, state) verify(listener) .onMediaDataLoaded( @@ -2603,4 +2603,11 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { eq(false), ) } + + /** Helper function to update state and run executors */ + private fun TestScope.onStateUpdated(key: String, state: PlaybackState) { + stateCallbackCaptor.value.invoke(key, state) + runCurrent() + advanceUntilIdle() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt index 680df1584f89..dcf32a5f574d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt @@ -137,7 +137,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { MediaTestUtils.emptyMediaData.copy( app = PACKAGE, packageName = PACKAGE, - token = session.sessionToken + token = session.sessionToken, ) resumeData = mediaData.copy(token = null, active = false, resumption = true) @@ -237,7 +237,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // Assuming we're registered testOnMediaDataLoaded_registersPlaybackListener() - mediaCallbackCaptor.value.onPlaybackStateChanged( + onPlaybackStateChanged( PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() ) assertThat(mainExecutor.numPending()).isEqualTo(1) @@ -249,7 +249,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // Assuming we have a pending timeout testOnPlaybackStateChanged_schedulesTimeout_whenPaused() - mediaCallbackCaptor.value.onPlaybackStateChanged( + onPlaybackStateChanged( PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 0f).build() ) assertThat(mainExecutor.numPending()).isEqualTo(0) @@ -261,7 +261,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // Assuming we have a pending timeout testOnPlaybackStateChanged_schedulesTimeout_whenPaused() - mediaCallbackCaptor.value.onPlaybackStateChanged( + onPlaybackStateChanged( PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build() ) assertThat(mainExecutor.numPending()).isEqualTo(1) @@ -435,7 +435,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // When the playback state changes, and has different actions val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build() - mediaCallbackCaptor.value.onPlaybackStateChanged(playingState) + onPlaybackStateChanged(playingState) assertThat(uiExecutor.runAllReady()).isEqualTo(1) // Then the callback is invoked @@ -448,7 +448,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { PlaybackState.CustomAction.Builder( "ACTION_1", "custom action 1", - android.R.drawable.ic_media_ff + android.R.drawable.ic_media_ff, ) .build() val pausedState = @@ -463,7 +463,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { PlaybackState.CustomAction.Builder( "ACTION_2", "custom action 2", - android.R.drawable.ic_media_rew + android.R.drawable.ic_media_rew, ) .build() val pausedStateTwoActions = @@ -472,7 +472,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { .addCustomAction(customOne) .addCustomAction(customTwo) .build() - mediaCallbackCaptor.value.onPlaybackStateChanged(pausedStateTwoActions) + onPlaybackStateChanged(pausedStateTwoActions) assertThat(uiExecutor.runAllReady()).isEqualTo(1) // Then the callback is invoked @@ -485,7 +485,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { loadMediaDataWithPlaybackState(stateWithActions) // When the playback state updates with the same actions - mediaCallbackCaptor.value.onPlaybackStateChanged(stateWithActions) + onPlaybackStateChanged(stateWithActions) // Then the callback is not invoked again verify(stateCallback, never()).invoke(eq(KEY), any()) @@ -512,7 +512,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { .setActions(PlaybackState.ACTION_PAUSE) .addCustomAction(customTwo) .build() - mediaCallbackCaptor.value.onPlaybackStateChanged(stateTwo) + onPlaybackStateChanged(stateTwo) // Then the callback is not invoked verify(stateCallback, never()).invoke(eq(KEY), any()) @@ -544,7 +544,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // When the playback state changes to playing val playingState = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build() - mediaCallbackCaptor.value.onPlaybackStateChanged(playingState) + onPlaybackStateChanged(playingState) uiExecutor.runAllReady() // Then the callback is invoked @@ -561,7 +561,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // When the playback state is updated, but still not playing val playingState = PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build() - mediaCallbackCaptor.value.onPlaybackStateChanged(playingState) + onPlaybackStateChanged(playingState) // Then the callback is not invoked verify(stateCallback, never()).invoke(eq(KEY), eq(playingState!!)) @@ -571,7 +571,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() { // When paused media is loaded testOnMediaDataLoaded_registersPlaybackListener() - mediaCallbackCaptor.value.onPlaybackStateChanged( + onPlaybackStateChanged( PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() ) verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor)) @@ -597,7 +597,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { val time = clock.currentTimeMillis() clock.setElapsedRealtime(time) testOnMediaDataLoaded_registersPlaybackListener() - mediaCallbackCaptor.value.onPlaybackStateChanged( + onPlaybackStateChanged( PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() ) verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor)) @@ -706,4 +706,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() { bgExecutor.runAllReady() uiExecutor.runAllReady() } + + private fun onPlaybackStateChanged(state: PlaybackState) { + mediaCallbackCaptor.value.onPlaybackStateChanged(state) + bgExecutor.runAllReady() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt index 03667cfb8a3b..570c64065c4a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt @@ -21,19 +21,19 @@ import android.content.res.ColorStateList import android.content.res.Configuration import android.database.ContentObserver import android.os.LocaleList +import android.platform.test.flag.junit.FlagsParameterization import android.provider.Settings import android.testing.TestableLooper import android.util.MathUtils.abs import android.view.View -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.internal.logging.InstanceId import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.systemui.Flags.mediaControlsUmoInflationInBackground import com.android.systemui.SysuiTestCase -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.dump.DumpManager import com.android.systemui.flags.DisableSceneContainer @@ -71,7 +71,6 @@ import com.android.systemui.statusbar.notification.collection.provider.OnReorder import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.testKosmos -import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.settings.GlobalSettings import com.android.systemui.util.settings.unconfinedDispatcherFakeSettings @@ -106,6 +105,8 @@ import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.capture import org.mockito.kotlin.eq +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters private val DATA = MediaTestUtils.emptyMediaData @@ -116,8 +117,8 @@ private const val PLAYING_LOCAL = "playing local" @ExperimentalCoroutinesApi @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) -@RunWith(AndroidJUnit4::class) -class MediaCarouselControllerTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase() { private val kosmos = testKosmos() private val testDispatcher = kosmos.unconfinedTestDispatcher private val secureSettings = kosmos.unconfinedDispatcherFakeSettings @@ -129,7 +130,6 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager @Mock lateinit var mediaHostState: MediaHostState @Mock lateinit var activityStarter: ActivityStarter - @Mock @Main private lateinit var executor: DelayableExecutor @Mock lateinit var mediaDataManager: MediaDataManager @Mock lateinit var configurationController: ConfigurationController @Mock lateinit var falsingManager: FalsingManager @@ -153,16 +153,33 @@ class MediaCarouselControllerTest : SysuiTestCase() { private val clock = FakeSystemClock() private lateinit var bgExecutor: FakeExecutor + private lateinit var uiExecutor: FakeExecutor private lateinit var mediaCarouselController: MediaCarouselController private var originalResumeSetting = Settings.Secure.getInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 1) + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.progressionOf( + com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_UMO_INFLATION_IN_BACKGROUND + ) + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + @Before fun setup() { MockitoAnnotations.initMocks(this) context.resources.configuration.setLocales(LocaleList(Locale.US, Locale.UK)) bgExecutor = FakeExecutor(clock) + uiExecutor = FakeExecutor(clock) + mediaCarouselController = MediaCarouselController( applicationScope = kosmos.applicationCoroutineScope, @@ -173,7 +190,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { activityStarter = activityStarter, systemClock = clock, mainDispatcher = kosmos.testDispatcher, - executor = executor, + uiExecutor = uiExecutor, bgExecutor = bgExecutor, backgroundDispatcher = testDispatcher, mediaManager = mediaDataManager, @@ -201,10 +218,11 @@ class MediaCarouselControllerTest : SysuiTestCase() { whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false) MediaPlayerData.clear() FakeExecutor.exhaustExecutors(bgExecutor) + FakeExecutor.exhaustExecutors(uiExecutor) verify(globalSettings) .registerContentObserverSync( eq(Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE)), - capture(settingsObserverCaptor) + capture(settingsObserverCaptor), ) } @@ -213,7 +231,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { Settings.Secure.putInt( context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, - originalResumeSetting + originalResumeSetting, ) } @@ -227,9 +245,9 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = true, isPlaying = true, playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false + resumption = false, ), - 4500L + 4500L, ) val playingCast = @@ -239,9 +257,9 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = true, isPlaying = true, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, - resumption = false + resumption = false, ), - 5000L + 5000L, ) val pausedLocal = @@ -251,9 +269,9 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = true, isPlaying = false, playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false + resumption = false, ), - 1000L + 1000L, ) val pausedCast = @@ -263,9 +281,9 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = true, isPlaying = false, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, - resumption = false + resumption = false, ), - 2000L + 2000L, ) val playingRcn = @@ -275,9 +293,9 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = true, isPlaying = true, playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, - resumption = false + resumption = false, ), - 5000L + 5000L, ) val pausedRcn = @@ -287,9 +305,9 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = true, isPlaying = false, playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, - resumption = false + resumption = false, ), - 5000L + 5000L, ) val active = @@ -299,9 +317,9 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = true, isPlaying = false, playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = true + resumption = true, ), - 250L + 250L, ) val resume1 = @@ -311,9 +329,9 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = false, isPlaying = false, playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = true + resumption = true, ), - 500L + 500L, ) val resume2 = @@ -323,9 +341,9 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = false, isPlaying = false, playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = true + resumption = true, ), - 1000L + 1000L, ) val activeMoreRecent = @@ -336,9 +354,9 @@ class MediaCarouselControllerTest : SysuiTestCase() { isPlaying = false, playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true, - lastActive = 2L + lastActive = 2L, ), - 1000L + 1000L, ) val activeLessRecent = @@ -349,9 +367,9 @@ class MediaCarouselControllerTest : SysuiTestCase() { isPlaying = false, playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true, - lastActive = 1L + lastActive = 1L, ), - 1000L + 1000L, ) // Expected ordering for media players: // Actively playing local sessions @@ -370,7 +388,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { pausedRcn, active, resume2, - resume1 + resume1, ) expected.forEach { @@ -380,7 +398,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { it.second.copy(notificationKey = it.first), panel, clock, - isSsReactivated = false + isSsReactivated = false, ) } @@ -403,7 +421,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true), panel, true, - clock + clock, ) // Then it should be shown immediately after any actively playing controls @@ -421,7 +439,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { listener.value.onSmartspaceMediaDataLoaded( SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true), - true + true, ) // Then it should be shown immediately after any actively playing controls @@ -439,7 +457,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true), panel, false, - clock + clock, ) // Then it should be shown at the end of the carousel's active entries @@ -461,8 +479,8 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = true, isPlaying = true, playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false - ) + resumption = false, + ), ) listener.value.onMediaDataLoaded( PLAYING_LOCAL, @@ -471,19 +489,20 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = true, isPlaying = false, playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = true - ) + resumption = true, + ), ) + runAllReady() assertEquals( MediaPlayerData.getMediaPlayerIndex(PAUSED_LOCAL), - mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex + mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex, ) // paused player order should stays the same in visibleMediaPLayer map. // paused player order should be first in mediaPlayer map. assertEquals( MediaPlayerData.visiblePlayerKeys().elementAt(3), - MediaPlayerData.playerKeys().elementAt(0) + MediaPlayerData.playerKeys().elementAt(0), ) } @@ -506,7 +525,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { mediaCarouselController.onDesiredLocationChanged( LOCATION_QS, mediaHostState, - animate = false + animate = false, ) bgExecutor.runAllReady() verify(logger).logCarouselPosition(LOCATION_QS) @@ -517,7 +536,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { mediaCarouselController.onDesiredLocationChanged( MediaHierarchyManager.LOCATION_QQS, mediaHostState, - animate = false + animate = false, ) bgExecutor.runAllReady() verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS) @@ -528,7 +547,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { mediaCarouselController.onDesiredLocationChanged( MediaHierarchyManager.LOCATION_LOCKSCREEN, mediaHostState, - animate = false + animate = false, ) bgExecutor.runAllReady() verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN) @@ -539,7 +558,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { mediaCarouselController.onDesiredLocationChanged( MediaHierarchyManager.LOCATION_DREAM_OVERLAY, mediaHostState, - animate = false + animate = false, ) bgExecutor.runAllReady() verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY) @@ -570,8 +589,8 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = true, isPlaying = true, playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false - ) + resumption = false, + ), ) listener.value.onMediaDataLoaded( PAUSED_LOCAL, @@ -580,14 +599,15 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = true, isPlaying = false, playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false - ) + resumption = false, + ), ) + runAllReady() // adding a media recommendation card. listener.value.onSmartspaceMediaDataLoaded( SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, - false + false, ) mediaCarouselController.shouldScrollToKey = true // switching between media players. @@ -598,8 +618,8 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = true, isPlaying = false, playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = true - ) + resumption = true, + ), ) listener.value.onMediaDataLoaded( PAUSED_LOCAL, @@ -608,13 +628,14 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = true, isPlaying = true, playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false - ) + resumption = false, + ), ) + runAllReady() assertEquals( MediaPlayerData.getMediaPlayerIndex(PAUSED_LOCAL), - mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex + mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex, ) } @@ -626,7 +647,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { listener.value.onSmartspaceMediaDataLoaded( SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true), - false + false, ) listener.value.onMediaDataLoaded( PLAYING_LOCAL, @@ -635,14 +656,15 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = true, isPlaying = true, playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false - ) + resumption = false, + ), ) + runAllReady() var playerIndex = MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL) assertEquals( playerIndex, - mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex + mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex, ) assertEquals(playerIndex, 0) @@ -657,9 +679,10 @@ class MediaCarouselControllerTest : SysuiTestCase() { isPlaying = true, playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false, - packageName = "PACKAGE_NAME" - ) + packageName = "PACKAGE_NAME", + ), ) + runAllReady() playerIndex = MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL) assertEquals(playerIndex, 0) } @@ -704,7 +727,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { player1.second.copy(notificationKey = player1.first), panel, clock, - isSsReactivated = false + isSsReactivated = false, ) assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1) @@ -717,7 +740,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { player2.second.copy(notificationKey = player2.first), panel, clock, - isSsReactivated = false + isSsReactivated = false, ) // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is @@ -732,7 +755,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { player3.second.copy(notificationKey = player3.first), panel, clock, - isSsReactivated = false + isSsReactivated = false, ) // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is @@ -822,7 +845,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { listener.value.onSmartspaceMediaDataLoaded( SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true), - true + true, ) // Then the carousel is updated @@ -841,7 +864,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { listener.value.onSmartspaceMediaDataLoaded( SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, - false + false, ) // Then it is added to the carousel with correct state @@ -886,7 +909,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { transitionRepository.sendTransitionSteps( from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE, - this + this, ) verify(mediaCarousel).visibility = View.VISIBLE @@ -932,7 +955,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { transitionRepository.sendTransitionSteps( from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN, - this + this, ) assertEquals(true, updatedVisibility) @@ -961,7 +984,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { transitionRepository.sendTransitionSteps( from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN, - this + this, ) assertEquals(true, updatedVisibility) @@ -1125,12 +1148,14 @@ class MediaCarouselControllerTest : SysuiTestCase() { Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0) val pausedMedia = DATA.copy(isPlaying = false) listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, pausedMedia) + runAllReady() mediaCarouselController.onSwipeToDismiss() // When it can be removed immediately on update whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true) val inactiveMedia = pausedMedia.copy(active = false) listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, inactiveMedia) + runAllReady() // This is processed as a user-initiated dismissal verify(debugLogger).logMediaRemoved(eq(PAUSED_LOCAL), eq(true)) @@ -1148,12 +1173,14 @@ class MediaCarouselControllerTest : SysuiTestCase() { val pausedMedia = DATA.copy(isPlaying = false) listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, pausedMedia) + runAllReady() mediaCarouselController.onSwipeToDismiss() // When it can't be removed immediately on update whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false) val inactiveMedia = pausedMedia.copy(active = false) listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, inactiveMedia) + runAllReady() visualStabilityCallback.value.onReorderingAllowed() // This is processed as a user-initiated dismissal @@ -1175,8 +1202,8 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = true, isPlaying = true, playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false - ) + resumption = false, + ), ) listener.value.onMediaDataLoaded( PAUSED_LOCAL, @@ -1185,18 +1212,20 @@ class MediaCarouselControllerTest : SysuiTestCase() { active = true, isPlaying = false, playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false - ) + resumption = false, + ), ) + runAllReady() val playersSize = MediaPlayerData.players().size reset(pageIndicator) function() + runAllReady() assertEquals(playersSize, MediaPlayerData.players().size) assertEquals( MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL), - mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex + mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex, ) } @@ -1225,4 +1254,11 @@ class MediaCarouselControllerTest : SysuiTestCase() { ) runCurrent() } + + private fun runAllReady() { + if (mediaControlsUmoInflationInBackground()) { + bgExecutor.runAllReady() + uiExecutor.runAllReady() + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java index eea02eec7099..2f8f45cb0197 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java @@ -29,6 +29,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -887,6 +888,34 @@ public class InternetDialogDelegateControllerTest extends SysuiTestCase { } @Test + public void getActiveAutoSwitchNonDdsSubId_registerCallbackForExistedSubId_notRegister() { + mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true); + + // Adds non DDS subId + SubscriptionInfo info = mock(SubscriptionInfo.class); + doReturn(SUB_ID2).when(info).getSubscriptionId(); + doReturn(false).when(info).isOpportunistic(); + when(mSubscriptionManager.getActiveSubscriptionInfo(anyInt())).thenReturn(info); + + mInternetDialogController.getActiveAutoSwitchNonDdsSubId(); + + // 1st time is onStart(), 2nd time is getActiveAutoSwitchNonDdsSubId() + verify(mTelephonyManager, times(2)).registerTelephonyCallback(any(), any()); + assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.size() == 2); + + // Adds non DDS subId again + doReturn(SUB_ID2).when(info).getSubscriptionId(); + doReturn(false).when(info).isOpportunistic(); + when(mSubscriptionManager.getActiveSubscriptionInfo(anyInt())).thenReturn(info); + + mInternetDialogController.getActiveAutoSwitchNonDdsSubId(); + + // Does not add due to cached subInfo in mSubIdTelephonyCallbackMap. + verify(mTelephonyManager, times(2)).registerTelephonyCallback(any(), any()); + assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.size() == 2); + } + + @Test public void getMobileNetworkSummary() { mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true); Resources res1 = mock(Resources.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt index a1750cdd0c84..b1ec740c5564 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt @@ -21,6 +21,7 @@ import android.platform.test.annotations.EnableFlags 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.OverlayKey import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue @@ -28,6 +29,7 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.data.repository.setSceneTransition import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes @@ -38,9 +40,9 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf +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 @@ -55,14 +57,9 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { private val configurationRepository = kosmos.fakeConfigurationRepository private val keyguardRepository = kosmos.fakeKeyguardRepository private val sceneInteractor = kosmos.sceneInteractor - private val shadeTestUtil = kosmos.shadeTestUtil + private val shadeTestUtil by lazy { kosmos.shadeTestUtil } - private lateinit var underTest: ShadeInteractorSceneContainerImpl - - @Before - fun setUp() { - underTest = kosmos.shadeInteractorSceneContainerImpl - } + private val underTest by lazy { kosmos.shadeInteractorSceneContainerImpl } @Test fun qsExpansionWhenInSplitShadeAndQsExpanded() = @@ -600,14 +597,14 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { @Test @EnableFlags(DualShade.FLAG_NAME) - fun expandNotificationShade_dualShadeEnabled_opensOverlay() = + fun expandNotificationsShade_dualShade_opensOverlay() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) assertThat(currentScene).isEqualTo(Scenes.Lockscreen) assertThat(currentOverlays).isEmpty() - underTest.expandNotificationShade("reason") + underTest.expandNotificationsShade("reason") assertThat(currentScene).isEqualTo(Scenes.Lockscreen) assertThat(currentOverlays).containsExactly(Overlays.NotificationsShade) @@ -615,14 +612,15 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { @Test @DisableFlags(DualShade.FLAG_NAME) - fun expandNotificationShade_dualShadeDisabled_switchesToShadeScene() = + fun expandNotificationsShade_singleShade_switchesToShadeScene() = testScope.runTest { + shadeTestUtil.setSplitShade(false) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) assertThat(currentScene).isEqualTo(Scenes.Lockscreen) assertThat(currentOverlays).isEmpty() - underTest.expandNotificationShade("reason") + underTest.expandNotificationsShade("reason") assertThat(currentScene).isEqualTo(Scenes.Shade) assertThat(currentOverlays).isEmpty() @@ -630,7 +628,7 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { @Test @EnableFlags(DualShade.FLAG_NAME) - fun expandNotificationShade_dualShadeEnabledAndQuickSettingsOpen_replacesOverlay() = + fun expandNotificationsShade_dualShadeQuickSettingsOpen_replacesOverlay() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) @@ -638,14 +636,14 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { assertThat(currentScene).isEqualTo(Scenes.Lockscreen) assertThat(currentOverlays).containsExactly(Overlays.QuickSettingsShade) - underTest.expandNotificationShade("reason") + underTest.expandNotificationsShade("reason") assertThat(currentScene).isEqualTo(Scenes.Lockscreen) assertThat(currentOverlays).containsExactly(Overlays.NotificationsShade) } @Test @EnableFlags(DualShade.FLAG_NAME) - fun expandQuickSettingsShade_dualShadeEnabled_opensOverlay() = + fun expandQuickSettingsShade_dualShade_opensOverlay() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) @@ -660,8 +658,9 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { @Test @DisableFlags(DualShade.FLAG_NAME) - fun expandQuickSettingsShade_dualShadeDisabled_switchesToQuickSettingsScene() = + fun expandQuickSettingsShade_singleShade_switchesToQuickSettingsScene() = testScope.runTest { + shadeTestUtil.setSplitShade(false) val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) assertThat(currentScene).isEqualTo(Scenes.Lockscreen) @@ -674,12 +673,28 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { } @Test + @DisableFlags(DualShade.FLAG_NAME) + fun expandQuickSettingsShade_splitShade_switchesToShadeScene() = + testScope.runTest { + shadeTestUtil.setSplitShade(true) + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + assertThat(currentOverlays).isEmpty() + + underTest.expandQuickSettingsShade("reason") + + assertThat(currentScene).isEqualTo(Scenes.Shade) + assertThat(currentOverlays).isEmpty() + } + + @Test @EnableFlags(DualShade.FLAG_NAME) - fun expandQuickSettingsShade_dualShadeEnabledAndNotificationsOpen_replacesOverlay() = + fun expandQuickSettingsShade_dualShadeNotificationsOpen_replacesOverlay() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) - underTest.expandNotificationShade("reason") + underTest.expandNotificationsShade("reason") assertThat(currentScene).isEqualTo(Scenes.Lockscreen) assertThat(currentOverlays).containsExactly(Overlays.NotificationsShade) @@ -687,4 +702,141 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { assertThat(currentScene).isEqualTo(Scenes.Lockscreen) assertThat(currentOverlays).containsExactly(Overlays.QuickSettingsShade) } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun collapseNotificationsShade_dualShade_hidesOverlay() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + openShade(Overlays.NotificationsShade) + + underTest.collapseNotificationsShade("reason") + + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + assertThat(currentOverlays).isEmpty() + } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun collapseNotificationsShade_singleShade_switchesToLockscreen() = + testScope.runTest { + shadeTestUtil.setSplitShade(false) + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + sceneInteractor.changeScene(Scenes.Shade, "reason") + assertThat(currentScene).isEqualTo(Scenes.Shade) + assertThat(currentOverlays).isEmpty() + + underTest.collapseNotificationsShade("reason") + + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + assertThat(currentOverlays).isEmpty() + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun collapseQuickSettingsShade_dualShade_hidesOverlay() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + openShade(Overlays.QuickSettingsShade) + + underTest.collapseQuickSettingsShade("reason") + + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + assertThat(currentOverlays).isEmpty() + } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun collapseQuickSettingsShadeNotBypassingShade_singleShade_switchesToShade() = + testScope.runTest { + shadeTestUtil.setSplitShade(false) + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + sceneInteractor.changeScene(Scenes.QuickSettings, "reason") + assertThat(currentScene).isEqualTo(Scenes.QuickSettings) + assertThat(currentOverlays).isEmpty() + + underTest.collapseQuickSettingsShade( + loggingReason = "reason", + bypassNotificationsShade = false, + ) + + assertThat(currentScene).isEqualTo(Scenes.Shade) + assertThat(currentOverlays).isEmpty() + } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun collapseQuickSettingsShadeNotBypassingShade_splitShade_switchesToLockscreen() = + testScope.runTest { + shadeTestUtil.setSplitShade(true) + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + sceneInteractor.changeScene(Scenes.QuickSettings, "reason") + assertThat(currentScene).isEqualTo(Scenes.QuickSettings) + assertThat(currentOverlays).isEmpty() + + underTest.collapseQuickSettingsShade( + loggingReason = "reason", + bypassNotificationsShade = false, + ) + + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + assertThat(currentOverlays).isEmpty() + } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun collapseQuickSettingsShadeBypassingShade_singleShade_switchesToLockscreen() = + testScope.runTest { + shadeTestUtil.setSplitShade(false) + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + sceneInteractor.changeScene(Scenes.QuickSettings, "reason") + assertThat(currentScene).isEqualTo(Scenes.QuickSettings) + assertThat(currentOverlays).isEmpty() + + underTest.collapseQuickSettingsShade( + loggingReason = "reason", + bypassNotificationsShade = true, + ) + + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + assertThat(currentOverlays).isEmpty() + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun collapseEitherShade_dualShade_hidesBothOverlays() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + openShade(Overlays.QuickSettingsShade) + openShade(Overlays.NotificationsShade) + assertThat(currentOverlays) + .containsExactly(Overlays.QuickSettingsShade, Overlays.NotificationsShade) + + underTest.collapseEitherShade("reason") + + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + assertThat(currentOverlays).isEmpty() + } + + private fun TestScope.openShade(overlay: OverlayKey) { + val isAnyExpanded by collectLastValue(underTest.isAnyExpanded) + val currentScene by collectLastValue(sceneInteractor.currentScene) + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + val initialScene = checkNotNull(currentScene) + sceneInteractor.showOverlay(overlay, "reason") + kosmos.setSceneTransition( + ObservableTransitionState.Idle(initialScene, checkNotNull(currentOverlays)) + ) + runCurrent() + assertThat(currentScene).isEqualTo(initialScene) + assertThat(currentOverlays).contains(overlay) + assertThat(isAnyExpanded).isTrue() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt index 0407fc14d35a..ac7388281a15 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt @@ -24,7 +24,7 @@ import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NOTIFICATIONS_USE_PEOPLE_FILTERING import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection import com.android.systemui.util.DeviceConfigProxyFake import com.android.systemui.util.Utils @@ -42,7 +42,7 @@ import org.mockito.quality.Strictness @RunWith(AndroidJUnit4::class) @SmallTest // this class has no testable logic with either of these flags enabled -@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.FLAG_NAME) +@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalism.FLAG_NAME) class NotificationSectionsFeatureManagerTest : SysuiTestCase() { lateinit var manager: NotificationSectionsFeatureManager private val proxyFake = DeviceConfigProxyFake() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java index 07c29a024a6c..0c65c9cbe1ef 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java @@ -20,7 +20,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; @@ -30,15 +29,12 @@ import static org.mockito.Mockito.when; import android.app.Notification.MediaStyle; import android.media.session.MediaSession; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; import android.service.notification.NotificationListenerService; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.internal.statusbar.IStatusBarService; -import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.statusbar.notification.InflationException; @@ -158,8 +154,7 @@ public final class MediaCoordinatorTest extends SysuiTestCase { } @Test - @DisableFlags(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS) - public void inflateMediaNotificationIconsMediaEnabled_old() throws InflationException { + public void inflateMediaNotificationIconsMediaEnabled() throws InflationException { finishSetupWithMediaFeatureFlagEnabled(true); mListener.onEntryInit(mMediaEntry); @@ -187,37 +182,7 @@ public final class MediaCoordinatorTest extends SysuiTestCase { } @Test - @EnableFlags(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS) - public void inflateMediaNotificationIconsMediaEnabled_new() throws InflationException { - finishSetupWithMediaFeatureFlagEnabled(true); - - mListener.onEntryInit(mMediaEntry); - mListener.onEntryAdded(mMediaEntry); - verify(mIconManager).createIcons(eq(mMediaEntry)); - verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean()); - clearInvocations(mIconManager); - - mFilter.shouldFilterOut(mMediaEntry, 0); - verify(mIconManager, never()).createIcons(eq(mMediaEntry)); - verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean()); - - mListener.onEntryUpdated(mMediaEntry); - verify(mIconManager, never()).createIcons(eq(mMediaEntry)); - verify(mIconManager).updateIcons(eq(mMediaEntry), /* usingCache = */ eq(false)); - - mListener.onEntryRemoved(mMediaEntry, NotificationListenerService.REASON_CANCEL); - mListener.onEntryCleanUp(mMediaEntry); - clearInvocations(mIconManager); - - mListener.onEntryInit(mMediaEntry); - mListener.onEntryAdded(mMediaEntry); - verify(mIconManager).createIcons(eq(mMediaEntry)); - verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean()); - } - - @Test - @DisableFlags(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS) - public void inflationException_old() throws InflationException { + public void inflationException() throws InflationException { finishSetupWithMediaFeatureFlagEnabled(true); mListener.onEntryInit(mMediaEntry); @@ -244,31 +209,6 @@ public final class MediaCoordinatorTest extends SysuiTestCase { verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean()); } - @Test - @EnableFlags(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS) - public void inflationException_new() throws InflationException { - finishSetupWithMediaFeatureFlagEnabled(true); - - doThrow(InflationException.class).when(mIconManager).createIcons(eq(mMediaEntry)); - - mListener.onEntryInit(mMediaEntry); - mListener.onEntryAdded(mMediaEntry); - verify(mIconManager).createIcons(eq(mMediaEntry)); - verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean()); - clearInvocations(mIconManager); - - mListener.onEntryUpdated(mMediaEntry); - verify(mIconManager).createIcons(eq(mMediaEntry)); - verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean()); - clearInvocations(mIconManager); - - doNothing().when(mIconManager).createIcons(eq(mMediaEntry)); - - mListener.onEntryUpdated(mMediaEntry); - verify(mIconManager).createIcons(eq(mMediaEntry)); - verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean()); - } - private void finishSetupWithMediaFeatureFlagEnabled(boolean mediaFeatureFlagEnabled) { when(mMediaFeatureFlag.getEnabled()).thenReturn(mediaFeatureFlagEnabled); mCoordinator = new MediaCoordinator(mMediaFeatureFlag, mStatusBarService, mIconManager); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt index deb3fc1224ce..a3f845225a99 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt @@ -15,8 +15,8 @@ */ package com.android.systemui.statusbar.notification.collection.coordinator -import android.app.Flags.lifetimeExtensionRefactor import android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR +import android.app.Flags.lifetimeExtensionRefactor import android.app.Notification import android.app.RemoteInputHistoryItem import android.os.Handler @@ -47,10 +47,10 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations.initMocks @SmallTest @@ -78,21 +78,20 @@ class RemoteInputCoordinatorTest : SysuiTestCase() { @Before fun setUp() { initMocks(this) - coordinator = RemoteInputCoordinator( + coordinator = + RemoteInputCoordinator( dumpManager, rebuilder, remoteInputManager, mainHandler, - smartReplyController - ) + smartReplyController, + ) `when`(pipeline.addNotificationLifetimeExtender(any())).thenAnswer { (it.arguments[0] as NotifLifetimeExtender).setCallback(lifetimeExtensionCallback) } `when`(pipeline.getInternalNotifUpdater(any())).thenReturn(notifUpdater) coordinator.attach(pipeline) - listener = withArgCaptor { - verify(remoteInputManager).setRemoteInputListener(capture()) - } + listener = withArgCaptor { verify(remoteInputManager).setRemoteInputListener(capture()) } entry1 = NotificationEntryBuilder().setId(1).build() entry2 = NotificationEntryBuilder().setId(2).build() `when`(rebuilder.rebuildForCanceledSmartReplies(any())).thenReturn(sbn) @@ -101,13 +100,17 @@ class RemoteInputCoordinatorTest : SysuiTestCase() { `when`(rebuilder.rebuildWithExistingReplies(any())).thenReturn(sbn) } - val remoteInputActiveExtender get() = coordinator.mRemoteInputActiveExtender - val remoteInputHistoryExtender get() = coordinator.mRemoteInputHistoryExtender - val smartReplyHistoryExtender get() = coordinator.mSmartReplyHistoryExtender + val remoteInputActiveExtender + get() = coordinator.mRemoteInputActiveExtender - val collectionListeners get() = captureMany { - verify(pipeline, times(1)).addCollectionListener(capture()) - } + val remoteInputHistoryExtender + get() = coordinator.mRemoteInputHistoryExtender + + val smartReplyHistoryExtender + get() = coordinator.mSmartReplyHistoryExtender + + val collectionListeners + get() = captureMany { verify(pipeline, times(1)).addCollectionListener(capture()) } @Test fun testRemoteInputActive() { @@ -179,7 +182,8 @@ class RemoteInputCoordinatorTest : SysuiTestCase() { @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR) fun testRemoteInputLifetimeExtensionListenerTrigger() { // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag. - val entry = NotificationEntryBuilder() + val entry = + NotificationEntryBuilder() .setId(3) .setTag("entry") .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true) @@ -187,9 +191,7 @@ class RemoteInputCoordinatorTest : SysuiTestCase() { `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(true) `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false) - collectionListeners.forEach { - it.onEntryUpdated(entry, true) - } + collectionListeners.forEach { it.onEntryUpdated(entry, true) } verify(rebuilder, times(1)).rebuildForRemoteInputReply(entry) } @@ -198,16 +200,15 @@ class RemoteInputCoordinatorTest : SysuiTestCase() { @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR) fun testSmartReplyLifetimeExtensionListenerTrigger() { // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag. - val entry = NotificationEntryBuilder() + val entry = + NotificationEntryBuilder() .setId(3) .setTag("entry") .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true) .build() `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false) `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(true) - collectionListeners.forEach { - it.onEntryUpdated(entry, true) - } + collectionListeners.forEach { it.onEntryUpdated(entry, true) } verify(rebuilder, times(1)).rebuildForCanceledSmartReplies(entry) verify(smartReplyController, times(1)).stopSending(entry) @@ -217,25 +218,25 @@ class RemoteInputCoordinatorTest : SysuiTestCase() { @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR) fun testRepeatedUpdateTriggersRebuild() { // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag. - val entry = NotificationEntryBuilder() + val entry = + NotificationEntryBuilder() .setId(3) .setTag("entry") .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true) .build() `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false) `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false) - collectionListeners.forEach { - it.onEntryUpdated(entry, true) - } + collectionListeners.forEach { it.onEntryUpdated(entry, true) } - verify(rebuilder, times(1)).rebuildWithExistingReplies(entry) + verify(rebuilder, times(1)).rebuildForRemoteInputReply(entry) } @Test @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR) fun testLifetimeExtensionListenerClearsRemoteInputs() { // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag. - val entry = NotificationEntryBuilder() + val entry = + NotificationEntryBuilder() .setId(3) .setTag("entry") .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, false) @@ -245,9 +246,7 @@ class RemoteInputCoordinatorTest : SysuiTestCase() { `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false) `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false) - collectionListeners.forEach { - it.onEntryUpdated(entry, true) - } + collectionListeners.forEach { it.onEntryUpdated(entry, true) } assertThat(entry.remoteInputs).isNull() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt index cea8857c01bf..7d5278ed1601 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt @@ -332,7 +332,10 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) // AND the lock screen is shown - keyguardTransitionRepository.emitInitialStepsFromOff(to = KeyguardState.LOCKSCREEN) + keyguardTransitionRepository.emitInitialStepsFromOff( + to = KeyguardState.LOCKSCREEN, + testSetup = true, + ) assertThat(showHeadsUpStatusBar).isFalse() } @@ -345,7 +348,10 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) // AND the lock screen is shown - keyguardTransitionRepository.emitInitialStepsFromOff(to = KeyguardState.LOCKSCREEN) + keyguardTransitionRepository.emitInitialStepsFromOff( + to = KeyguardState.LOCKSCREEN, + testSetup = true, + ) // AND bypass is enabled faceAuthRepository.isBypassEnabled.value = true @@ -359,7 +365,10 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // WHEN no pinned rows // AND the lock screen is shown - keyguardTransitionRepository.emitInitialStepsFromOff(to = KeyguardState.LOCKSCREEN) + keyguardTransitionRepository.emitInitialStepsFromOff( + to = KeyguardState.LOCKSCREEN, + testSetup = true, + ) // AND bypass is enabled faceAuthRepository.isBypassEnabled.value = true diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt index 83ad18b6468b..46f3a6b66429 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt @@ -38,6 +38,7 @@ import com.android.systemui.shade.shadeTestUtil import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRepository import com.android.systemui.statusbar.notification.collection.render.NotifStats import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository +import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.testKosmos import com.android.systemui.util.ui.isAnimating @@ -254,6 +255,39 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(buttonLabel).isEqualTo(R.string.manage_notifications_history_text) } + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME) + @Test + fun manageButtonOnClick_whenHistoryDisabled() = + testScope.runTest { + val onClick by collectLastValue(underTest.manageOrHistoryButtonClick) + runCurrent() + + // WHEN notification history is disabled + fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0) + + // THEN onClick leads to settings page + assertThat(onClick?.targetIntent?.action) + .isEqualTo(Settings.ACTION_NOTIFICATION_SETTINGS) + assertThat(onClick?.backStack).isEmpty() + } + + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME) + @Test + fun historyButtonOnClick_whenHistoryEnabled() = + testScope.runTest { + val onClick by collectLastValue(underTest.manageOrHistoryButtonClick) + runCurrent() + + // WHEN notification history is enabled + fakeSecureSettingsRepository.setInt(Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 1) + + // THEN onClick leads to history page + assertThat(onClick?.targetIntent?.action) + .isEqualTo(Settings.ACTION_NOTIFICATION_HISTORY) + assertThat(onClick?.backStack?.map { it.action }) + .containsExactly(Settings.ACTION_NOTIFICATION_SETTINGS) + } + @Test fun manageButtonVisible_whenMessageVisible() = testScope.runTest { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt index e396b567ac89..0598b87aec9d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt @@ -133,7 +133,10 @@ class KeyguardStatusBarViewModelTest(flags: FlagsParameterization) : SysuiTestCa // WHEN HUN displayed on the bypass lock screen headsUpRepository.setNotifications(FakeHeadsUpRowRepository("key 0", isPinned = true)) - keyguardTransitionRepository.emitInitialStepsFromOff(KeyguardState.LOCKSCREEN) + keyguardTransitionRepository.emitInitialStepsFromOff( + KeyguardState.LOCKSCREEN, + testSetup = true, + ) kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen) faceAuthRepository.isBypassEnabled.value = true 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 a73c184a1ba8..4d0e603aadd6 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 @@ -48,9 +48,8 @@ import kotlinx.coroutines.test.runCurrent * with OFF -> GONE. Construct with initInLockscreen = false if your test requires this behavior. */ @SysUISingleton -class FakeKeyguardTransitionRepository( - private val initInLockscreen: Boolean = true, -) : KeyguardTransitionRepository { +class FakeKeyguardTransitionRepository(private val initInLockscreen: Boolean = true) : + KeyguardTransitionRepository { private val _transitions = MutableSharedFlow<TransitionStep>(replay = 3, onBufferOverflow = BufferOverflow.DROP_OLDEST) override val transitions: SharedFlow<TransitionStep> = _transitions @@ -63,7 +62,7 @@ class FakeKeyguardTransitionRepository( ownerName = "", from = KeyguardState.OFF, to = KeyguardState.LOCKSCREEN, - animator = null + animator = null, ) ) override var currentTransitionInfoInternal = _currentTransitionInfo.asStateFlow() @@ -71,12 +70,7 @@ class FakeKeyguardTransitionRepository( init { // Seed with a FINISHED transition in OFF, same as the real repository. _transitions.tryEmit( - TransitionStep( - KeyguardState.OFF, - KeyguardState.OFF, - 1f, - TransitionState.FINISHED, - ) + TransitionStep(KeyguardState.OFF, KeyguardState.OFF, 1f, TransitionState.FINISHED) ) if (initInLockscreen) { @@ -173,7 +167,7 @@ class FakeKeyguardTransitionRepository( transitionState = TransitionState.RUNNING, from = from, to = to, - value = 0.5f + value = 0.5f, ) ) testScheduler.runCurrent() @@ -184,7 +178,7 @@ class FakeKeyguardTransitionRepository( transitionState = TransitionState.RUNNING, from = from, to = to, - value = 1f + value = 1f, ) ) testScheduler.runCurrent() @@ -208,7 +202,7 @@ class FakeKeyguardTransitionRepository( this.sendTransitionStep( step = step, validateStep = validateStep, - ownerName = step.ownerName + ownerName = step.ownerName, ) } @@ -240,9 +234,9 @@ class FakeKeyguardTransitionRepository( to = to, value = value, transitionState = transitionState, - ownerName = ownerName + ownerName = ownerName, ), - validateStep: Boolean = true + validateStep: Boolean = true, ) { if (step.transitionState == TransitionState.STARTED) { _currentTransitionInfo.value = @@ -273,7 +267,7 @@ class FakeKeyguardTransitionRepository( fun sendTransitionStepJava( coroutineScope: CoroutineScope, step: TransitionStep, - validateStep: Boolean = true + validateStep: Boolean = true, ): Job { return coroutineScope.launch { sendTransitionStep(step = step, validateStep = validateStep) @@ -283,7 +277,7 @@ class FakeKeyguardTransitionRepository( suspend fun sendTransitionSteps( steps: List<TransitionStep>, testScope: TestScope, - validateSteps: Boolean = true + validateSteps: Boolean = true, ) { steps.forEach { sendTransitionStep(step = it, validateStep = validateSteps) @@ -296,7 +290,7 @@ class FakeKeyguardTransitionRepository( return if (info.animator == null) UUID.randomUUID() else null } - override suspend fun emitInitialStepsFromOff(to: KeyguardState) { + override suspend fun emitInitialStepsFromOff(to: KeyguardState, testSetup: Boolean) { tryEmitInitialStepsFromOff(to) } @@ -318,14 +312,14 @@ class FakeKeyguardTransitionRepository( 1f, TransitionState.FINISHED, ownerName = "KeyguardTransitionRepository(boot)", - ), + ) ) } override suspend fun updateTransition( transitionId: UUID, @FloatRange(from = 0.0, to = 1.0) value: Float, - state: TransitionState + state: TransitionState, ) = Unit } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt index e2b283b06562..2f13ba4e4966 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt @@ -34,11 +34,11 @@ val Kosmos.keyguardDismissActionInteractor by transitionInteractor = keyguardTransitionInteractor, dismissInteractor = keyguardDismissInteractor, applicationScope = testScope.backgroundScope, - sceneInteractor = { sceneInteractor }, deviceUnlockedInteractor = { deviceUnlockedInteractor }, powerInteractor = powerInteractor, alternateBouncerInteractor = alternateBouncerInteractor, shadeInteractor = { shadeInteractor }, keyguardInteractor = { keyguardInteractor }, + sceneInteractor = { sceneInteractor }, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt index 0c538ff1d6fe..ab7ccb3bc029 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.viewmodel import android.os.fakeExecutorHandler import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos val Kosmos.keyguardBlueprintViewModel by @@ -25,5 +26,6 @@ val Kosmos.keyguardBlueprintViewModel by KeyguardBlueprintViewModel( fakeExecutorHandler, keyguardBlueprintInteractor, + keyguardTransitionInteractor, ) } 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 38626a5dbac3..3c87106bf5aa 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 @@ -47,6 +47,8 @@ val Kosmos.keyguardRootViewModel by Fixture { alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel, alternateBouncerToLockscreenTransitionViewModel = alternateBouncerToLockscreenTransitionViewModel, + alternateBouncerToOccludedTransitionViewModel = + alternateBouncerToOccludedTransitionViewModel, aodToGoneTransitionViewModel = aodToGoneTransitionViewModel, aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel, aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel, @@ -69,9 +71,12 @@ val Kosmos.keyguardRootViewModel by Fixture { lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel, lockscreenToPrimaryBouncerTransitionViewModel = lockscreenToPrimaryBouncerTransitionViewModel, + occludedToAlternateBouncerTransitionViewModel = + occludedToAlternateBouncerTransitionViewModel, occludedToAodTransitionViewModel = occludedToAodTransitionViewModel, occludedToDozingTransitionViewModel = occludedToDozingTransitionViewModel, occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel, + offToLockscreenTransitionViewModel = offToLockscreenTransitionViewModel, primaryBouncerToAodTransitionViewModel = primaryBouncerToAodTransitionViewModel, primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel, primaryBouncerToLockscreenTransitionViewModel = diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModelKosmos.kt new file mode 100644 index 000000000000..2acd1b40af3e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToAlternateBouncerTransitionViewModelKosmos.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import kotlinx.coroutines.ExperimentalCoroutinesApi + +val Kosmos.occludedToAlternateBouncerTransitionViewModel by Fixture { + OccludedToAlternateBouncerTransitionViewModel(animationFlow = keyguardTransitionAnimationFlow) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelKosmos.kt new file mode 100644 index 000000000000..5d62a0f4a0cf --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModelKosmos.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import kotlinx.coroutines.ExperimentalCoroutinesApi + +val Kosmos.offToLockscreenTransitionViewModel by Fixture { + OffToLockscreenTransitionViewModel(animationFlow = keyguardTransitionAnimationFlow) +} 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 d37d8f39b9ee..dbb3e386cc71 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 @@ -19,14 +19,15 @@ package com.android.systemui.qs.composefragment.viewmodel import android.content.res.mainResources import androidx.lifecycle.LifecycleCoroutineScope import com.android.systemui.common.ui.domain.interactor.configurationInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.kosmos.Kosmos 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.ui.viewmodel.quickSettingsContainerViewModel import com.android.systemui.shade.largeScreenHeaderHelper import com.android.systemui.shade.transition.largeScreenShadeInterpolator import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository -import com.android.systemui.statusbar.phone.keyguardBypassController import com.android.systemui.statusbar.sysuiStatusBarStateController val Kosmos.qsFragmentComposeViewModelFactory by @@ -41,11 +42,12 @@ val Kosmos.qsFragmentComposeViewModelFactory by footerActionsViewModelFactory, footerActionsController, sysuiStatusBarStateController, - keyguardBypassController, + deviceEntryInteractor, disableFlagsRepository, largeScreenShadeInterpolator, configurationInteractor, largeScreenHeaderHelper, + tileSquishinessInteractor, lifecycleScope, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepositoryKosmos.kt new file mode 100644 index 000000000000..d9fad32aa924 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/TileSquishinessRepositoryKosmos.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.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.tileSquishinessRepository by Kosmos.Fixture { TileSquishinessRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt index 3f62b4d9f9cb..546129fe340e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt @@ -20,6 +20,9 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel +import com.android.systemui.qs.panels.ui.viewmodel.tileSquishinessViewModel val Kosmos.infiniteGridLayout by - Kosmos.Fixture { InfiniteGridLayout(iconTilesViewModel, fixedColumnsSizeViewModel) } + Kosmos.Fixture { + InfiniteGridLayout(iconTilesViewModel, fixedColumnsSizeViewModel, tileSquishinessViewModel) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractorKosmos.kt new file mode 100644 index 000000000000..23db70fad3a9 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/TileSquishinessInteractorKosmos.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.qs.panels.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.panels.data.repository.tileSquishinessRepository + +val Kosmos.tileSquishinessInteractor by + Kosmos.Fixture { TileSquishinessInteractor(tileSquishinessRepository) } 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 40d26242e36c..babbd50ece98 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 @@ -27,6 +27,7 @@ val Kosmos.quickQuickSettingsViewModel by currentTilesInteractor, fixedColumnsSizeViewModel, quickQuickSettingsRowInteractor, + tileSquishinessViewModel, iconTilesViewModel, applicationCoroutineScope, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModelKosmos.kt new file mode 100644 index 000000000000..ecc8cd179a9a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/TileSquishinessViewModelKosmos.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.qs.panels.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor + +val Kosmos.tileSquishinessViewModel by + Kosmos.Fixture { TileSquishinessViewModel(tileSquishinessInteractor) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt index ff8b478b368b..6540ed6bba45 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt @@ -18,11 +18,13 @@ package com.android.systemui.qs.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory val Kosmos.quickSettingsShadeOverlayContentViewModel: QuickSettingsShadeOverlayContentViewModel by Kosmos.Fixture { QuickSettingsShadeOverlayContentViewModel( + shadeInteractor = shadeInteractor, sceneInteractor = sceneInteractor, shadeHeaderViewModelFactory = shadeHeaderViewModelFactory, quickSettingsContainerViewModel = quickSettingsContainerViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt index 6d488d21301e..60141c60a265 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt @@ -25,6 +25,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.shade.data.repository.ShadeRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope @@ -133,7 +134,7 @@ interface ShadeTestUtilDelegate { class ShadeTestUtilLegacyImpl( val testScope: TestScope, val shadeRepository: FakeShadeRepository, - val context: SysuiTestableContext + val context: SysuiTestableContext, ) : ShadeTestUtilDelegate { override fun setShadeAndQsExpansion(shadeExpansion: Float, qsExpansion: Float) { shadeRepository.setLegacyShadeExpansion(shadeExpansion) @@ -191,6 +192,7 @@ class ShadeTestUtilLegacyImpl( } /** Sets up shade state for tests when the scene container flag is enabled. */ +@OptIn(ExperimentalCoroutinesApi::class) class ShadeTestUtilSceneImpl( val testScope: TestScope, val sceneInteractor: SceneInteractor, @@ -269,7 +271,7 @@ class ShadeTestUtilSceneImpl( from: SceneKey, to: SceneKey, progress: Float, - isInitiatedByUserInput: Boolean = true + isInitiatedByUserInput: Boolean = true, ) { sceneInteractor.changeScene(from, "test") val transitionState = diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt index 9cdd51994262..718347fc3490 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt @@ -20,6 +20,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModelFactory val Kosmos.notificationsShadeOverlayContentViewModel: @@ -28,5 +29,6 @@ val Kosmos.notificationsShadeOverlayContentViewModel: shadeHeaderViewModelFactory = shadeHeaderViewModelFactory, notificationsPlaceholderViewModelFactory = notificationsPlaceholderViewModelFactory, sceneInteractor = sceneInteractor, + shadeInteractor = shadeInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt index 7eb9f3472482..f5b856df8835 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt @@ -20,7 +20,6 @@ import android.content.applicationContext import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.kosmos.Kosmos import com.android.systemui.plugins.activityStarter -import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.privacyChipInteractor import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor @@ -32,7 +31,6 @@ val Kosmos.shadeHeaderViewModel: ShadeHeaderViewModel by ShadeHeaderViewModel( context = applicationContext, activityStarter = activityStarter, - sceneInteractor = sceneInteractor, shadeInteractor = shadeInteractor, mobileIconsInteractor = mobileIconsInteractor, mobileIconsViewModel = mobileIconsViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt index b19e221d099c..3d2bd6cf49d4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt @@ -45,3 +45,17 @@ var Kosmos.lockScreenShowOnlyUnseenNotificationsSetting: Boolean UserHandle.USER_CURRENT, ) } + +var Kosmos.lockScreenNotificationMinimalismSetting: Boolean + get() = + fakeSettings.getIntForUser( + Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM, + UserHandle.USER_CURRENT, + ) == 1 + set(value) { + fakeSettings.putIntForUser( + Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM, + if (value) 1 else 0, + UserHandle.USER_CURRENT, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt new file mode 100644 index 000000000000..8fdb948e2d1d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.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.statusbar.notification.emptyshade.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.shared.notifications.domain.interactor.notificationSettingsInteractor +import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor +import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor + +val Kosmos.emptyShadeViewModel by + Kosmos.Fixture { + EmptyShadeViewModel( + applicationContext, + zenModeInteractor, + seenNotificationsInteractor, + notificationSettingsInteractor, + dumpManager, + ) + } + +val Kosmos.emptyShadeViewModelFactory: EmptyShadeViewModel.Factory by + Kosmos.Fixture { + object : EmptyShadeViewModel.Factory { + override fun create() = emptyShadeViewModel + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt index de8b3500a88a..c3bc744e09b0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt @@ -23,13 +23,12 @@ import com.android.systemui.kosmos.testDispatcher import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor -import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor +import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.emptyShadeViewModelFactory import com.android.systemui.statusbar.notification.footer.ui.viewmodel.footerViewModel import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.notificationShelfViewModel import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackInteractor import com.android.systemui.statusbar.policy.domain.interactor.userSetupInteractor -import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor import java.util.Optional val Kosmos.notificationListViewModel by Fixture { @@ -37,15 +36,14 @@ val Kosmos.notificationListViewModel by Fixture { notificationShelfViewModel, hideListViewModel, Optional.of(footerViewModel), + emptyShadeViewModelFactory, Optional.of(notificationListLoggerViewModel), activeNotificationsInteractor, notificationStackInteractor, headsUpNotificationInteractor, remoteInputInteractor, - seenNotificationsInteractor, shadeInteractor, userSetupInteractor, - zenModeInteractor, testDispatcher, dumpManager, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt index a9e117affefb..237f7e4c4dc8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt @@ -42,6 +42,7 @@ import com.android.systemui.keyguard.ui.viewmodel.lockscreenToPrimaryBouncerTran import com.android.systemui.keyguard.ui.viewmodel.occludedToAodTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.occludedToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.occludedToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.offToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.primaryBouncerToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.primaryBouncerToLockscreenTransitionViewModel import com.android.systemui.kosmos.Kosmos @@ -85,6 +86,7 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { occludedToAodTransitionViewModel = occludedToAodTransitionViewModel, occludedToGoneTransitionViewModel = occludedToGoneTransitionViewModel, occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel, + offToLockscreenTransitionViewModel = offToLockscreenTransitionViewModel, primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel, primaryBouncerToLockscreenTransitionViewModel = primaryBouncerToLockscreenTransitionViewModel, diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java index 428eb57f20bf..b4b871524291 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java @@ -84,10 +84,10 @@ public class RavenwoodTestStats { try { mOutputWriter = new PrintWriter(mOutputFile); } catch (IOException e) { - throw new RuntimeException("Failed to crete logfile. File=" + mOutputFile, e); + throw new RuntimeException("Failed to create logfile. File=" + mOutputFile, e); } - // Crete the "latest" symlink. + // Create the "latest" symlink. Path symlink = Paths.get(tmpdir, basename + "latest.csv"); try { if (Files.exists(symlink)) { @@ -96,7 +96,7 @@ public class RavenwoodTestStats { Files.createSymbolicLink(symlink, Paths.get(mOutputFile.getName())); } catch (IOException e) { - throw new RuntimeException("Failed to crete logfile. File=" + mOutputFile, e); + throw new RuntimeException("Failed to create logfile. File=" + mOutputFile, e); } Log.i(TAG, "Test result stats file: " + mOutputFile); diff --git a/ravenwood/runtime-jni/ravenwood_sysprop.cpp b/ravenwood/runtime-jni/ravenwood_sysprop.cpp index 4fb61b6c590d..aafc4268d782 100644 --- a/ravenwood/runtime-jni/ravenwood_sysprop.cpp +++ b/ravenwood/runtime-jni/ravenwood_sysprop.cpp @@ -56,7 +56,7 @@ static bool property_set(const char* key, const char* value) { if (key == nullptr || *key == '\0') return false; if (value == nullptr) value = ""; bool read_only = !strncmp(key, "ro.", 3); - if (!read_only && strlen(value) >= PROP_VALUE_MAX) return -1; + if (!read_only && strlen(value) >= PROP_VALUE_MAX) return false; std::lock_guard lock(g_properties_lock); auto [it, success] = g_properties.emplace(key, value); diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index 3224b27d5803..73b7b35ba9a7 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -165,16 +165,27 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ protected final AccessibilitySecurityPolicy mSecurityPolicy; protected final AccessibilityTrace mTrace; - // The attribution tag set by the service that is bound to this instance + /** The attribution tag set by the client that is bound to this instance */ protected String mAttributionTag; protected int mDisplayTypes = DISPLAY_TYPE_DEFAULT; - // The service that's bound to this instance. Whenever this value is non-null, this - // object is registered as a death recipient - IBinder mService; + /** + * Binder of the {@link #mClient}. + * + * <p>Whenever this value is non-null, it should be registered as a {@link + * IBinder.DeathRecipient} + */ + @Nullable IBinder mClientBinder; - IAccessibilityServiceClient mServiceInterface; + /** + * The accessibility client this class represents. + * + * <p>The client is in the application process, i.e., it's a client of system_server. Depending + * on the use case, the client can be an {@link AccessibilityService}, a {@code UiAutomation}, + * etc. + */ + @Nullable IAccessibilityServiceClient mClient; int mEventTypes; @@ -218,10 +229,10 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ int mGenericMotionEventSources; int mObservedMotionEventSources; - // the events pending events to be dispatched to this service + /** Pending events to be dispatched to the client */ final SparseArray<AccessibilityEvent> mPendingEvents = new SparseArray<>(); - /** Whether this service relies on its {@link AccessibilityCache} being up to date */ + /** Whether the client relies on its {@link AccessibilityCache} being up to date */ boolean mUsesAccessibilityCache = false; // Handler only for dispatching accessibility events since we use event @@ -230,7 +241,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ final SparseArray<IBinder> mOverlayWindowTokens = new SparseArray(); - // All the embedded accessibility overlays that have been added by this service. + /** All the embedded accessibility overlays that have been added by the client. */ private List<SurfaceControl> mOverlays = new ArrayList<>(); /** The timestamp of requesting to take screenshot in milliseconds */ @@ -274,7 +285,8 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ /** * Called back to notify system that the client has changed - * @param serviceInfoChanged True if the service's AccessibilityServiceInfo changed. + * + * @param serviceInfoChanged True if the client's AccessibilityServiceInfo changed. */ void onClientChangeLocked(boolean serviceInfoChanged); @@ -360,21 +372,22 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); mIPlatformCompat = IPlatformCompat.Stub.asInterface( ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); - mEventDispatchHandler = new Handler(mainHandler.getLooper()) { - @Override - public void handleMessage(Message message) { - final int eventType = message.what; - AccessibilityEvent event = (AccessibilityEvent) message.obj; - boolean serviceWantsEvent = message.arg1 != 0; - notifyAccessibilityEventInternal(eventType, event, serviceWantsEvent); - } - }; + mEventDispatchHandler = + new Handler(mainHandler.getLooper()) { + @Override + public void handleMessage(Message message) { + final int eventType = message.what; + AccessibilityEvent event = (AccessibilityEvent) message.obj; + boolean clientWantsEvent = message.arg1 != 0; + notifyAccessibilityEventInternal(eventType, event, clientWantsEvent); + } + }; setDynamicallyConfigurableProperties(accessibilityServiceInfo); } @Override public boolean onKeyEvent(KeyEvent keyEvent, int sequenceNumber) { - if (!mRequestFilterKeyEvents || (mServiceInterface == null)) { + if (!mRequestFilterKeyEvents || (mClient == null)) { return false; } if((mAccessibilityServiceInfo.getCapabilities() @@ -388,7 +401,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ if (svcClientTracingEnabled()) { logTraceSvcClient("onKeyEvent", keyEvent + ", " + sequenceNumber); } - mServiceInterface.onKeyEvent(keyEvent, sequenceNumber); + mClient.onKeyEvent(keyEvent, sequenceNumber); } catch (RemoteException e) { return false; } @@ -470,7 +483,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ } public boolean canReceiveEventsLocked() { - return (mEventTypes != 0 && mService != null); + return (mEventTypes != 0 && mClientBinder != null); } @RequiresNoPermission @@ -520,7 +533,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ final long identity = Binder.clearCallingIdentity(); try { synchronized (mLock) { - // If the XML manifest had data to configure the service its info + // If the XML manifest had data to configure the AccessibilityService, its info // should be already set. In such a case update only the dynamically // configurable properties. boolean oldRequestIme = mRequestImeApis; @@ -1733,40 +1746,40 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ try { // Clear the proxy in the other process so this // IAccessibilityServiceConnection can be garbage collected. - if (mServiceInterface != null) { + if (mClient != null) { if (svcClientTracingEnabled()) { logTraceSvcClient("init", "null, " + mId + ", null"); } - mServiceInterface.init(null, mId, null); + mClient.init(null, mId, null); } } catch (RemoteException re) { /* ignore */ } - if (mService != null) { + if (mClientBinder != null) { try { - mService.unlinkToDeath(this, 0); + mClientBinder.unlinkToDeath(this, 0); } catch (NoSuchElementException e) { Slog.e(LOG_TAG, "Failed unregistering death link"); } - mService = null; + mClientBinder = null; } - mServiceInterface = null; + mClient = null; mReceivedAccessibilityButtonCallbackSinceBind = false; } public boolean isConnectedLocked() { - return (mService != null); + return (mClientBinder != null); } public void notifyAccessibilityEvent(AccessibilityEvent event) { synchronized (mLock) { final int eventType = event.getEventType(); - final boolean serviceWantsEvent = wantsEventLocked(event); + final boolean clientWantsEvent = clientWantsEventLocked(event); final boolean requiredForCacheConsistency = mUsesAccessibilityCache && ((AccessibilityCache.CACHE_CRITICAL_EVENTS_MASK & eventType) != 0); - if (!serviceWantsEvent && !requiredForCacheConsistency) { + if (!clientWantsEvent && !requiredForCacheConsistency) { return; } @@ -1774,7 +1787,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ return; } // Make a copy since during dispatch it is possible the event to - // be modified to remove its source if the receiving service does + // be modified to remove its source if the receiving client does // not have permission to access the window content. AccessibilityEvent newEvent = AccessibilityEvent.obtain(event); Message message; @@ -1792,22 +1805,20 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ // Send all messages, bypassing mPendingEvents message = mEventDispatchHandler.obtainMessage(eventType, newEvent); } - message.arg1 = serviceWantsEvent ? 1 : 0; + message.arg1 = clientWantsEvent ? 1 : 0; mEventDispatchHandler.sendMessageDelayed(message, mNotificationTimeout); } } /** - * Determines if given event can be dispatched to a service based on the package of the - * event source. Specifically, a service is notified if it is interested in events from the - * package. + * Determines if given event can be dispatched to a client based on the package of the event + * source. Specifically, a client is notified if it is interested in events from the package. * * @param event The event. - * @return True if the listener should be notified, false otherwise. + * @return True if the client should be notified, false otherwise. */ - private boolean wantsEventLocked(AccessibilityEvent event) { - + private boolean clientWantsEventLocked(AccessibilityEvent event) { if (!canReceiveEventsLocked()) { return false; } @@ -1838,22 +1849,20 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ } /** - * Notifies an accessibility service client for a scheduled event given the event type. + * Notifies a client for a scheduled event given the event type. * * @param eventType The type of the event to dispatch. */ private void notifyAccessibilityEventInternal( - int eventType, - AccessibilityEvent event, - boolean serviceWantsEvent) { - IAccessibilityServiceClient listener; + int eventType, AccessibilityEvent event, boolean clientWantsEvent) { + IAccessibilityServiceClient client; synchronized (mLock) { - listener = mServiceInterface; + client = mClient; - // If the service died/was disabled while the message for dispatching - // the accessibility event was propagating the listener may be null. - if (listener == null) { + // If the client (in the application process) died/was disabled while the message for + // dispatching the accessibility event was propagating, "client" may be null. + if (client == null) { return; } @@ -1868,7 +1877,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ // 1) A binder thread calls notifyAccessibilityServiceDelayedLocked // which posts a message for dispatching an event and stores the event // in mPendingEvents. - // 2) The message is pulled from the queue by the handler on the service + // 2) The message is pulled from the queue by the handler on the client // thread and this method is just about to acquire the lock. // 3) Another binder thread acquires the lock in notifyAccessibilityEvent // 4) notifyAccessibilityEvent recycles the event that this method was about @@ -1876,7 +1885,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ // 5) This method grabs the new event, processes it, and removes it from // mPendingEvents // 6) The second message dispatched in (4) arrives, but the event has been - // remvoved in (5). + // removed in (5). event = mPendingEvents.get(eventType); if (event == null) { return; @@ -1893,14 +1902,14 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ try { if (svcClientTracingEnabled()) { - logTraceSvcClient("onAccessibilityEvent", event + ";" + serviceWantsEvent); + logTraceSvcClient("onAccessibilityEvent", event + ";" + clientWantsEvent); } - listener.onAccessibilityEvent(event, serviceWantsEvent); + client.onAccessibilityEvent(event, clientWantsEvent); if (DEBUG) { - Slog.i(LOG_TAG, "Event " + event + " sent to " + listener); + Slog.i(LOG_TAG, "Event " + event + " sent to " + client); } } catch (RemoteException re) { - Slog.e(LOG_TAG, "Error during sending " + event + " to " + listener, re); + Slog.e(LOG_TAG, "Error during sending " + event + " to " + client, re); } finally { event.recycle(); } @@ -1978,122 +1987,126 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ return (mGenericMotionEventSources & eventSourceWithoutClass) != 0; } - /** - * Called by the invocation handler to notify the service that the - * state of magnification has changed. + * Called by the invocation handler to notify the client that the state of magnification has + * changed. */ - private void notifyMagnificationChangedInternal(int displayId, @NonNull Region region, - @NonNull MagnificationConfig config) { - final IAccessibilityServiceClient listener = getServiceInterfaceSafely(); - if (listener != null) { + private void notifyMagnificationChangedInternal( + int displayId, @NonNull Region region, @NonNull MagnificationConfig config) { + final IAccessibilityServiceClient client = getClientSafely(); + if (client != null) { try { if (svcClientTracingEnabled()) { logTraceSvcClient("onMagnificationChanged", displayId + ", " + region + ", " + config.toString()); } - listener.onMagnificationChanged(displayId, region, config); + client.onMagnificationChanged(displayId, region, config); } catch (RemoteException re) { - Slog.e(LOG_TAG, "Error sending magnification changes to " + mService, re); + Slog.e(LOG_TAG, "Error sending magnification changes to " + mClientBinder, re); } } } /** - * Called by the invocation handler to notify the service that the state of the soft - * keyboard show mode has changed. + * Called by the invocation handler to notify the client that the state of the soft keyboard + * show mode has changed. */ private void notifySoftKeyboardShowModeChangedInternal(int showState) { - final IAccessibilityServiceClient listener = getServiceInterfaceSafely(); - if (listener != null) { + final IAccessibilityServiceClient client = getClientSafely(); + if (client != null) { try { if (svcClientTracingEnabled()) { logTraceSvcClient("onSoftKeyboardShowModeChanged", String.valueOf(showState)); } - listener.onSoftKeyboardShowModeChanged(showState); + client.onSoftKeyboardShowModeChanged(showState); } catch (RemoteException re) { - Slog.e(LOG_TAG, "Error sending soft keyboard show mode changes to " + mService, + Slog.e( + LOG_TAG, + "Error sending soft keyboard show mode changes to " + mClientBinder, re); } } } private void notifyAccessibilityButtonClickedInternal(int displayId) { - final IAccessibilityServiceClient listener = getServiceInterfaceSafely(); - if (listener != null) { + final IAccessibilityServiceClient client = getClientSafely(); + if (client != null) { try { if (svcClientTracingEnabled()) { logTraceSvcClient("onAccessibilityButtonClicked", String.valueOf(displayId)); } - listener.onAccessibilityButtonClicked(displayId); + client.onAccessibilityButtonClicked(displayId); } catch (RemoteException re) { - Slog.e(LOG_TAG, "Error sending accessibility button click to " + mService, re); + Slog.e(LOG_TAG, "Error sending accessibility button click to " + mClientBinder, re); } } } private void notifyAccessibilityButtonAvailabilityChangedInternal(boolean available) { - // Only notify the service if it's not been notified or the state has changed + // Only notify the client if it's not been notified or the state has changed if (mReceivedAccessibilityButtonCallbackSinceBind && (mLastAccessibilityButtonCallbackState == available)) { return; } mReceivedAccessibilityButtonCallbackSinceBind = true; mLastAccessibilityButtonCallbackState = available; - final IAccessibilityServiceClient listener = getServiceInterfaceSafely(); - if (listener != null) { + final IAccessibilityServiceClient client = getClientSafely(); + if (client != null) { try { if (svcClientTracingEnabled()) { logTraceSvcClient("onAccessibilityButtonAvailabilityChanged", String.valueOf(available)); } - listener.onAccessibilityButtonAvailabilityChanged(available); + client.onAccessibilityButtonAvailabilityChanged(available); } catch (RemoteException re) { - Slog.e(LOG_TAG, - "Error sending accessibility button availability change to " + mService, + Slog.e( + LOG_TAG, + "Error sending accessibility button availability change to " + + mClientBinder, re); } } } private void notifyGestureInternal(AccessibilityGestureEvent gestureInfo) { - final IAccessibilityServiceClient listener = getServiceInterfaceSafely(); - if (listener != null) { + final IAccessibilityServiceClient client = getClientSafely(); + if (client != null) { try { if (svcClientTracingEnabled()) { logTraceSvcClient("onGesture", gestureInfo.toString()); } - listener.onGesture(gestureInfo); + client.onGesture(gestureInfo); } catch (RemoteException re) { - Slog.e(LOG_TAG, "Error during sending gesture " + gestureInfo - + " to " + mService, re); + Slog.e( + LOG_TAG, + "Error during sending gesture " + gestureInfo + " to " + mClientBinder, + re); } } } private void notifySystemActionsChangedInternal() { - final IAccessibilityServiceClient listener = getServiceInterfaceSafely(); - if (listener != null) { + final IAccessibilityServiceClient client = getClientSafely(); + if (client != null) { try { if (svcClientTracingEnabled()) { logTraceSvcClient("onSystemActionsChanged", ""); } - listener.onSystemActionsChanged(); + client.onSystemActionsChanged(); } catch (RemoteException re) { - Slog.e(LOG_TAG, "Error sending system actions change to " + mService, - re); + Slog.e(LOG_TAG, "Error sending system actions change to " + mClientBinder, re); } } } private void notifyClearAccessibilityCacheInternal() { - final IAccessibilityServiceClient listener = getServiceInterfaceSafely(); - if (listener != null) { + final IAccessibilityServiceClient client = getClientSafely(); + if (client != null) { try { if (svcClientTracingEnabled()) { logTraceSvcClient("clearAccessibilityCache", ""); } - listener.clearAccessibilityCache(); + client.clearAccessibilityCache(); } catch (RemoteException re) { Slog.e(LOG_TAG, "Error during requesting accessibility info cache" + " to be cleared.", re); @@ -2106,70 +2119,66 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ private void setImeSessionEnabledInternal(IAccessibilityInputMethodSession session, boolean enabled) { - final IAccessibilityServiceClient listener = getServiceInterfaceSafely(); - if (listener != null && session != null) { + final IAccessibilityServiceClient client = getClientSafely(); + if (client != null && session != null) { try { if (svcClientTracingEnabled()) { logTraceSvcClient("createImeSession", ""); } - listener.setImeSessionEnabled(session, enabled); + client.setImeSessionEnabled(session, enabled); } catch (RemoteException re) { - Slog.e(LOG_TAG, - "Error requesting IME session from " + mService, re); + Slog.e(LOG_TAG, "Error requesting IME session from " + mClientBinder, re); } } } private void bindInputInternal() { - final IAccessibilityServiceClient listener = getServiceInterfaceSafely(); - if (listener != null) { + final IAccessibilityServiceClient client = getClientSafely(); + if (client != null) { try { if (svcClientTracingEnabled()) { logTraceSvcClient("bindInput", ""); } - listener.bindInput(); + client.bindInput(); } catch (RemoteException re) { - Slog.e(LOG_TAG, - "Error binding input to " + mService, re); + Slog.e(LOG_TAG, "Error binding input to " + mClientBinder, re); } } } private void unbindInputInternal() { - final IAccessibilityServiceClient listener = getServiceInterfaceSafely(); - if (listener != null) { + final IAccessibilityServiceClient client = getClientSafely(); + if (client != null) { try { if (svcClientTracingEnabled()) { logTraceSvcClient("unbindInput", ""); } - listener.unbindInput(); + client.unbindInput(); } catch (RemoteException re) { - Slog.e(LOG_TAG, - "Error unbinding input to " + mService, re); + Slog.e(LOG_TAG, "Error unbinding input to " + mClientBinder, re); } } } private void startInputInternal(IRemoteAccessibilityInputConnection connection, EditorInfo editorInfo, boolean restarting) { - final IAccessibilityServiceClient listener = getServiceInterfaceSafely(); - if (listener != null) { + final IAccessibilityServiceClient client = getClientSafely(); + if (client != null) { try { if (svcClientTracingEnabled()) { logTraceSvcClient("startInput", "editorInfo=" + editorInfo + " restarting=" + restarting); } - listener.startInput(connection, editorInfo, restarting); + client.startInput(connection, editorInfo, restarting); } catch (RemoteException re) { - Slog.e(LOG_TAG, - "Error starting input to " + mService, re); + Slog.e(LOG_TAG, "Error starting input to " + mClientBinder, re); } } } - protected IAccessibilityServiceClient getServiceInterfaceSafely() { + protected IAccessibilityServiceClient getClientSafely() { synchronized (mLock) { - return mServiceInterface; + return mClient; } } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 7580b697b516..d595d02016e0 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -1435,8 +1435,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub interfacesToInterrupt = new ArrayList<>(services.size()); for (int i = 0; i < services.size(); i++) { AccessibilityServiceConnection service = services.get(i); - IBinder a11yServiceBinder = service.mService; - IAccessibilityServiceClient a11yServiceInterface = service.mServiceInterface; + IBinder a11yServiceBinder = service.mClientBinder; + IAccessibilityServiceClient a11yServiceInterface = service.mClient; if ((a11yServiceBinder != null) && (a11yServiceInterface != null)) { interfacesToInterrupt.add(a11yServiceInterface); } @@ -4962,9 +4962,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled() && android.security.Flags.extendEcmToAllSettings()) { try { - return !mContext.getSystemService(EnhancedConfirmationManager.class) - .isRestricted(packageName, - AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE); + final EnhancedConfirmationManager userContextEcm = + mContext.createContextAsUser(UserHandle.of(userId), /* flags = */ 0) + .getSystemService(EnhancedConfirmationManager.class); + if (userContextEcm != null) { + return !userContextEcm.isRestricted(packageName, + AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE); + } + return false; } catch (PackageManager.NameNotFoundException e) { Log.e(LOG_TAG, "Exception when retrieving package:" + packageName, e); return false; diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java index 786d167af5de..15999d19ebc0 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java @@ -166,8 +166,9 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect if (userState.getBindInstantServiceAllowedLocked()) { flags |= Context.BIND_ALLOW_INSTANT; } - if (mService == null && mContext.bindServiceAsUser( - mIntent, this, flags, new UserHandle(userState.mUserId))) { + if (mClientBinder == null + && mContext.bindServiceAsUser( + mIntent, this, flags, new UserHandle(userState.mUserId))) { userState.getBindingServicesLocked().add(mComponentName); } } finally { @@ -227,20 +228,20 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect addWindowTokensForAllDisplays(); } synchronized (mLock) { - if (mService != service) { - if (mService != null) { - mService.unlinkToDeath(this, 0); + if (mClientBinder != service) { + if (mClientBinder != null) { + mClientBinder.unlinkToDeath(this, 0); } - mService = service; + mClientBinder = service; try { - mService.linkToDeath(this, 0); + mClientBinder.linkToDeath(this, 0); } catch (RemoteException re) { Slog.e(LOG_TAG, "Failed registering death link"); binderDied(); return; } } - mServiceInterface = IAccessibilityServiceClient.Stub.asInterface(service); + mClient = IAccessibilityServiceClient.Stub.asInterface(service); if (userState == null) return; userState.addServiceLocked(this); mSystemSupport.onClientChangeLocked(false); @@ -261,7 +262,7 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect } private void initializeService() { - IAccessibilityServiceClient serviceInterface = null; + IAccessibilityServiceClient client = null; synchronized (mLock) { AccessibilityUserState userState = mUserStateWeakReference.get(); if (userState == null) return; @@ -272,18 +273,17 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect bindingServices.remove(mComponentName); crashedServices.remove(mComponentName); mAccessibilityServiceInfo.crashed = false; - serviceInterface = mServiceInterface; + client = mClient; } // There's a chance that service is removed from enabled_accessibility_services setting // key, but skip unbinding because of it's in binding state. Unbinds it if it's // not in enabled service list. - if (serviceInterface != null - && !userState.getEnabledServicesLocked().contains(mComponentName)) { + if (client != null && !userState.getEnabledServicesLocked().contains(mComponentName)) { mSystemSupport.onClientChangeLocked(false); return; } } - if (serviceInterface == null) { + if (client == null) { binderDied(); return; } @@ -292,10 +292,9 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect logTraceSvcClient("init", this + "," + mId + "," + mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY)); } - serviceInterface.init(this, mId, mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY)); + client.init(this, mId, mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY)); } catch (RemoteException re) { - Slog.w(LOG_TAG, "Error while setting connection for service: " - + serviceInterface, re); + Slog.w(LOG_TAG, "Error while setting connection for service: " + client, re); binderDied(); } } @@ -496,7 +495,7 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect @Override public boolean isCapturingFingerprintGestures() { - return (mServiceInterface != null) + return (mClient != null) && mSecurityPolicy.canCaptureFingerprintGestures(this) && mCaptureFingerprintGestures; } @@ -506,17 +505,17 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect if (!isCapturingFingerprintGestures()) { return; } - IAccessibilityServiceClient serviceInterface; + IAccessibilityServiceClient client; synchronized (mLock) { - serviceInterface = mServiceInterface; + client = mClient; } - if (serviceInterface != null) { + if (client != null) { try { if (svcClientTracingEnabled()) { logTraceSvcClient( "onFingerprintCapturingGesturesChanged", String.valueOf(active)); } - mServiceInterface.onFingerprintCapturingGesturesChanged(active); + mClient.onFingerprintCapturingGesturesChanged(active); } catch (RemoteException e) { } } @@ -527,16 +526,16 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect if (!isCapturingFingerprintGestures()) { return; } - IAccessibilityServiceClient serviceInterface; + IAccessibilityServiceClient client; synchronized (mLock) { - serviceInterface = mServiceInterface; + client = mClient; } - if (serviceInterface != null) { + if (client != null) { try { if (svcClientTracingEnabled()) { logTraceSvcClient("onFingerprintGesture", String.valueOf(gesture)); } - mServiceInterface.onFingerprintGesture(gesture); + mClient.onFingerprintGesture(gesture); } catch (RemoteException e) { } } @@ -546,7 +545,7 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect @Override public void dispatchGesture(int sequence, ParceledListSlice gestureSteps, int displayId) { synchronized (mLock) { - if (mServiceInterface != null && mSecurityPolicy.canPerformGestures(this)) { + if (mClient != null && mSecurityPolicy.canPerformGestures(this)) { final long identity = Binder.clearCallingIdentity(); try { MotionEventInjector motionEventInjector = @@ -557,16 +556,18 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect if (motionEventInjector != null && mWindowManagerService.isTouchOrFaketouchDevice()) { motionEventInjector.injectEvents( - gestureSteps.getList(), mServiceInterface, sequence, displayId); + gestureSteps.getList(), mClient, sequence, displayId); } else { try { if (svcClientTracingEnabled()) { logTraceSvcClient("onPerformGestureResult", sequence + ", false"); } - mServiceInterface.onPerformGestureResult(sequence, false); + mClient.onPerformGestureResult(sequence, false); } catch (RemoteException re) { - Slog.e(LOG_TAG, "Error sending motion event injection failure to " - + mServiceInterface, re); + Slog.e( + LOG_TAG, + "Error sending motion event injection failure to " + mClient, + re); } } } finally { @@ -631,48 +632,47 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect @Override protected void createImeSessionInternal() { - final IAccessibilityServiceClient listener = getServiceInterfaceSafely(); - if (listener != null) { + final IAccessibilityServiceClient client = getClientSafely(); + if (client != null) { try { if (svcClientTracingEnabled()) { logTraceSvcClient("createImeSession", ""); } AccessibilityInputMethodSessionCallback callback = new AccessibilityInputMethodSessionCallback(mUserId); - listener.createImeSession(callback); + client.createImeSession(callback); } catch (RemoteException re) { - Slog.e(LOG_TAG, - "Error requesting IME session from " + mService, re); + Slog.e(LOG_TAG, "Error requesting IME session from " + mClientBinder, re); } } } private void notifyMotionEventInternal(MotionEvent event) { - final IAccessibilityServiceClient listener = getServiceInterfaceSafely(); - if (listener != null) { + final IAccessibilityServiceClient client = getClientSafely(); + if (client != null) { try { if (mTrace.isA11yTracingEnabled()) { logTraceSvcClient(".onMotionEvent ", event.toString()); } - listener.onMotionEvent(event); + client.onMotionEvent(event); } catch (RemoteException re) { - Slog.e(LOG_TAG, "Error sending motion event to" + mService, re); + Slog.e(LOG_TAG, "Error sending motion event to" + mClientBinder, re); } } } private void notifyTouchStateInternal(int displayId, int state) { - final IAccessibilityServiceClient listener = getServiceInterfaceSafely(); - if (listener != null) { + final IAccessibilityServiceClient client = getClientSafely(); + if (client != null) { try { if (mTrace.isA11yTracingEnabled()) { logTraceSvcClient(".onTouchStateChanged ", TouchInteractionController.stateToString(state)); } - listener.onTouchStateChanged(displayId, state); + client.onTouchStateChanged(displayId, state); } catch (RemoteException re) { - Slog.e(LOG_TAG, "Error sending motion event to" + mService, re); + Slog.e(LOG_TAG, "Error sending motion event to" + mClientBinder, re); } } } diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java index 4cb3d247edb0..cd97d838e3a0 100644 --- a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java @@ -109,14 +109,11 @@ public class ProxyAccessibilityServiceConnection extends AccessibilityServiceCon return mDeviceId; } - /** - * Called when the proxy is registered. - */ - void initializeServiceInterface(IAccessibilityServiceClient serviceInterface) - throws RemoteException { - mServiceInterface = serviceInterface; - mService = serviceInterface.asBinder(); - mServiceInterface.init(this, mId, this.mOverlayWindowTokens.get(mDisplayId)); + /** Called when the proxy is registered. */ + void initializeClient(IAccessibilityServiceClient client) throws RemoteException { + mClient = client; + mClientBinder = client.asBinder(); + mClient.init(this, mId, this.mOverlayWindowTokens.get(mDisplayId)); } /** diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java index b4deeb0a6872..da11a76d5282 100644 --- a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java +++ b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java @@ -214,7 +214,7 @@ public class ProxyManager { mA11yInputFilter.disableFeaturesForDisplayIfInstalled(displayId); } }); - connection.initializeServiceInterface(client); + connection.initializeClient(client); } private void registerVirtualDeviceListener() { @@ -561,8 +561,8 @@ public class ProxyManager { final ProxyAccessibilityServiceConnection proxy = mProxyA11yServiceConnections.valueAt(i); if (proxy != null && proxy.getDeviceId() == deviceId) { - final IBinder proxyBinder = proxy.mService; - final IAccessibilityServiceClient proxyInterface = proxy.mServiceInterface; + final IBinder proxyBinder = proxy.mClientBinder; + final IAccessibilityServiceClient proxyInterface = proxy.mClient; if ((proxyBinder != null) && (proxyInterface != null)) { interfaces.add(proxyInterface); } diff --git a/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java b/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java index f85d786f89c5..ed4eeb534412 100644 --- a/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java +++ b/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java @@ -107,8 +107,7 @@ class UiAutomationManager { Binder.getCallingUserHandle().getIdentifier()); if (mUiAutomationService != null) { throw new IllegalStateException( - "UiAutomationService " + mUiAutomationService.mServiceInterface - + "already registered!"); + "UiAutomationService " + mUiAutomationService.mClient + "already registered!"); } try { @@ -130,10 +129,9 @@ class UiAutomationManager { mainHandler, mLock, securityPolicy, systemSupport, trace, windowManagerInternal, systemActionPerformer, awm); mUiAutomationServiceOwner = owner; - mUiAutomationService.mServiceInterface = serviceClient; + mUiAutomationService.mClient = serviceClient; try { - mUiAutomationService.mServiceInterface.asBinder().linkToDeath(mUiAutomationService, - 0); + mUiAutomationService.mClient.asBinder().linkToDeath(mUiAutomationService, 0); } catch (RemoteException re) { Slog.e(LOG_TAG, "Failed registering death link: " + re); destroyUiAutomationService(); @@ -149,10 +147,10 @@ class UiAutomationManager { synchronized (mLock) { if (useAccessibility() && ((mUiAutomationService == null) - || (serviceClient == null) - || (mUiAutomationService.mServiceInterface == null) - || (serviceClient.asBinder() - != mUiAutomationService.mServiceInterface.asBinder()))) { + || (serviceClient == null) + || (mUiAutomationService.mClient == null) + || (serviceClient.asBinder() + != mUiAutomationService.mClient.asBinder()))) { throw new IllegalStateException("UiAutomationService " + serviceClient + " not registered!"); } @@ -230,8 +228,7 @@ class UiAutomationManager { private void destroyUiAutomationService() { synchronized (mLock) { if (mUiAutomationService != null) { - mUiAutomationService.mServiceInterface.asBinder().unlinkToDeath( - mUiAutomationService, 0); + mUiAutomationService.mClient.asBinder().unlinkToDeath(mUiAutomationService, 0); mUiAutomationService.onRemoved(); mUiAutomationService.resetLocked(); mUiAutomationService = null; @@ -271,40 +268,48 @@ class UiAutomationManager { void connectServiceUnknownThread() { // This needs to be done on the main thread - mMainHandler.post(() -> { - try { - final IAccessibilityServiceClient serviceInterface; - final UiAutomationService uiAutomationService; - synchronized (mLock) { - serviceInterface = mServiceInterface; - uiAutomationService = mUiAutomationService; - if (serviceInterface == null) { - mService = null; - } else { - mService = mServiceInterface.asBinder(); - mService.linkToDeath(this, 0); + mMainHandler.post( + () -> { + try { + final IAccessibilityServiceClient client; + final UiAutomationService uiAutomationService; + synchronized (mLock) { + client = mClient; + uiAutomationService = mUiAutomationService; + if (client == null) { + mClientBinder = null; + } else { + mClientBinder = mClient.asBinder(); + mClientBinder.linkToDeath(this, 0); + } + } + // If the client is null, the UiAutomation has been shut down on + // another thread. + if (client != null && uiAutomationService != null) { + uiAutomationService.addWindowTokensForAllDisplays(); + if (mTrace.isA11yTracingEnabledForTypes( + AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT)) { + mTrace.logTrace( + "UiAutomationService.connectServiceUnknownThread", + AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT, + "serviceConnection=" + + this + + ";connectionId=" + + mId + + "windowToken=" + + mOverlayWindowTokens.get( + Display.DEFAULT_DISPLAY)); + } + client.init( + this, + mId, + mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY)); + } + } catch (RemoteException re) { + Slog.w(LOG_TAG, "Error initializing connection", re); + destroyUiAutomationService(); } - } - // If the serviceInterface is null, the UiAutomation has been shut down on - // another thread. - if (serviceInterface != null && uiAutomationService != null) { - uiAutomationService.addWindowTokensForAllDisplays(); - if (mTrace.isA11yTracingEnabledForTypes( - AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT)) { - mTrace.logTrace("UiAutomationService.connectServiceUnknownThread", - AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT, - "serviceConnection=" + this + ";connectionId=" + mId - + "windowToken=" - + mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY)); - } - serviceInterface.init(this, mId, - mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY)); - } - } catch (RemoteException re) { - Slog.w(LOG_TAG, "Error initializing connection", re); - destroyUiAutomationService(); - } - }); + }); } @Override diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java index c8f8c2a6b223..b6c8fc7b80c7 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java @@ -16,20 +16,35 @@ package com.android.server.appfunctions; +import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_DISABLED; +import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_ENABLED; +import static android.app.appfunctions.AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_METADATA_DB; +import static android.app.appfunctions.AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_NAMESPACE; + import static com.android.server.appfunctions.AppFunctionExecutors.THREAD_POOL_EXECUTOR; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.WorkerThread; +import android.app.appfunctions.AppFunctionManager; +import android.app.appfunctions.AppFunctionManagerHelper; +import android.app.appfunctions.AppFunctionRuntimeMetadata; import android.app.appfunctions.AppFunctionStaticMetadataHelper; import android.app.appfunctions.ExecuteAppFunctionAidlRequest; import android.app.appfunctions.ExecuteAppFunctionResponse; +import android.app.appfunctions.IAppFunctionEnabledCallback; import android.app.appfunctions.IAppFunctionManager; import android.app.appfunctions.IAppFunctionService; +import android.app.appfunctions.ICancellationCallback; import android.app.appfunctions.IExecuteAppFunctionCallback; import android.app.appfunctions.SafeOneTimeExecuteAppFunctionCallback; +import android.app.appsearch.AppSearchBatchResult; import android.app.appsearch.AppSearchManager; +import android.app.appsearch.AppSearchManager.SearchContext; import android.app.appsearch.AppSearchResult; +import android.app.appsearch.GenericDocument; +import android.app.appsearch.GetByDocumentIdRequest; +import android.app.appsearch.PutDocumentsRequest; import android.app.appsearch.observer.DocumentChangeInfo; import android.app.appsearch.observer.ObserverCallback; import android.app.appsearch.observer.ObserverSpec; @@ -37,17 +52,25 @@ import android.app.appsearch.observer.SchemaChangeInfo; import android.content.Context; import android.content.Intent; import android.os.Binder; +import android.os.CancellationSignal; +import android.os.ICancellationSignal; +import android.os.OutcomeReceiver; +import android.os.ParcelableException; +import android.os.RemoteException; import android.os.UserHandle; import android.text.TextUtils; import android.util.Slog; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.infra.AndroidFuture; import com.android.server.SystemService.TargetUser; import com.android.server.appfunctions.RemoteServiceCaller.RunServiceCallCallback; import com.android.server.appfunctions.RemoteServiceCaller.ServiceUsageCompleteListener; import java.util.Objects; import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; /** Implementation of the AppFunctionManagerService. */ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { @@ -58,6 +81,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { private final ServiceHelper mInternalServiceHelper; private final ServiceConfig mServiceConfig; private final Context mContext; + private final Object mLock = new Object(); public AppFunctionManagerServiceImpl(@NonNull Context context) { this( @@ -99,7 +123,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { } @Override - public void executeAppFunction( + public ICancellationSignal executeAppFunction( @NonNull ExecuteAppFunctionAidlRequest requestInternal, @NonNull IExecuteAppFunctionCallback executeAppFunctionCallback) { Objects.requireNonNull(requestInternal); @@ -120,11 +144,14 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { ExecuteAppFunctionResponse.RESULT_DENIED, exception.getMessage(), /* extras= */ null)); - return; + return null; } int callingUid = Binder.getCallingUid(); - int callingPid = Binder.getCallingUid(); + int callingPid = Binder.getCallingPid(); + + ICancellationSignal localCancelTransport = CancellationSignal.createTransport(); + THREAD_POOL_EXECUTOR.execute( () -> { try { @@ -132,12 +159,14 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { requestInternal, callingUid, callingPid, + localCancelTransport, safeExecuteAppFunctionCallback); } catch (Exception e) { safeExecuteAppFunctionCallback.onResult( mapExceptionToExecuteAppFunctionResponse(e)); } }); + return localCancelTransport; } @WorkerThread @@ -145,6 +174,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { ExecuteAppFunctionAidlRequest requestInternal, int callingUid, int callingPid, + ICancellationSignal localCancelTransport, SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback) { UserHandle targetUser = requestInternal.getUserHandle(); // TODO(b/354956319): Add and honor the new enterprise policies. @@ -168,59 +198,233 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { return; } - var unused = - mCallerValidator - .verifyCallerCanExecuteAppFunction( - callingUid, - callingPid, - requestInternal.getCallingPackage(), - targetPackageName, - requestInternal.getClientRequest().getFunctionIdentifier()) - .thenAccept( - canExecute -> { - if (!canExecute) { - safeExecuteAppFunctionCallback.onResult( - ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_DENIED, - "Caller does not have permission to execute" - + " the appfunction", - /* extras= */ null)); - return; - } - Intent serviceIntent = - mInternalServiceHelper.resolveAppFunctionService( - targetPackageName, targetUser); - if (serviceIntent == null) { - safeExecuteAppFunctionCallback.onResult( - ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse - .RESULT_INTERNAL_ERROR, - "Cannot find the target service.", - /* extras= */ null)); - return; - } - bindAppFunctionServiceUnchecked( - requestInternal, - serviceIntent, - targetUser, - safeExecuteAppFunctionCallback, - /* bindFlags= */ Context.BIND_AUTO_CREATE - | Context.BIND_FOREGROUND_SERVICE); - }) - .exceptionally( - ex -> { - safeExecuteAppFunctionCallback.onResult( - mapExceptionToExecuteAppFunctionResponse(ex)); - return null; - }); + mCallerValidator + .verifyCallerCanExecuteAppFunction( + callingUid, + callingPid, + requestInternal.getCallingPackage(), + targetPackageName, + requestInternal.getClientRequest().getFunctionIdentifier()) + .thenAccept( + canExecute -> { + if (!canExecute) { + safeExecuteAppFunctionCallback.onResult( + ExecuteAppFunctionResponse.newFailure( + ExecuteAppFunctionResponse.RESULT_DENIED, + "Caller does not have permission to execute the" + + " appfunction", + /* extras= */ null)); + } + }) + .thenCompose( + isEnabled -> + isAppFunctionEnabled( + requestInternal.getClientRequest().getFunctionIdentifier(), + requestInternal.getClientRequest().getTargetPackageName(), + getAppSearchManagerAsUser(requestInternal.getUserHandle()), + THREAD_POOL_EXECUTOR)) + .thenAccept( + isEnabled -> { + if (!isEnabled) { + throw new DisabledAppFunctionException( + "The app function is disabled"); + } + }) + .thenAccept( + unused -> { + Intent serviceIntent = + mInternalServiceHelper.resolveAppFunctionService( + targetPackageName, targetUser); + if (serviceIntent == null) { + safeExecuteAppFunctionCallback.onResult( + ExecuteAppFunctionResponse.newFailure( + ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR, + "Cannot find the target service.", + /* extras= */ null)); + return; + } + bindAppFunctionServiceUnchecked( + requestInternal, + serviceIntent, + targetUser, + localCancelTransport, + safeExecuteAppFunctionCallback, + /* bindFlags= */ Context.BIND_AUTO_CREATE + | Context.BIND_FOREGROUND_SERVICE); + }) + .exceptionally( + ex -> { + safeExecuteAppFunctionCallback.onResult( + mapExceptionToExecuteAppFunctionResponse(ex)); + return null; + }); + } + + private static AndroidFuture<Boolean> isAppFunctionEnabled( + @NonNull String functionIdentifier, + @NonNull String targetPackage, + @NonNull AppSearchManager appSearchManager, + @NonNull Executor executor) { + AndroidFuture<Boolean> future = new AndroidFuture<>(); + AppFunctionManagerHelper.isAppFunctionEnabled( + functionIdentifier, + targetPackage, + appSearchManager, + executor, + new OutcomeReceiver<>() { + @Override + public void onResult(@NonNull Boolean result) { + future.complete(result); + } + + @Override + public void onError(@NonNull Exception error) { + future.completeExceptionally(error); + } + }); + return future; + } + + @Override + public void setAppFunctionEnabled( + @NonNull String callingPackage, + @NonNull String functionIdentifier, + @NonNull UserHandle userHandle, + @AppFunctionManager.EnabledState int enabledState, + @NonNull IAppFunctionEnabledCallback callback) { + try { + mCallerValidator.validateCallingPackage(callingPackage); + } catch (SecurityException e) { + reportException(callback, e); + return; + } + THREAD_POOL_EXECUTOR.execute( + () -> { + try { + // TODO(357551503): Instead of holding a global lock, hold a per-package + // lock. + synchronized (mLock) { + setAppFunctionEnabledInternalLocked( + callingPackage, functionIdentifier, userHandle, enabledState); + } + callback.onSuccess(); + } catch (Exception e) { + Slog.e(TAG, "Error in setAppFunctionEnabled: ", e); + reportException(callback, e); + } + }); + } + + private static void reportException( + @NonNull IAppFunctionEnabledCallback callback, @NonNull Exception exception) { + try { + callback.onError(new ParcelableException(exception)); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to report the exception", e); + } + } + + /** + * Sets the enabled status of a specified app function. + * <p> + * Required to hold a lock to call this function to avoid document changes during the process. + */ + @WorkerThread + @GuardedBy("mLock") + private void setAppFunctionEnabledInternalLocked( + @NonNull String callingPackage, + @NonNull String functionIdentifier, + @NonNull UserHandle userHandle, + @AppFunctionManager.EnabledState int enabledState) + throws Exception { + AppSearchManager perUserAppSearchManager = getAppSearchManagerAsUser(userHandle); + + if (perUserAppSearchManager == null) { + throw new IllegalStateException( + "AppSearchManager not found for user:" + userHandle.getIdentifier()); + } + SearchContext runtimeMetadataSearchContext = + new SearchContext.Builder(APP_FUNCTION_RUNTIME_METADATA_DB).build(); + + try (FutureAppSearchSession runtimeMetadataSearchSession = + new FutureAppSearchSessionImpl( + perUserAppSearchManager, + THREAD_POOL_EXECUTOR, + runtimeMetadataSearchContext)) { + AppFunctionRuntimeMetadata existingMetadata = + new AppFunctionRuntimeMetadata( + getRuntimeMetadataGenericDocument( + callingPackage, + functionIdentifier, + runtimeMetadataSearchSession)); + AppFunctionRuntimeMetadata.Builder newMetadata = + new AppFunctionRuntimeMetadata.Builder(existingMetadata); + switch (enabledState) { + case AppFunctionManager.APP_FUNCTION_STATE_DEFAULT -> { + newMetadata.setEnabled(null); + } + case APP_FUNCTION_STATE_ENABLED -> { + newMetadata.setEnabled(true); + } + case APP_FUNCTION_STATE_DISABLED -> { + newMetadata.setEnabled(false); + } + default -> + throw new IllegalArgumentException( + "Value of EnabledState is unsupported."); + } + AppSearchBatchResult<String, Void> putDocumentBatchResult = + runtimeMetadataSearchSession + .put( + new PutDocumentsRequest.Builder() + .addGenericDocuments(newMetadata.build()) + .build()) + .get(); + if (!putDocumentBatchResult.isSuccess()) { + throw new IllegalStateException("Failed writing updated doc to AppSearch due to " + + putDocumentBatchResult); + } + } + } + + @WorkerThread + @NonNull + private AppFunctionRuntimeMetadata getRuntimeMetadataGenericDocument( + @NonNull String packageName, + @NonNull String functionId, + @NonNull FutureAppSearchSession runtimeMetadataSearchSession) + throws Exception { + String documentId = + AppFunctionRuntimeMetadata.getDocumentIdForAppFunction(packageName, functionId); + GetByDocumentIdRequest request = + new GetByDocumentIdRequest.Builder(APP_FUNCTION_RUNTIME_NAMESPACE) + .addIds(documentId) + .build(); + AppSearchBatchResult<String, GenericDocument> result = + runtimeMetadataSearchSession.getByDocumentId(request).get(); + if (result.isSuccess()) { + return new AppFunctionRuntimeMetadata((result.getSuccesses().get(documentId))); + } + throw new IllegalArgumentException("Function " + functionId + " does not exist"); } private void bindAppFunctionServiceUnchecked( @NonNull ExecuteAppFunctionAidlRequest requestInternal, @NonNull Intent serviceIntent, @NonNull UserHandle targetUser, + @NonNull ICancellationSignal cancellationSignalTransport, @NonNull SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback, int bindFlags) { + CancellationSignal cancellationSignal = + CancellationSignal.fromTransport(cancellationSignalTransport); + ICancellationCallback cancellationCallback = + new ICancellationCallback.Stub() { + @Override + public void sendCancellationTransport( + @NonNull ICancellationSignal cancellationTransport) { + cancellationSignal.setRemote(cancellationTransport); + } + }; boolean bindServiceResult = mRemoteServiceCaller.runServiceCall( serviceIntent, @@ -236,6 +440,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { try { service.executeAppFunction( requestInternal.getClientRequest(), + cancellationCallback, new IExecuteAppFunctionCallback.Stub() { @Override public void onResult( @@ -277,24 +482,27 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { } } + private AppSearchManager getAppSearchManagerAsUser(@NonNull UserHandle userHandle) { + return mContext.createContextAsUser(userHandle, /* flags= */ 0) + .getSystemService(AppSearchManager.class); + } + private ExecuteAppFunctionResponse mapExceptionToExecuteAppFunctionResponse(Throwable e) { if (e instanceof CompletionException) { e = e.getCause(); } - - if (e instanceof AppSearchException) { - AppSearchException appSearchException = (AppSearchException) e; - return ExecuteAppFunctionResponse.newFailure( + int resultCode = ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR; + if (e instanceof AppSearchException appSearchException) { + resultCode = mapAppSearchResultFailureCodeToExecuteAppFunctionResponse( - appSearchException.getResultCode()), - appSearchException.getMessage(), - /* extras= */ null); + appSearchException.getResultCode()); + } else if (e instanceof SecurityException) { + resultCode = ExecuteAppFunctionResponse.RESULT_DENIED; + } else if (e instanceof DisabledAppFunctionException) { + resultCode = ExecuteAppFunctionResponse.RESULT_DISABLED; } - return ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR, - e.getMessage(), - /* extras= */ null); + resultCode, e.getMessage(), /* extras= */ null); } private int mapAppSearchResultFailureCodeToExecuteAppFunctionResponse(int resultCode) { @@ -409,4 +617,11 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { } } } + + /** Throws when executing a disabled app function. */ + private static class DisabledAppFunctionException extends RuntimeException { + private DisabledAppFunctionException(@NonNull String errorMessage) { + super(errorMessage); + } + } } diff --git a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java index 070a99d5bb28..ffca8491abcd 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java @@ -65,8 +65,7 @@ public class RemoteServiceCallerImpl<T> implements RemoteServiceCaller<T> { @NonNull UserHandle userHandle, @NonNull RunServiceCallCallback<T> callback) { OneOffServiceConnection serviceConnection = - new OneOffServiceConnection( - intent, bindFlags, userHandle, callback); + new OneOffServiceConnection(intent, bindFlags, userHandle, callback); return serviceConnection.bindAndRun(); } @@ -93,7 +92,7 @@ public class RemoteServiceCallerImpl<T> implements RemoteServiceCaller<T> { boolean bindServiceResult = mContext.bindServiceAsUser(mIntent, this, mFlags, mUserHandle); - if(!bindServiceResult) { + if (!bindServiceResult) { safeUnbind(); } diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index b2c679faed5d..f6ac706c4985 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -1444,7 +1444,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku * in {@link #allocateAppWidgetId}. * * @param callingPackage The package that calls this method. - * @param appWidgetId The id of theapp widget to bind. + * @param appWidgetId The id of the widget to bind. * @param providerProfileId The user/profile id of the provider. * @param providerComponent The {@link ComponentName} that provides the widget. * @param options The options to pass to the provider. @@ -1738,6 +1738,14 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku return false; } + /** + * Called by a {@link AppWidgetHost} to remove all records (i.e. {@link Host} + * and all {@link Widget} associated with the host) from a specified host. + * + * @param callingPackage The package that calls this method. + * @param hostId id of the {@link Host}. + * @see AppWidgetHost#deleteHost() + */ @Override public void deleteHost(String callingPackage, int hostId) { final int userId = UserHandle.getCallingUserId(); @@ -1771,6 +1779,15 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * Called by a host process to remove all records (i.e. {@link Host} + * and all {@link Widget} associated with the host) from all hosts associated + * with the calling process. + * + * Typically used in clean up after test execution. + * + * @see AppWidgetHost#deleteAllHosts() + */ @Override public void deleteAllHosts() { final int userId = UserHandle.getCallingUserId(); @@ -1805,6 +1822,18 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * Returns the {@link AppWidgetProviderInfo} for the specified AppWidget. + * + * Typically used by launcher during the restore of an AppWidget, the binding + * of new AppWidget, and during grid size migration. + * + * @param callingPackage The package that calls this method. + * @param appWidgetId Id of the widget. + * @return The {@link AppWidgetProviderInfo} for the specified widget. + * + * @see AppWidgetManager#getAppWidgetInfo(int) + */ @Override public AppWidgetProviderInfo getAppWidgetInfo(String callingPackage, int appWidgetId) { final int userId = UserHandle.getCallingUserId(); @@ -1859,6 +1888,17 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * Returns the most recent {@link RemoteViews} of the specified AppWidget. + * Typically serves as a cache of the content of the AppWidget. + * + * @param callingPackage The package that calls this method. + * @param appWidgetId Id of the widget. + * @return The {@link RemoteViews} of the specified widget. + * + * @see AppWidgetHost#updateAppWidgetDeferred(String, int) + * @see AppWidgetHost#setListener(int, AppWidgetHostListener) + */ @Override public RemoteViews getAppWidgetViews(String callingPackage, int appWidgetId) { final int userId = UserHandle.getCallingUserId(); @@ -1886,6 +1926,29 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * Update the extras for a given widget instance. + * <p> + * The extras can be used to embed additional information about this widget to be accessed + * by the associated widget's AppWidgetProvider. + * + * <p> + * The new options are merged into existing options using {@link Bundle#putAll} semantics. + * + * <p> + * Typically called by a {@link AppWidgetHost} (e.g. Launcher) to notify + * {@link AppWidgetProvider} regarding contextual changes (e.g. sizes) when rendering the + * widget. + * Calling this method would trigger onAppWidgetOptionsChanged() callback on the provider's + * side. + * + * @param callingPackage The package that calls this method. + * @param appWidgetId Id of the widget. + * @param options New options associate with this widget. + * + * @see AppWidgetManager#getAppWidgetOptions(int, Bundle) + * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle) + */ @Override public void updateAppWidgetOptions(String callingPackage, int appWidgetId, Bundle options) { final int userId = UserHandle.getCallingUserId(); @@ -1919,6 +1982,21 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * Get the extras associated with a given widget instance. + * <p> + * The extras can be used to embed additional information about this widget to be accessed + * by the associated widget's AppWidgetProvider. + * + * Typically called by a host process (e.g. Launcher) to determine if they need to update the + * options of the widget. + * + * @see #updateAppWidgetOptions(String, int, Bundle) + * + * @param callingPackage The package that calls this method. + * @param appWidgetId Id of the widget. + * @return The options associated with the specified widget instance. + */ @Override public Bundle getAppWidgetOptions(String callingPackage, int appWidgetId) { final int userId = UserHandle.getCallingUserId(); @@ -1946,6 +2024,28 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * Updates the content of the widgets (as specified by appWidgetIds) using the provided + * {@link RemoteViews}. + * + * Typically called by the provider's process. Either in response to the invocation of + * {@link AppWidgetProvider#onUpdate} or upon receiving the + * {@link AppWidgetManager#ACTION_APPWIDGET_UPDATE} broadcast. + * + * <p> + * Note that the RemoteViews parameter will be cached by the AppWidgetService, and hence should + * contain a complete representation of the widget. For performing partial widget updates, see + * {@link #partiallyUpdateAppWidgetIds(String, int[], RemoteViews)}. + * + * @param callingPackage The package that calls this method. + * @param appWidgetIds Ids of the widgets to be updated. + * @param views The RemoteViews object containing the update. + * + * @see AppWidgetProvider#onUpdate(Context, AppWidgetManager, int[]) + * @see AppWidgetManager#ACTION_APPWIDGET_UPDATE + * @see AppWidgetManager#updateAppWidget(int, RemoteViews) + * @see AppWidgetManager#updateAppWidget(int[], RemoteViews) + */ @Override public void updateAppWidgetIds(String callingPackage, int[] appWidgetIds, RemoteViews views) { @@ -1956,6 +2056,27 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku updateAppWidgetIds(callingPackage, appWidgetIds, views, false); } + /** + * Perform an incremental update or command on the widget(s) specified by appWidgetIds. + * <p> + * This update differs from {@link #updateAppWidgetIds(int[], RemoteViews)} in that the + * RemoteViews object which is passed is understood to be an incomplete representation of the + * widget, and hence does not replace the cached representation of the widget. As of API + * level 17, the new properties set within the views objects will be appended to the cached + * representation of the widget, and hence will persist. + * + * <p> + * This method will be ignored if a widget has not received a full update via + * {@link #updateAppWidget(int[], RemoteViews)}. + * + * @param callingPackage The package that calls this method. + * @param appWidgetIds Ids of the widgets to be updated. + * @param views The RemoteViews object containing the incremental update / command. + * + * @see AppWidgetManager#partiallyUpdateAppWidget(int[], RemoteViews) + * @see RemoteViews#setDisplayedChild(int, int) + * @see RemoteViews#setScrollPosition(int, int) + */ @Override public void partiallyUpdateAppWidgetIds(String callingPackage, int[] appWidgetIds, RemoteViews views) { @@ -1966,6 +2087,24 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku updateAppWidgetIds(callingPackage, appWidgetIds, views, true); } + /** + * Callback function which marks specified providers as extended from AppWidgetProvider. + * + * This information is used to determine if the system can combine + * {@link AppWidgetManager#ACTION_APPWIDGET_ENABLED} and + * {@link AppWidgetManager#ACTION_APPWIDGET_UPDATE} into a single broadcast. + * + * Note: The system can only combine the two broadcasts if the provider is extended from + * AppWidgetProvider. When they do, they are expected to override the + * {@link AppWidgetProvider#onUpdate} callback function to provide updates, as opposed to + * listening for {@link AppWidgetManager#ACTION_APPWIDGET_UPDATE} broadcasts directly. + * + * @see AppWidgetManager#ACTION_APPWIDGET_ENABLED + * @see AppWidgetManager#ACTION_APPWIDGET_UPDATE + * @see AppWidgetManager#ACTION_APPWIDGET_ENABLE_AND_UPDATE + * @see AppWidgetProvider#onReceive(Context, Intent) + * @see #sendEnableAndUpdateIntentLocked + */ @Override public void notifyProviderInheritance(@Nullable final ComponentName[] componentNames) { final int userId = UserHandle.getCallingUserId(); @@ -2000,6 +2139,15 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * Notifies the specified collection view in all the specified AppWidget instances + * to invalidate their data. + * + * This method is effectively deprecated since + * {@link RemoteViews#setRemoteAdapter(int, Intent)} has been deprecated. + * + * @see AppWidgetManager#notifyAppWidgetViewDataChanged(int[], int) + */ @Override public void notifyAppWidgetViewDataChanged(String callingPackage, int[] appWidgetIds, int viewId) { @@ -2035,6 +2183,18 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * Updates the content of all widgets associated with given provider (as specified by + * componentName) using the provided {@link RemoteViews}. + * + * Typically called by the provider's process when there's an update that needs to be supplied + * to all instances of the widgets. + * + * @param componentName The component name of the provider. + * @param views The RemoteViews object containing the update. + * + * @see AppWidgetManager#updateAppWidget(ComponentName, RemoteViews) + */ @Override public void updateAppWidgetProvider(ComponentName componentName, RemoteViews views) { final int userId = UserHandle.getCallingUserId(); @@ -2068,6 +2228,27 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * Updates the info for the supplied AppWidget provider. Apps can use this to change the default + * behavior of the widget based on the state of the app (e.g., if the user is logged in + * or not). Calling this API completely replaces the previous definition. + * + * <p> + * The manifest entry of the provider should contain an additional meta-data tag similar to + * {@link AppWidgetManager#META_DATA_APPWIDGET_PROVIDER} which should point to any alternative + * definitions for the provider. + * + * <p> + * This is persisted across device reboots and app updates. If this meta-data key is not + * present in the manifest entry, the info reverts to default. + * + * @param provider {@link ComponentName} for the {@link + * android.content.BroadcastReceiver BroadcastReceiver} provider for your AppWidget. + * @param metaDataKey key for the meta-data tag pointing to the new provider info. Use null + * to reset any previously set info. + * + * @see AppWidgetManager#updateAppWidgetProviderInfo(ComponentName, String) + */ @Override public void updateAppWidgetProviderInfo(ComponentName componentName, String metadataKey) { final int userId = UserHandle.getCallingUserId(); @@ -2119,6 +2300,11 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * Returns true if the default launcher app on the device (the one that currently + * holds the android.app.role.HOME role) can support pinning widgets + * (typically means adding widgets into home screen). + */ @Override public boolean isRequestPinAppWidgetSupported() { synchronized (mLock) { @@ -2133,6 +2319,44 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku LauncherApps.PinItemRequest.REQUEST_TYPE_APPWIDGET); } + /** + * Request to pin an app widget on the current launcher. It's up to the launcher to accept this + * request (optionally showing a user confirmation). If the request is accepted, the caller will + * get a confirmation with extra {@link #EXTRA_APPWIDGET_ID}. + * + * <p>When a request is denied by the user, the caller app will not get any response. + * + * <p>Only apps with a foreground activity or a foreground service can call it. Otherwise + * it'll throw {@link IllegalStateException}. + * + * <p>It's up to the launcher how to handle previous pending requests when the same package + * calls this API multiple times in a row. It may ignore the previous requests, + * for example. + * + * <p>Launcher will not show the configuration activity associated with the provider in this + * case. The app could either show the configuration activity as a response to the callback, + * or show if before calling the API (various configurations can be encapsulated in + * {@code successCallback} to avoid persisting them before the widgetId is known). + * + * @param provider The {@link ComponentName} for the {@link + * android.content.BroadcastReceiver BroadcastReceiver} provider for your AppWidget. + * @param extras If not null, this is passed to the launcher app. For eg {@link + * #EXTRA_APPWIDGET_PREVIEW} can be used for a custom preview. + * @param successCallback If not null, this intent will be sent when the widget is created. + * + * @return {@code TRUE} if the launcher supports this feature. Note the API will return without + * waiting for the user to respond, so getting {@code TRUE} from this API does *not* mean + * the shortcut is pinned. {@code FALSE} if the launcher doesn't support this feature or if + * calling app belongs to a user-profile with items restricted on home screen. + * + * @see android.content.pm.ShortcutManager#isRequestPinShortcutSupported() + * @see android.content.pm.ShortcutManager#requestPinShortcut(ShortcutInfo, IntentSender) + * @see AppWidgetManager#isRequestPinAppWidgetSupported() + * @see AppWidgetManager#requestPinAppWidget(ComponentName, Bundle, PendingIntent) + * + * @throws IllegalStateException The caller doesn't have a foreground activity or a foreground + * service or when the user is locked. + */ @Override public boolean requestPinAppWidget(String callingPackage, ComponentName componentName, Bundle extras, IntentSender resultSender) { @@ -2184,6 +2408,24 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku callingPid, callingUid) == PackageManager.PERMISSION_GRANTED; } + /** + * Gets the AppWidget providers for the given user profile. User profile can only + * be the current user or a profile of the current user. For example, the current + * user may have a corporate profile. In this case the parent user profile has a + * child profile, the corporate one. + * + * @param categoryFilter Will only return providers which register as any of the specified + * specified categories. See {@link AppWidgetProviderInfo#widgetCategory}. + * @param profile A profile of the current user which to be queried. The user + * is itself also a profile. If null, the providers only for the current user + * are returned. + * @param packageName If specified, will only return providers from the given package. + * @return The installed providers. + * + * @see android.os.Process#myUserHandle() + * @see android.os.UserManager#getUserProfiles() + * @see AppWidgetManager#getInstalledProvidersForProfile(int, UserHandle, String) + */ @Override public ParceledListSlice<AppWidgetProviderInfo> getInstalledProvidersForProfile(int categoryFilter, int profileId, String packageName) { @@ -2244,6 +2486,26 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * Updates the content of the widgets (as specified by appWidgetIds) using the provided + * {@link RemoteViews}. + * + * If performing a partial update, the given RemoteViews object is merged into existing + * RemoteViews object. + * + * Fails silently if appWidgetIds is null or empty, or cannot found a widget with the given + * appWidgetId. + * + * @param callingPackage The package that calls this method. + * @param appWidgetIds Ids of the widgets to be updated. + * @param views The RemoteViews object containing the update. + * @param partially Whether it was a partial update. + * + * @see AppWidgetProvider#onUpdate(Context, AppWidgetManager, int[]) + * @see AppWidgetManager#ACTION_APPWIDGET_UPDATE + * @see AppWidgetManager#updateAppWidget(int, RemoteViews) + * @see AppWidgetManager#updateAppWidget(int[], RemoteViews) + */ private void updateAppWidgetIds(String callingPackage, int[] appWidgetIds, RemoteViews views, boolean partially) { final int userId = UserHandle.getCallingUserId(); @@ -2273,12 +2535,29 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * Increment the counter of widget ids and return the new id. + * + * Typically called by {@link #allocateAppWidgetId} when a instance of widget is created, + * either as a result of being pinned by launcher or added during a restore. + * + * Note: A widget id is a monotonically increasing integer that uniquely identifies the widget + * instance. + * + * TODO: Revisit this method and determine whether we need to alter the widget id during + * the restore since widget id mismatch potentially leads to some issues in the past. + */ private int incrementAndGetAppWidgetIdLocked(int userId) { final int appWidgetId = peekNextAppWidgetIdLocked(userId) + 1; mNextAppWidgetIds.put(userId, appWidgetId); return appWidgetId; } + /** + * Called by {@link #readProfileStateFromFileLocked} when widgets/providers/hosts are loaded + * from disk, which ensures mNextAppWidgetIds is larger than any existing widget id for given + * user. + */ private void setMinAppWidgetIdLocked(int userId, int minWidgetId) { final int nextAppWidgetId = peekNextAppWidgetIdLocked(userId); if (nextAppWidgetId < minWidgetId) { diff --git a/services/core/java/android/os/BatteryStatsInternal.java b/services/core/java/android/os/BatteryStatsInternal.java index 0713999d4354..60b826b50045 100644 --- a/services/core/java/android/os/BatteryStatsInternal.java +++ b/services/core/java/android/os/BatteryStatsInternal.java @@ -41,6 +41,7 @@ public abstract class BatteryStatsInternal { public static final int CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER = 3; public static final int CPU_WAKEUP_SUBSYSTEM_SENSOR = 4; public static final int CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA = 5; + public static final int CPU_WAKEUP_SUBSYSTEM_BLUETOOTH = 6; /** @hide */ @IntDef(prefix = {"CPU_WAKEUP_SUBSYSTEM_"}, value = { @@ -50,6 +51,7 @@ public abstract class BatteryStatsInternal { CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER, CPU_WAKEUP_SUBSYSTEM_SENSOR, CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA, + CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, }) @Retention(RetentionPolicy.SOURCE) public @interface CpuWakeupSubsystem { @@ -99,6 +101,14 @@ public abstract class BatteryStatsInternal { public abstract void noteCpuWakingNetworkPacket(Network network, long elapsedMillis, int uid); /** + * Informs battery stats of a sysproxy packet that woke up the CPU + * + * @param uid The uid that received the packet. + * @param elapsedMillis The time of the packet's arrival in elapsed timebase. + */ + public abstract void noteCpuWakingBluetoothProxyPacket(int uid, long elapsedMillis); + + /** * Informs battery stats of binder stats for the given work source UID. */ public abstract void noteBinderCallStats(int workSourceUid, long incrementalBinderCallCount, diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags index 5b271a3730fc..7474df2a91ca 100644 --- a/services/core/java/com/android/server/EventLogTags.logtags +++ b/services/core/java/com/android/server/EventLogTags.logtags @@ -87,7 +87,7 @@ option java_package com.android.server # replaces 27510 with a row per notification 27531 notification_visibility (key|3),(visibile|1),(lifespan|1),(freshness|1),(exposure|1),(rank|1) # a notification emited noise, vibration, or light -27532 notification_alert (key|3),(buzz|1),(beep|1),(blink|1),(politeness|1) +27532 notification_alert (key|3),(buzz|1),(beep|1),(blink|1),(politeness|1),(mute_reason|1) # a notification was added to a autogroup 27533 notification_autogrouped (key|3) # notification was removed from an autogroup diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java index fbe593fd3df1..682eb768a23e 100644 --- a/services/core/java/com/android/server/PackageWatchdog.java +++ b/services/core/java/com/android/server/PackageWatchdog.java @@ -25,6 +25,7 @@ import static com.android.server.crashrecovery.CrashRecoveryUtils.dumpCrashRecov import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.BroadcastReceiver; import android.content.Context; @@ -91,6 +92,7 @@ import java.util.concurrent.TimeUnit; * Monitors the health of packages on the system and notifies interested observers when packages * fail. On failure, the registered observer with the least user impacting mitigation will * be notified. + * @hide */ public class PackageWatchdog { private static final String TAG = "PackageWatchdog"; @@ -108,13 +110,25 @@ public class PackageWatchdog { private static final long NUMBER_OF_NATIVE_CRASH_POLLS = 10; + /** Reason for package failure could not be determined. */ public static final int FAILURE_REASON_UNKNOWN = 0; + + /** The package had a native crash. */ public static final int FAILURE_REASON_NATIVE_CRASH = 1; + + /** The package failed an explicit health check. */ public static final int FAILURE_REASON_EXPLICIT_HEALTH_CHECK = 2; + + /** The app crashed. */ public static final int FAILURE_REASON_APP_CRASH = 3; + + /** The app was not responding. */ public static final int FAILURE_REASON_APP_NOT_RESPONDING = 4; + + /** The device was boot looping. */ public static final int FAILURE_REASON_BOOT_LOOP = 5; + /** @hide */ @IntDef(prefix = { "FAILURE_REASON_" }, value = { FAILURE_REASON_UNKNOWN, FAILURE_REASON_NATIVE_CRASH, @@ -186,7 +200,8 @@ public class PackageWatchdog { // aborted. private static final String METADATA_FILE = "/metadata/watchdog/mitigation_count.txt"; - @GuardedBy("PackageWatchdog.class") + private static final Object sPackageWatchdogLock = new Object(); + @GuardedBy("sPackageWatchdogLock") private static PackageWatchdog sPackageWatchdog; private final Object mLock = new Object(); @@ -278,8 +293,8 @@ public class PackageWatchdog { } /** Creates or gets singleton instance of PackageWatchdog. */ - public static PackageWatchdog getInstance(Context context) { - synchronized (PackageWatchdog.class) { + public static @NonNull PackageWatchdog getInstance(@NonNull Context context) { + synchronized (sPackageWatchdogLock) { if (sPackageWatchdog == null) { new PackageWatchdog(context); } @@ -290,6 +305,7 @@ public class PackageWatchdog { /** * Called during boot to notify when packages are ready on the device so we can start * binding. + * @hide */ public void onPackagesReady() { synchronized (mLock) { @@ -311,6 +327,7 @@ public class PackageWatchdog { * * <p>Observers are expected to call this on boot. It does not specify any packages but * it will resume observing any packages requested from a previous boot. + * @hide */ public void registerHealthObserver(PackageHealthObserver observer) { synchronized (mLock) { @@ -344,6 +361,7 @@ public class PackageWatchdog { * * <p>If {@code durationMs} is less than 1, a default monitoring duration * {@link #DEFAULT_OBSERVING_DURATION_MS} will be used. + * @hide */ public void startObservingHealth(PackageHealthObserver observer, List<String> packageNames, long durationMs) { @@ -407,6 +425,7 @@ public class PackageWatchdog { * Unregisters {@code observer} from listening to package failure. * Additionally, this stops observing any packages that may have previously been observed * even from a previous boot. + * @hide */ public void unregisterHealthObserver(PackageHealthObserver observer) { mLongTaskHandler.post(() -> { @@ -425,7 +444,7 @@ public class PackageWatchdog { * * <p>This method could be called frequently if there is a severe problem on the device. */ - public void onPackageFailure(List<VersionedPackage> packages, + public void onPackageFailure(@NonNull List<VersionedPackage> packages, @FailureReasons int failureReason) { if (packages == null) { Slog.w(TAG, "Could not resolve a list of failing packages"); @@ -566,6 +585,7 @@ public class PackageWatchdog { * * Note: PackageWatchdog considers system_server restart loop as bootloop. Full reboots * are not counted in bootloop. + * @hide */ @SuppressWarnings("GuardedBy") public void noteBoot() { @@ -620,7 +640,7 @@ public class PackageWatchdog { // TODO(b/120598832): Optimize write? Maybe only write a separate smaller file? Also // avoid holding lock? // This currently adds about 7ms extra to shutdown thread - /** Writes the package information to file during shutdown. */ + /** @hide Writes the package information to file during shutdown. */ public void writeNow() { synchronized (mLock) { // Must only run synchronous tasks as this runs on the ShutdownThread and no other @@ -674,6 +694,7 @@ public class PackageWatchdog { * Since this method can eventually trigger a rollback, it should be called * only once boot has completed {@code onBootCompleted} and not earlier, because the install * session must be entirely completed before we try to rollback. + * @hide */ public void scheduleCheckAndMitigateNativeCrashes() { Slog.i(TAG, "Scheduling " + mNumberOfNativeCrashPollsRemaining + " polls to check " @@ -695,7 +716,9 @@ public class PackageWatchdog { return mPackagesExemptFromImpactLevelThreshold; } - /** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}. */ + /** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}. + * @hide + */ @Retention(SOURCE) @IntDef(value = {PackageHealthObserverImpact.USER_IMPACT_LEVEL_0, PackageHealthObserverImpact.USER_IMPACT_LEVEL_10, @@ -787,7 +810,7 @@ public class PackageWatchdog { * Identifier for the observer, should not change across device updates otherwise the * watchdog may drop observing packages with the old name. */ - String getUniqueIdentifier(); + @NonNull String getUniqueIdentifier(); /** * An observer will not be pruned if this is set, even if the observer is not explicitly @@ -804,7 +827,7 @@ public class PackageWatchdog { * <p> A persistent observer may choose to start observing certain failing packages, even if * it has not explicitly asked to watch the package with {@link #startObservingHealth}. */ - default boolean mayObservePackage(String packageName) { + default boolean mayObservePackage(@NonNull String packageName) { return false; } } @@ -1240,7 +1263,7 @@ public class PackageWatchdog { } } - /** Convert a {@code LongArrayQueue} to a String of comma-separated values. */ + /** @hide Convert a {@code LongArrayQueue} to a String of comma-separated values. */ public static String longArrayQueueToString(LongArrayQueue queue) { if (queue.size() > 0) { StringBuilder sb = new StringBuilder(); @@ -1254,7 +1277,7 @@ public class PackageWatchdog { return ""; } - /** Parse a comma-separated String of longs into a LongArrayQueue. */ + /** @hide Parse a comma-separated String of longs into a LongArrayQueue. */ public static LongArrayQueue parseLongArrayQueue(String commaSeparatedValues) { LongArrayQueue result = new LongArrayQueue(); if (!TextUtils.isEmpty(commaSeparatedValues)) { @@ -1268,7 +1291,7 @@ public class PackageWatchdog { /** Dump status of every observer in mAllObservers. */ - public void dump(PrintWriter pw) { + public void dump(@NonNull PrintWriter pw) { IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); ipw.println("Package Watchdog status"); ipw.increaseIndent(); @@ -1395,6 +1418,7 @@ public class PackageWatchdog { /** * Increments failure counts of {@code packageName}. * @returns {@code true} if failure threshold is exceeded, {@code false} otherwise + * @hide */ @GuardedBy("mLock") public boolean onPackageFailureLocked(String packageName) { @@ -1514,6 +1538,7 @@ public class PackageWatchdog { } } + /** @hide */ @Retention(SOURCE) @IntDef(value = { HealthCheckState.ACTIVE, @@ -1603,7 +1628,9 @@ public class PackageWatchdog { updateHealthCheckStateLocked(); } - /** Writes the salient fields to disk using {@code out}. */ + /** Writes the salient fields to disk using {@code out}. + * @hide + */ @GuardedBy("mLock") public void writeLocked(TypedXmlSerializer out) throws IOException { out.startTag(null, TAG_PACKAGE); diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index d86bae19f174..e64a4803b14f 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -219,7 +219,7 @@ class StorageManagerService extends IStorageManager.Stub public static final int FAILED_MOUNT_RESET_TIMEOUT_SECONDS = 10; /** Extended timeout for the system server watchdog. */ - private static final int SLOW_OPERATION_WATCHDOG_TIMEOUT_MS = 20 * 1000; + private static final int SLOW_OPERATION_WATCHDOG_TIMEOUT_MS = 30 * 1000; /** Extended timeout for the system server watchdog for vold#partition operation. */ private static final int PARTITION_OPERATION_WATCHDOG_TIMEOUT_MS = 3 * 60 * 1000; @@ -3251,7 +3251,7 @@ class StorageManagerService extends IStorageManager.Stub if (Binder.getCallingUid() != android.os.Process.SYSTEM_UID) { throw new SecurityException("no permission to commit checkpoint changes"); } - + extendWatchdogTimeout("vold#commitChanges might be slow"); mVold.commitChanges(); } diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 88edb121c0c8..3499a3a5edde 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -1773,8 +1773,7 @@ public class AccountManagerService // Create a Session for the target user and pass in the bundle completeCloningAccount(response, result, account, toAccounts, userFrom); } else { - // Bundle format is not defined. - super.onResultSkipSanitization(result); + super.onResult(result); } } }.bind(); @@ -1861,8 +1860,7 @@ public class AccountManagerService // account to avoid retries? // TODO: what we do with the visibility? - // Bundle format is not defined. - super.onResultSkipSanitization(result); + super.onResult(result); } @Override @@ -2108,7 +2106,6 @@ public class AccountManagerService @Override public void onResult(Bundle result) { Bundle.setDefusable(result, true); - result = sanitizeBundle(result); IAccountManagerResponse response = getResponseAndClose(); if (response != null) { try { @@ -2462,7 +2459,6 @@ public class AccountManagerService @Override public void onResult(Bundle result) { Bundle.setDefusable(result, true); - result = sanitizeBundle(result); if (result != null && result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) && !result.containsKey(AccountManager.KEY_INTENT)) { final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT); @@ -2977,7 +2973,6 @@ public class AccountManagerService @Override public void onResult(Bundle result) { Bundle.setDefusable(result, true); - result = sanitizeBundle(result); if (result != null) { String label = result.getString(AccountManager.KEY_AUTH_TOKEN_LABEL); Bundle bundle = new Bundle(); @@ -3155,7 +3150,6 @@ public class AccountManagerService @Override public void onResult(Bundle result) { Bundle.setDefusable(result, true); - result = sanitizeBundle(result); if (result != null) { if (result.containsKey(AccountManager.KEY_AUTH_TOKEN_LABEL)) { Intent intent = newGrantCredentialsPermissionIntent( @@ -3627,12 +3621,6 @@ public class AccountManagerService @Override public void onResult(Bundle result) { Bundle.setDefusable(result, true); - Bundle sessionBundle = null; - if (result != null) { - // Session bundle will be removed from result. - sessionBundle = result.getBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE); - } - result = sanitizeBundle(result); mNumResults++; Intent intent = null; if (result != null) { @@ -3694,6 +3682,7 @@ public class AccountManagerService // bundle contains data necessary for finishing the session // later. The session bundle will be encrypted here and // decrypted later when trying to finish the session. + Bundle sessionBundle = result.getBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE); if (sessionBundle != null) { String accountType = sessionBundle.getString(AccountManager.KEY_ACCOUNT_TYPE); if (TextUtils.isEmpty(accountType) @@ -4081,7 +4070,6 @@ public class AccountManagerService @Override public void onResult(Bundle result) { Bundle.setDefusable(result, true); - result = sanitizeBundle(result); IAccountManagerResponse response = getResponseAndClose(); if (response == null) { return; @@ -4395,7 +4383,6 @@ public class AccountManagerService @Override public void onResult(Bundle result) { Bundle.setDefusable(result, true); - result = sanitizeBundle(result); mNumResults++; if (result == null) { onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "null bundle"); @@ -4952,68 +4939,6 @@ public class AccountManagerService callback, resultReceiver); } - - // All keys for Strings passed from AbstractAccountAuthenticator using Bundle. - private static final String[] sStringBundleKeys = new String[] { - AccountManager.KEY_ACCOUNT_NAME, - AccountManager.KEY_ACCOUNT_TYPE, - AccountManager.KEY_AUTHTOKEN, - AccountManager.KEY_AUTH_TOKEN_LABEL, - AccountManager.KEY_ERROR_MESSAGE, - AccountManager.KEY_PASSWORD, - AccountManager.KEY_ACCOUNT_STATUS_TOKEN}; - - /** - * Keep only documented fields in a Bundle received from AbstractAccountAuthenticator. - */ - protected static Bundle sanitizeBundle(Bundle bundle) { - if (bundle == null) { - return null; - } - Bundle sanitizedBundle = new Bundle(); - Bundle.setDefusable(sanitizedBundle, true); - int updatedKeysCount = 0; - for (String stringKey : sStringBundleKeys) { - if (bundle.containsKey(stringKey)) { - String value = bundle.getString(stringKey); - sanitizedBundle.putString(stringKey, value); - updatedKeysCount++; - } - } - String key = AbstractAccountAuthenticator.KEY_CUSTOM_TOKEN_EXPIRY; - if (bundle.containsKey(key)) { - long expiryMillis = bundle.getLong(key, 0L); - sanitizedBundle.putLong(key, expiryMillis); - updatedKeysCount++; - } - key = AccountManager.KEY_BOOLEAN_RESULT; - if (bundle.containsKey(key)) { - boolean booleanResult = bundle.getBoolean(key, false); - sanitizedBundle.putBoolean(key, booleanResult); - updatedKeysCount++; - } - key = AccountManager.KEY_ERROR_CODE; - if (bundle.containsKey(key)) { - int errorCode = bundle.getInt(key, 0); - sanitizedBundle.putInt(key, errorCode); - updatedKeysCount++; - } - key = AccountManager.KEY_INTENT; - if (bundle.containsKey(key)) { - Intent intent = bundle.getParcelable(key, Intent.class); - sanitizedBundle.putParcelable(key, intent); - updatedKeysCount++; - } - if (bundle.containsKey(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE)) { - // The field is not copied in sanitized bundle. - updatedKeysCount++; - } - if (updatedKeysCount != bundle.size()) { - Log.w(TAG, "Size mismatch after sanitizeBundle call."); - } - return sanitizedBundle; - } - private abstract class Session extends IAccountAuthenticatorResponse.Stub implements IBinder.DeathRecipient, ServiceConnection { private final Object mSessionLock = new Object(); @@ -5304,15 +5229,10 @@ public class AccountManagerService } } } + @Override public void onResult(Bundle result) { Bundle.setDefusable(result, true); - result = sanitizeBundle(result); - onResultSkipSanitization(result); - } - - public void onResultSkipSanitization(Bundle result) { - Bundle.setDefusable(result, true); mNumResults++; Intent intent = null; if (result != null) { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index ad72941595d7..35323d6cb391 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -262,6 +262,7 @@ import android.appwidget.AppWidgetManagerInternal; import android.content.AttributionSource; import android.content.AutofillOptions; import android.content.BroadcastReceiver; +import android.content.ClipData; import android.content.ComponentCallbacks2; import android.content.ComponentName; import android.content.ContentCaptureOptions; @@ -419,7 +420,6 @@ import com.android.internal.util.FastPrintWriter; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.MemInfoReader; import com.android.internal.util.Preconditions; -import com.android.server.crashrecovery.CrashRecoveryHelper; import com.android.server.AlarmManagerInternal; import com.android.server.BootReceiver; import com.android.server.DeviceIdleInternal; @@ -439,6 +439,7 @@ import com.android.server.am.LowMemDetector.MemFactor; import com.android.server.appop.AppOpsService; import com.android.server.compat.PlatformCompat; import com.android.server.contentcapture.ContentCaptureManagerInternal; +import com.android.server.crashrecovery.CrashRecoveryHelper; import com.android.server.criticalevents.CriticalEventLog; import com.android.server.firewall.IntentFirewall; import com.android.server.graphics.fonts.FontManagerInternal; @@ -483,6 +484,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; +import java.lang.ref.WeakReference; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; @@ -500,6 +502,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; @@ -19097,4 +19100,87 @@ public class ActivityManagerService extends IActivityManager.Stub Freezer getFreezer() { return mFreezer; } + + // Set of IntentCreatorToken objects that are currently active. + private static final Map<IntentCreatorToken.Key, WeakReference<IntentCreatorToken>> + sIntentCreatorTokenCache = new ConcurrentHashMap<>(); + + /** + * A binder token used to keep track of which app created the intent. This token can be used to + * defend against intent redirect attacks. It stores uid of the intent creator and key fields of + * the intent to make it impossible for attacker to fake uid with a malicious intent. + * + * @hide + */ + public static final class IntentCreatorToken extends Binder { + @NonNull + private final Key mKeyFields; + + public IntentCreatorToken(int creatorUid, Intent intent) { + super(); + this.mKeyFields = new Key(creatorUid, intent); + } + + public int getCreatorUid() { + return mKeyFields.mCreatorUid; + } + + /** {@hide} */ + public static boolean isValid(@NonNull Intent intent) { + IBinder binder = intent.getCreatorToken(); + IntentCreatorToken token = null; + if (binder instanceof IntentCreatorToken) { + token = (IntentCreatorToken) binder; + } + return token != null && token.mKeyFields.equals( + new Key(token.mKeyFields.mCreatorUid, intent)); + } + + private static class Key { + private Key(int creatorUid, Intent intent) { + this.mCreatorUid = creatorUid; + this.mAction = intent.getAction(); + this.mData = intent.getData(); + this.mType = intent.getType(); + this.mPackage = intent.getPackage(); + this.mComponent = intent.getComponent(); + this.mFlags = intent.getFlags() & Intent.IMMUTABLE_FLAGS; + ClipData clipData = intent.getClipData(); + if (clipData != null) { + this.mClipDataUris = new ArrayList<>(clipData.getItemCount()); + for (int i = 0; i < clipData.getItemCount(); i++) { + this.mClipDataUris.add(clipData.getItemAt(i).getUri()); + } + } + } + + private final int mCreatorUid; + private final String mAction; + private final Uri mData; + private final String mType; + private final String mPackage; + private final ComponentName mComponent; + private final int mFlags; + private List<Uri> mClipDataUris; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Key key = (Key) o; + return mCreatorUid == key.mCreatorUid && mFlags == key.mFlags && Objects.equals( + mAction, key.mAction) && Objects.equals(mData, key.mData) + && Objects.equals(mType, key.mType) && Objects.equals(mPackage, + key.mPackage) && Objects.equals(mComponent, key.mComponent) + && Objects.equals(mClipDataUris, key.mClipDataUris); + } + + @Override + public int hashCode() { + return Objects.hash(mCreatorUid, mAction, mData, mType, mPackage, mComponent, + mFlags, + mClipDataUris); + } + } + } } diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 15277cebac6e..ef82c7477558 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -664,6 +664,8 @@ public final class BatteryStatsService extends IBatteryStats.Stub } else if (nc.hasTransport(TRANSPORT_CELLULAR)) { return CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA; } + // For TRANSPORT_BLUETOOTH, we have a separate channel to catch Bluetooth wakeups. + // See noteCpuWakingSysproxyPacket method. return CPU_WAKEUP_SUBSYSTEM_UNKNOWN; } @@ -686,6 +688,15 @@ public final class BatteryStatsService extends IBatteryStats.Stub } @Override + public void noteCpuWakingBluetoothProxyPacket(int uid, long elapsedMillis) { + if (uid < 0) { + Slog.e(TAG, "Invalid uid for waking bluetooth proxy packet: " + uid); + return; + } + noteCpuWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, elapsedMillis, uid); + } + + @Override public void noteBinderCallStats(int workSourceUid, long incrementatCallCount, Collection<BinderCallsStats.CallStat> callStats) { synchronized (BatteryStatsService.this.mLock) { diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index a13ce654bb95..bae9a670c438 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -40,6 +40,7 @@ import android.aconfigd.Aconfigd.StorageRequestMessages; 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 java.io.DataInputStream; import java.io.DataOutputStream; @@ -491,14 +492,18 @@ public class SettingsToPropertiesMapper { static void writeFlagOverrideRequest( ProtoOutputStream proto, String packageName, String flagName, String flagValue, boolean isLocal) { + int localOverrideTag = supportImmediateLocalOverrides() + ? StorageRequestMessage.LOCAL_IMMEDIATE + : StorageRequestMessage.LOCAL_ON_REBOOT; + long msgsToken = proto.start(StorageRequestMessages.MSGS); long msgToken = proto.start(StorageRequestMessage.FLAG_OVERRIDE_MESSAGE); proto.write(StorageRequestMessage.FlagOverrideMessage.PACKAGE_NAME, packageName); proto.write(StorageRequestMessage.FlagOverrideMessage.FLAG_NAME, flagName); proto.write(StorageRequestMessage.FlagOverrideMessage.FLAG_VALUE, flagValue); proto.write(StorageRequestMessage.FlagOverrideMessage.OVERRIDE_TYPE, isLocal - ? StorageRequestMessage.LOCAL_ON_REBOOT - : StorageRequestMessage.SERVER_ON_REBOOT); + ? localOverrideTag + : StorageRequestMessage.SERVER_ON_REBOOT); proto.end(msgToken); proto.end(msgsToken); } diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index 9b51b6ae4b0f..4f6da3baca12 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -205,4 +205,12 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} + +flag { + name: "defer_display_events_when_frozen" + namespace: "system_performance" + is_fixed_read_only: true + description: "Defer submitting display events to frozen processes." + bug: "326315985" +} diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index a6389f7f5311..e0cf96fbccd0 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -1031,6 +1031,9 @@ public class AppOpsService extends IAppOpsService.Stub { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); + if (action == null) { + return; + } String pkgName = intent.getData().getEncodedSchemeSpecificPart().intern(); int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID); diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index feef5409d14f..4c917894100a 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -725,7 +725,7 @@ public class BiometricService extends SystemService { return -1; } - if (!Utils.isValidAuthenticatorConfig(promptInfo)) { + if (!Utils.isValidAuthenticatorConfig(getContext(), promptInfo)) { throw new SecurityException("Invalid authenticator configuration"); } @@ -763,7 +763,7 @@ public class BiometricService extends SystemService { + ", Caller=" + callingUserId + ", Authenticators=" + authenticators); - if (!Utils.isValidAuthenticatorConfig(authenticators)) { + if (!Utils.isValidAuthenticatorConfig(getContext(), authenticators)) { throw new SecurityException("Invalid authenticator configuration"); } @@ -1038,7 +1038,7 @@ public class BiometricService extends SystemService { + ", Caller=" + callingUserId + ", Authenticators=" + authenticators); - if (!Utils.isValidAuthenticatorConfig(authenticators)) { + if (!Utils.isValidAuthenticatorConfig(getContext(), authenticators)) { throw new SecurityException("Invalid authenticator configuration"); } @@ -1060,7 +1060,7 @@ public class BiometricService extends SystemService { Slog.d(TAG, "getSupportedModalities: Authenticators=" + authenticators); - if (!Utils.isValidAuthenticatorConfig(authenticators)) { + if (!Utils.isValidAuthenticatorConfig(getContext(), authenticators)) { throw new SecurityException("Invalid authenticator configuration"); } diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java index 407ef1e41aa6..de7bce78587b 100644 --- a/services/core/java/com/android/server/biometrics/Utils.java +++ b/services/core/java/com/android/server/biometrics/Utils.java @@ -16,6 +16,7 @@ package com.android.server.biometrics; +import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE; import static android.hardware.biometrics.BiometricManager.Authenticators; @@ -233,17 +234,18 @@ public class Utils { * @param promptInfo * @return */ - static boolean isValidAuthenticatorConfig(PromptInfo promptInfo) { + static boolean isValidAuthenticatorConfig(Context context, PromptInfo promptInfo) { final int authenticators = promptInfo.getAuthenticators(); - return isValidAuthenticatorConfig(authenticators); + return isValidAuthenticatorConfig(context, authenticators); } /** - * Checks if the authenticator configuration is a valid combination of the public APIs - * @param authenticators - * @return + * Checks if the authenticator configuration is a valid combination of the public APIs. + * + * throws {@link SecurityException} if the caller requests for mandatory biometrics without + * {@link SET_BIOMETRIC_DIALOG_ADVANCED} permission */ - static boolean isValidAuthenticatorConfig(int authenticators) { + static boolean isValidAuthenticatorConfig(Context context, int authenticators) { // The caller is not required to set the authenticators. But if they do, check the below. if (authenticators == 0) { return true; @@ -271,6 +273,9 @@ public class Utils { } else if (biometricBits == Authenticators.BIOMETRIC_WEAK) { return true; } else if (isMandatoryBiometricsRequested(authenticators)) { + //TODO(b/347123256): Update CTS test + context.enforceCallingOrSelfPermission(SET_BIOMETRIC_DIALOG_ADVANCED, + "Must have SET_BIOMETRIC_DIALOG_ADVANCED permission"); return true; } diff --git a/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java b/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java index 5f2fbcedce88..8a81aaa1e636 100644 --- a/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java +++ b/services/core/java/com/android/server/crashrecovery/CrashRecoveryModule.java @@ -23,7 +23,10 @@ import com.android.server.RescueParty; import com.android.server.SystemService; -/** This class encapsulate the lifecycle methods of CrashRecovery module. */ +/** This class encapsulate the lifecycle methods of CrashRecovery module. + * + * @hide + */ public class CrashRecoveryModule { private static final String TAG = "CrashRecoveryModule"; diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index dc263c5a6b32..af9c9accb635 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -1080,7 +1080,7 @@ public class DisplayDeviceConfig { */ public float[] getNits() { if (mEvenDimmerBrightnessData != null) { - return mEvenDimmerBrightnessData.mNits; + return mEvenDimmerBrightnessData.nits; } return mNits; } @@ -1093,7 +1093,7 @@ public class DisplayDeviceConfig { @VisibleForTesting public float[] getBacklight() { if (mEvenDimmerBrightnessData != null) { - return mEvenDimmerBrightnessData.mBacklight; + return mEvenDimmerBrightnessData.backlight; } return mBacklight; } @@ -1107,7 +1107,7 @@ public class DisplayDeviceConfig { */ public float getBacklightFromBrightness(float brightness) { if (mEvenDimmerBrightnessData != null) { - return mEvenDimmerBrightnessData.mBrightnessToBacklight.interpolate(brightness); + return mEvenDimmerBrightnessData.brightnessToBacklight.interpolate(brightness); } return mBrightnessToBacklightSpline.interpolate(brightness); } @@ -1120,7 +1120,7 @@ public class DisplayDeviceConfig { */ public float getBrightnessFromBacklight(float backlight) { if (mEvenDimmerBrightnessData != null) { - return mEvenDimmerBrightnessData.mBacklightToBrightness.interpolate(backlight); + return mEvenDimmerBrightnessData.backlightToBrightness.interpolate(backlight); } return mBacklightToBrightnessSpline.interpolate(backlight); } @@ -1131,7 +1131,7 @@ public class DisplayDeviceConfig { */ private Spline getBacklightToBrightnessSpline() { if (mEvenDimmerBrightnessData != null) { - return mEvenDimmerBrightnessData.mBacklightToBrightness; + return mEvenDimmerBrightnessData.backlightToBrightness; } return mBacklightToBrightnessSpline; } @@ -1144,11 +1144,11 @@ public class DisplayDeviceConfig { */ public float getNitsFromBacklight(float backlight) { if (mEvenDimmerBrightnessData != null) { - if (mEvenDimmerBrightnessData.mBacklightToNits == null) { + if (mEvenDimmerBrightnessData.backlightToNits == null) { return INVALID_NITS; } backlight = Math.max(backlight, mBacklightMinimum); - return mEvenDimmerBrightnessData.mBacklightToNits.interpolate(backlight); + return mEvenDimmerBrightnessData.backlightToNits.interpolate(backlight); } if (mBacklightToNitsSpline == null) { @@ -1165,14 +1165,14 @@ public class DisplayDeviceConfig { */ public float getBacklightFromNits(float nits) { if (mEvenDimmerBrightnessData != null) { - return mEvenDimmerBrightnessData.mNitsToBacklight.interpolate(nits); + return mEvenDimmerBrightnessData.nitsToBacklight.interpolate(nits); } return mNitsToBacklightSpline.interpolate(nits); } private Spline getNitsToBacklightSpline() { if (mEvenDimmerBrightnessData != null) { - return mEvenDimmerBrightnessData.mNitsToBacklight; + return mEvenDimmerBrightnessData.nitsToBacklight; } return mNitsToBacklightSpline; } @@ -1186,7 +1186,7 @@ public class DisplayDeviceConfig { if (mEvenDimmerBrightnessData == null) { return INVALID_NITS; } - return mEvenDimmerBrightnessData.mMinLuxToNits.interpolate(lux); + return mEvenDimmerBrightnessData.minLuxToNits.interpolate(lux); } /** @@ -1197,7 +1197,7 @@ public class DisplayDeviceConfig { if (mEvenDimmerBrightnessData == null) { return PowerManager.BRIGHTNESS_MIN; } - return mEvenDimmerBrightnessData.mTransitionPoint; + return mEvenDimmerBrightnessData.transitionPoint; } /** @@ -1268,7 +1268,7 @@ public class DisplayDeviceConfig { */ public float[] getBrightness() { if (mEvenDimmerBrightnessData != null) { - return mEvenDimmerBrightnessData.mBrightness; + return mEvenDimmerBrightnessData.brightness; } return mBrightness; } @@ -2617,13 +2617,13 @@ public class DisplayDeviceConfig { List<NonNegativeFloatToFloatPoint> points = map.getMap().getPoint(); for (NonNegativeFloatToFloatPoint point : points) { float lux = point.getFirst().floatValue(); - float maxBrightness = point.getSecond().floatValue(); - if (maxBrightness > hbmTransitionPoint) { + float maxBacklight = point.getSecond().floatValue(); + if (maxBacklight > hbmTransitionPoint) { Slog.wtf(TAG, - "Invalid NBM config: maxBrightness is greater than hbm" + "Invalid NBM config: maxBacklight is greater than hbm" + ".transitionPoint. type=" - + type + "; lux=" + lux + "; maxBrightness=" - + maxBrightness); + + type + "; lux=" + lux + "; maxBacklight=" + + maxBacklight); continue; } if (luxToTransitionPointMap.containsKey(lux)) { @@ -2632,8 +2632,7 @@ public class DisplayDeviceConfig { + lux); continue; } - luxToTransitionPointMap.put(lux, - getBrightnessFromBacklight(maxBrightness)); + luxToTransitionPointMap.put(lux, getBrightnessFromBacklight(maxBacklight)); } if (!luxToTransitionPointMap.isEmpty()) { mLuxThrottlingData.put(mappedType, luxToTransitionPointMap); diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index ae33b83b49dc..1f9eb082aaf4 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -3377,10 +3377,18 @@ public final class DisplayManagerService extends SystemService { private void dumpInternal(PrintWriter pw) { pw.println("DISPLAY MANAGER (dumpsys display)"); BrightnessTracker brightnessTrackerLocal; + SparseArray<DisplayPowerController> displayPowerControllersLocal = new SparseArray<>(); + int displayPowerControllerCount; synchronized (mSyncRoot) { brightnessTrackerLocal = mBrightnessTracker; + displayPowerControllerCount = mDisplayPowerControllers.size(); + for (int i = 0; i < displayPowerControllerCount; i++) { + displayPowerControllersLocal.put( + mDisplayPowerControllers.keyAt(i), mDisplayPowerControllers.valueAt(i)); + } + pw.println(" mSafeMode=" + mSafeMode); pw.println(" mPendingTraversal=" + mPendingTraversal); pw.println(" mViewports=" + mViewports); @@ -3451,13 +3459,6 @@ public final class DisplayManagerService extends SystemService { + ", mWifiDisplayScanRequested=" + callback.mWifiDisplayScanRequested); } - final int displayPowerControllerCount = mDisplayPowerControllers.size(); - pw.println(); - pw.println("Display Power Controllers: size=" + displayPowerControllerCount); - for (int i = 0; i < displayPowerControllerCount; i++) { - mDisplayPowerControllers.valueAt(i).dump(pw); - } - pw.println(); mPersistentDataStore.dump(pw); @@ -3470,6 +3471,12 @@ public final class DisplayManagerService extends SystemService { mDisplayWindowPolicyControllers.valueAt(i).second.dump(" ", pw); } } + pw.println(); + pw.println("Display Power Controllers: size=" + displayPowerControllerCount); + for (int i = 0; i < displayPowerControllerCount; i++) { + displayPowerControllersLocal.valueAt(i).dump(pw); + } + if (brightnessTrackerLocal != null) { pw.println(); brightnessTrackerLocal.dump(pw); @@ -5293,6 +5300,17 @@ public final class DisplayManagerService extends SystemService { } @Override + public IntArray getDisplayIds() { + IntArray displayIds = new IntArray(); + synchronized (mSyncRoot) { + mLogicalDisplayMapper.forEachLocked((logicalDisplay -> { + displayIds.add(logicalDisplay.getDisplayIdLocked()); + }), /* includeDisabled= */ false); + } + return displayIds; + } + + @Override public DisplayManagerInternal.DisplayOffloadSession registerDisplayOffloader( int displayId, @NonNull DisplayManagerInternal.DisplayOffloader displayOffloader) { if (!mFlags.isDisplayOffloadEnabled()) { diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 711bc7972091..03fec0115613 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -22,6 +22,7 @@ import static android.hardware.display.DisplayManagerInternal.DisplayPowerReques import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT; import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE; import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE; +import static com.android.server.display.brightness.BrightnessEvent.FLAG_EVEN_DIMMER; import static com.android.server.display.config.DisplayBrightnessMappingConfig.autoBrightnessPresetToString; import android.animation.Animator; @@ -1479,29 +1480,24 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call brightnessState = clampScreenBrightness(brightnessState); } - if (useDozeBrightness) { - // TODO(b/329676661): Introduce a config property to choose between this brightness - // strategy and DOZE_DEFAULT - // On some devices, when auto-brightness is disabled and the device is dozing, we use - // the current brightness setting scaled by the doze scale factor - if ((Float.isNaN(brightnessState) - || displayBrightnessState.getDisplayBrightnessStrategyName() - .equals(DisplayBrightnessStrategyConstants.FALLBACK_BRIGHTNESS_STRATEGY_NAME)) - && mFlags.isDisplayOffloadEnabled() - && mDisplayOffloadSession != null + if (useDozeBrightness && (Float.isNaN(brightnessState) + || displayBrightnessState.getDisplayBrightnessStrategyName() + .equals(DisplayBrightnessStrategyConstants.FALLBACK_BRIGHTNESS_STRATEGY_NAME))) { + if (mFlags.isDisplayOffloadEnabled() && mDisplayOffloadSession != null && (mAutomaticBrightnessController == null || !mAutomaticBrightnessStrategy.shouldUseAutoBrightness())) { + // TODO(b/329676661): Introduce a config property to choose between this brightness + // strategy and DOZE_DEFAULT + // On some devices, when auto-brightness is disabled and the device is dozing, we + // use the current brightness setting scaled by the doze scale factor rawBrightnessState = getDozeBrightnessForOffload(); brightnessState = clampScreenBrightness(rawBrightnessState); updateScreenBrightnessSetting = false; mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_MANUAL); mTempBrightnessEvent.setFlags( mTempBrightnessEvent.getFlags() | BrightnessEvent.FLAG_DOZE_SCALE); - } - - // Use default brightness when dozing unless overridden. - if (Float.isNaN(brightnessState) - && !mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig()) { + } else if (!mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig()) { + // Use default brightness when dozing unless overridden. rawBrightnessState = mScreenBrightnessDozeConfig; brightnessState = clampScreenBrightness(rawBrightnessState); mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_DEFAULT); @@ -1755,6 +1751,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call final float brightnessOnAvailableScale = MathUtils.constrainedMap(0.0f, 1.0f, clampedState.getMinBrightness(), clampedMax, brightnessState); + final boolean evenDimmerModeOn = + mCdsi != null && mCdsi.getReduceBrightColorsActivatedForEvenDimmer(); mTempBrightnessEvent.setPercent(Math.round( 1000.0f * com.android.internal.display.BrightnessUtils.convertLinearToGamma( brightnessOnAvailableScale) / 10)); // rounded to one dp @@ -1769,7 +1767,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mTempBrightnessEvent.setHbmMode(mBrightnessRangeController.getHighBrightnessMode()); mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags() | (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0) - | (mPowerRequest.lowPowerMode ? BrightnessEvent.FLAG_LOW_POWER_MODE : 0)); + | (mPowerRequest.lowPowerMode ? BrightnessEvent.FLAG_LOW_POWER_MODE : 0) + | (evenDimmerModeOn ? FLAG_EVEN_DIMMER : 0)); mTempBrightnessEvent.setRbcStrength(mCdsi != null ? mCdsi.getReduceBrightColorsStrength() : -1); mTempBrightnessEvent.setPowerFactor(mPowerRequest.screenLowPowerBrightnessFactor); diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index 0570b2ab510b..5edea0a8b031 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -37,9 +37,9 @@ import android.os.SystemProperties; import android.os.Trace; import android.util.DisplayUtils; import android.util.LongSparseArray; -import android.util.MathUtils; import android.util.Slog; import android.util.SparseArray; +import android.util.Spline; import android.view.Display; import android.view.DisplayAddress; import android.view.DisplayCutout; @@ -81,10 +81,6 @@ final class LocalDisplayAdapter extends DisplayAdapter { private static final String UNIQUE_ID_PREFIX = "local:"; private static final String PROPERTY_EMULATOR_CIRCULAR = "ro.boot.emulator.circular"; - // Min and max strengths for even dimmer feature. - private static final float EVEN_DIMMER_MIN_STRENGTH = 0.0f; - private static final float EVEN_DIMMER_MAX_STRENGTH = 90.0f; - private static final float BRIGHTNESS_MIN = 0.0f; private final LongSparseArray<LocalDisplayDevice> mDevices = new LongSparseArray<>(); @@ -99,7 +95,9 @@ final class LocalDisplayAdapter extends DisplayAdapter { private Context mOverlayContext; private int mEvenDimmerStrength = -1; + private boolean mEvenDimmerEnabled = false; private ColorDisplayService.ColorDisplayServiceInternal mCdsi; + private Spline mNitsToEvenDimmerStrength; // Called with SyncRoot lock held. LocalDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, Context context, @@ -279,7 +277,7 @@ final class LocalDisplayAdapter extends DisplayAdapter { mIsFirstDisplay = isFirstDisplay; updateDisplayPropertiesLocked(staticDisplayInfo, dynamicInfo, modeSpecs); mSidekickInternal = LocalServices.getService(SidekickInternal.class); - mBacklightAdapter = new BacklightAdapter(displayToken, isFirstDisplay, + mBacklightAdapter = mInjector.getBacklightAdapter(displayToken, isFirstDisplay, mSurfaceControlProxy); mActiveSfDisplayModeAtStartId = dynamicInfo.activeDisplayModeId; } @@ -998,26 +996,36 @@ final class LocalDisplayAdapter extends DisplayAdapter { } private void applyColorMatrixBasedDimming(float brightnessState) { - int strength = (int) (MathUtils.constrainedMap( - // to this range: - EVEN_DIMMER_MAX_STRENGTH, EVEN_DIMMER_MIN_STRENGTH, - // from this range: - BRIGHTNESS_MIN, mDisplayDeviceConfig.getEvenDimmerTransitionPoint(), - // map this (+ rounded up): - brightnessState) + 0.5); - - if (mEvenDimmerStrength < 0 // uninitialised - || MathUtils.abs(mEvenDimmerStrength - strength) > 1 - || strength <= 1) { - mEvenDimmerStrength = strength; - } - boolean enabled = mEvenDimmerStrength > 0.0f; - if (mCdsi == null) { mCdsi = LocalServices.getService( ColorDisplayService.ColorDisplayServiceInternal.class); } - if (mCdsi != null) { + if (mCdsi == null) { + return; + } + + final float minHardwareNits = backlightToNits(brightnessToBacklight( + mDisplayDeviceConfig.getEvenDimmerTransitionPoint())); + final float requestedNits = + backlightToNits(brightnessToBacklight(brightnessState)); + mNitsToEvenDimmerStrength = + mCdsi.fetchEvenDimmerSpline(minHardwareNits); + + if (mNitsToEvenDimmerStrength == null) { + return; + } + + // Find required dimming strength, rounded up. + int strength = Math.round(mNitsToEvenDimmerStrength + .interpolate(requestedNits)); + boolean enabled = strength > 0.0f; + if (mEvenDimmerEnabled != enabled) { + Slog.i(TAG, "Setting Extra Dim; strength: " + strength + + ", " + (enabled ? "enabled" : "disabled")); + } + if (mEvenDimmerStrength != strength || mEvenDimmerEnabled != enabled) { + mEvenDimmerEnabled = enabled; + mEvenDimmerStrength = strength; mCdsi.applyEvenDimmerColorChanges(enabled, strength); } } @@ -1290,6 +1298,9 @@ final class LocalDisplayAdapter extends DisplayAdapter { pw.println("DisplayDeviceConfig: "); pw.println("---------------------"); pw.println(mDisplayDeviceConfig); + pw.println("mEvenDimmerEnabled=" + mEvenDimmerEnabled); + pw.println("mEvenDimmerStrength=" + mEvenDimmerStrength); + pw.println("mNitsToEvenDimmerStrength=" + mNitsToEvenDimmerStrength); } private int findSfDisplayModeIdLocked(int displayModeId, int modeGroup) { @@ -1461,6 +1472,12 @@ final class LocalDisplayAdapter extends DisplayAdapter { long physicalDisplayId, boolean isFirstDisplay, DisplayManagerFlags flags) { return DisplayDeviceConfig.create(context, physicalDisplayId, isFirstDisplay, flags); } + + public BacklightAdapter getBacklightAdapter(IBinder displayToken, boolean isFirstDisplay, + SurfaceControlProxy surfaceControlProxy) { + return new BacklightAdapter(displayToken, isFirstDisplay, surfaceControlProxy); + + } } public interface DisplayEventListener { diff --git a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java index ad57ebfb0600..9e9b899ffa7d 100644 --- a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java +++ b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java @@ -44,6 +44,7 @@ public final class BrightnessEvent { public static final int FLAG_DOZE_SCALE = 0x4; public static final int FLAG_USER_SET = 0x8; public static final int FLAG_LOW_POWER_MODE = 0x20; + public static final int FLAG_EVEN_DIMMER = 0x40; private static final SimpleDateFormat FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); @@ -492,6 +493,7 @@ public final class BrightnessEvent { + ((mFlags & FLAG_RBC) != 0 ? "rbc " : "") + ((mFlags & FLAG_INVALID_LUX) != 0 ? "invalid_lux " : "") + ((mFlags & FLAG_DOZE_SCALE) != 0 ? "doze_scale " : "") - + ((mFlags & FLAG_LOW_POWER_MODE) != 0 ? "low_power_mode " : ""); + + ((mFlags & FLAG_LOW_POWER_MODE) != 0 ? "low_power_mode " : "") + + ((mFlags & FLAG_EVEN_DIMMER) != 0 ? "even_dimmer " : ""); } } diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java index bd759f378d5b..dc59e66d85f2 100644 --- a/services/core/java/com/android/server/display/color/ColorDisplayService.java +++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java @@ -70,6 +70,7 @@ import android.provider.Settings.System; import android.util.MathUtils; import android.util.Slog; import android.util.SparseIntArray; +import android.util.Spline; import android.view.Display; import android.view.SurfaceControl; import android.view.accessibility.AccessibilityManager; @@ -114,6 +115,8 @@ public final class ColorDisplayService extends SystemService { Matrix.setIdentityM(MATRIX_IDENTITY, 0); } + private static final int EVEN_DIMMER_MAX_PERCENT_ALLOWED = 100; + private static final int MSG_USER_CHANGED = 0; private static final int MSG_SET_UP = 1; private static final int MSG_APPLY_NIGHT_DISPLAY_IMMEDIATE = 2; @@ -193,6 +196,9 @@ public final class ColorDisplayService extends SystemService { private final boolean mVisibleBackgroundUsersEnabled; private final UserManagerService mUserManager; + private Spline mEvenDimmerSpline; + private boolean mEvenDimmerActivated; + public ColorDisplayService(Context context) { super(context); mHandler = new TintHandler(DisplayThread.get().getLooper()); @@ -1625,6 +1631,16 @@ public final class ColorDisplayService extends SystemService { } /** + * Gets the adjusted nits, given a strength and nits. + * @param strength of reduce bright colors + * @param nits target nits + * @return the actual nits that would be output, given input nits and rbc strength. + */ + public float getAdjustedNitsForStrength(float nits, int strength) { + return mReduceBrightColorsTintController.getAdjustedNitsForStrength(nits, strength); + } + + /** * Sets the listener and returns whether reduce bright colors is currently enabled. */ public boolean setReduceBrightColorsListener(ReduceBrightColorsListener listener) { @@ -1644,6 +1660,14 @@ public final class ColorDisplayService extends SystemService { } /** + * + * @return whether reduce bright colors is on, due to even dimmer being activated + */ + public boolean getReduceBrightColorsActivatedForEvenDimmer() { + return mEvenDimmerActivated; + } + + /** * Gets the computed brightness, in nits, when the reduce bright colors feature is applied * at the current strength. * @@ -1667,10 +1691,42 @@ public final class ColorDisplayService extends SystemService { * Applies tint changes for even dimmer feature. */ public void applyEvenDimmerColorChanges(boolean enabled, int strength) { + mEvenDimmerActivated = enabled; mReduceBrightColorsTintController.setActivated(enabled); mReduceBrightColorsTintController.setMatrix(strength); mHandler.sendEmptyMessage(MSG_APPLY_REDUCE_BRIGHT_COLORS); } + + /** + * Get spline to map between requested nits, and required even dimmer strength. + * @return nits to strength spline + */ + public Spline fetchEvenDimmerSpline(float nits) { + if (mEvenDimmerSpline == null) { + mEvenDimmerSpline = createNitsToStrengthSpline(nits); + } + return mEvenDimmerSpline; + } + + /** + * Creates a spline mapping requested nits values, for each resulting strength value + * (in percent) for even dimmer. + * Returns null if coefficients are not initialised. + * @return spline from nits to strength + */ + private Spline createNitsToStrengthSpline(float nits) { + final float[] requestedNits = new float[EVEN_DIMMER_MAX_PERCENT_ALLOWED + 1]; + final float[] resultingStrength = new float[EVEN_DIMMER_MAX_PERCENT_ALLOWED + 1]; + for (int i = 0; i <= EVEN_DIMMER_MAX_PERCENT_ALLOWED; i++) { + resultingStrength[EVEN_DIMMER_MAX_PERCENT_ALLOWED - i] = i; + requestedNits[EVEN_DIMMER_MAX_PERCENT_ALLOWED - i] = + getAdjustedNitsForStrength(nits, i); + if (requestedNits[EVEN_DIMMER_MAX_PERCENT_ALLOWED - i] == 0) { + return null; + } + } + return new Spline.LinearSpline(requestedNits, resultingStrength); + } } /** diff --git a/services/core/java/com/android/server/display/color/ReduceBrightColorsTintController.java b/services/core/java/com/android/server/display/color/ReduceBrightColorsTintController.java index f529c4c65a9a..4f6fbd003d9a 100644 --- a/services/core/java/com/android/server/display/color/ReduceBrightColorsTintController.java +++ b/services/core/java/com/android/server/display/color/ReduceBrightColorsTintController.java @@ -123,6 +123,14 @@ public class ReduceBrightColorsTintController extends TintController { return computeComponentValue(mStrength) * nits; } + /** + * Returns the effective brightness (in nits), which has been adjusted to account for the effect + * of the bright color reduction. + */ + public float getAdjustedNitsForStrength(float nits, int strength) { + return computeComponentValue(strength) * nits; + } + private float computeComponentValue(int strengthLevel) { final float percentageStrength = strengthLevel / 100f; final float squaredPercentageStrength = percentageStrength * percentageStrength; diff --git a/services/core/java/com/android/server/display/config/EvenDimmerBrightnessData.java b/services/core/java/com/android/server/display/config/EvenDimmerBrightnessData.java index 7e2e10a7639f..9a590a293ea3 100644 --- a/services/core/java/com/android/server/display/config/EvenDimmerBrightnessData.java +++ b/services/core/java/com/android/server/display/config/EvenDimmerBrightnessData.java @@ -34,72 +34,76 @@ public class EvenDimmerBrightnessData { /** * Brightness value at which even dimmer methods are used. */ - public final float mTransitionPoint; + public final float transitionPoint; /** * Nits array, maps to mBacklight */ - public final float[] mNits; + public final float[] nits; /** * Backlight array, maps to mBrightness and mNits */ - public final float[] mBacklight; + public final float[] backlight; /** * Brightness array, maps to mBacklight */ - public final float[] mBrightness; + public final float[] brightness; + /** * Spline, mapping between backlight and nits */ - public final Spline mBacklightToNits; + public final Spline backlightToNits; + /** * Spline, mapping between nits and backlight */ - public final Spline mNitsToBacklight; + public final Spline nitsToBacklight; + /** * Spline, mapping between brightness and backlight */ - public final Spline mBrightnessToBacklight; + public final Spline brightnessToBacklight; + /** * Spline, mapping between backlight and brightness */ - public final Spline mBacklightToBrightness; + public final Spline backlightToBrightness; /** * Spline, mapping the minimum nits for each lux condition. */ - public final Spline mMinLuxToNits; + public final Spline minLuxToNits; @VisibleForTesting public EvenDimmerBrightnessData(float transitionPoint, float[] nits, float[] backlight, float[] brightness, Spline backlightToNits, Spline nitsToBacklight, Spline brightnessToBacklight, Spline backlightToBrightness, Spline minLuxToNits) { - mTransitionPoint = transitionPoint; - mNits = nits; - mBacklight = backlight; - mBrightness = brightness; - mBacklightToNits = backlightToNits; - mNitsToBacklight = nitsToBacklight; - mBrightnessToBacklight = brightnessToBacklight; - mBacklightToBrightness = backlightToBrightness; - mMinLuxToNits = minLuxToNits; + this.transitionPoint = transitionPoint; + this.nits = nits; + this.backlight = backlight; + this.brightness = brightness; + this.backlightToNits = backlightToNits; + this.nitsToBacklight = nitsToBacklight; + this.brightnessToBacklight = brightnessToBacklight; + this.backlightToBrightness = backlightToBrightness; + this.minLuxToNits = minLuxToNits; } @Override public String toString() { return "EvenDimmerBrightnessData {" - + "mTransitionPoint: " + mTransitionPoint - + ", mNits: " + Arrays.toString(mNits) - + ", mBacklight: " + Arrays.toString(mBacklight) - + ", mBrightness: " + Arrays.toString(mBrightness) - + ", mBacklightToNits: " + mBacklightToNits - + ", mNitsToBacklight: " + mNitsToBacklight - + ", mBrightnessToBacklight: " + mBrightnessToBacklight - + ", mBacklightToBrightness: " + mBacklightToBrightness - + ", mMinLuxToNits: " + mMinLuxToNits + + "transitionPoint: " + transitionPoint + + ", nits: " + Arrays.toString(nits) + + ", backlight: " + Arrays.toString(backlight) + + ", brightness: " + Arrays.toString(brightness) + + ", backlightToNits: " + backlightToNits + + ", nitsToBacklight: " + nitsToBacklight + + ", brightnessToBacklight: " + brightnessToBacklight + + ", backlightToBrightness: " + backlightToBrightness + + ", minLuxToNits: " + minLuxToNits + "} "; } @@ -122,7 +126,6 @@ public class EvenDimmerBrightnessData { if (map == null) { return null; } - String interpolation = map.getInterpolation(); List<BrightnessPoint> brightnessPoints = map.getBrightnessPoint(); if (brightnessPoints.isEmpty()) { @@ -169,22 +172,11 @@ public class EvenDimmerBrightnessData { ++i; } - // Explicitly choose linear interpolation. - if ("linear".equals(interpolation)) { - return new EvenDimmerBrightnessData(transitionPoint, nits, backlight, brightness, - new Spline.LinearSpline(backlight, nits), - new Spline.LinearSpline(nits, backlight), - new Spline.LinearSpline(brightness, backlight), - new Spline.LinearSpline(backlight, brightness), - new Spline.LinearSpline(minLux, minNits) - ); - } - return new EvenDimmerBrightnessData(transitionPoint, nits, backlight, brightness, - Spline.createSpline(backlight, nits), - Spline.createSpline(nits, backlight), - Spline.createSpline(brightness, backlight), - Spline.createSpline(backlight, brightness), + new Spline.LinearSpline(backlight, nits), + new Spline.LinearSpline(nits, backlight), + new Spline.LinearSpline(brightness, backlight), + new Spline.LinearSpline(backlight, brightness), Spline.createSpline(minLux, minNits) ); } diff --git a/services/core/java/com/android/server/flags/pinner.aconfig b/services/core/java/com/android/server/flags/pinner.aconfig index 2f817dbb9a7f..345366882d5a 100644 --- a/services/core/java/com/android/server/flags/pinner.aconfig +++ b/services/core/java/com/android/server/flags/pinner.aconfig @@ -9,8 +9,11 @@ flag { } flag { - name: "skip_home_art_pins" - namespace: "system_performance" - description: "Ablation study flag that controls if home app odex/vdex files should be pinned in memory." - bug: "340935152" -}
\ No newline at end of file + name: "pin_global_quota" + namespace: "system_performance" + description: "This flag controls whether pinner will use a global quota or not" + bug: "340935152" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index 92812670057a..99f7f12567b4 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -23,6 +23,7 @@ import android.graphics.PointF; import android.hardware.display.DisplayViewport; import android.hardware.input.KeyGestureEvent; import android.os.IBinder; +import android.util.SparseBooleanArray; import android.view.InputChannel; import android.view.inputmethod.InputMethodSubtype; @@ -45,9 +46,11 @@ public abstract class InputManagerInternal { /** * Called by the power manager to tell the input manager whether it should start - * watching for wake events. + * watching for wake events on given displays. + * + * @param displayInteractivities Map of display ids to their current interactive state. */ - public abstract void setInteractive(boolean interactive); + public abstract void setDisplayInteractivities(SparseBooleanArray displayInteractivities); /** * Toggles Caps Lock state for input device with specific id. diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 909c47bc9359..f04557665477 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -96,6 +96,7 @@ import android.os.vibrator.VibrationEffectSegment; import android.provider.DeviceConfig; import android.text.TextUtils; import android.util.ArrayMap; +import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; @@ -3337,10 +3338,22 @@ public class InputManagerService extends IInputManager.Stub } @Override - public void setInteractive(boolean interactive) { - mNative.setInteractive(interactive); - mBatteryController.onInteractiveChanged(interactive); - mKeyboardBacklightController.onInteractiveChanged(interactive); + public void setDisplayInteractivities(SparseBooleanArray displayInteractivities) { + boolean globallyInteractive = false; + ArraySet<Integer> nonInteractiveDisplays = new ArraySet<>(); + for (int i = 0; i < displayInteractivities.size(); i++) { + final int displayId = displayInteractivities.keyAt(i); + final boolean displayInteractive = displayInteractivities.get(displayId); + if (displayInteractive) { + globallyInteractive = true; + } else { + nonInteractiveDisplays.add(displayId); + } + } + mNative.setNonInteractiveDisplays( + nonInteractiveDisplays.stream().mapToInt(Integer::intValue).toArray()); + mBatteryController.onInteractiveChanged(globallyInteractive); + mKeyboardBacklightController.onInteractiveChanged(globallyInteractive); } // TODO(b/358569822): Remove this method from InputManagerInternal after key gesture diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index d17e256e34fc..4404d63e02fc 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -141,7 +141,7 @@ interface NativeInputManagerService { void setShowTouches(boolean enabled); - void setInteractive(boolean interactive); + void setNonInteractiveDisplays(int[] displayIds); void reloadCalibration(); @@ -409,7 +409,7 @@ interface NativeInputManagerService { public native void setShowTouches(boolean enabled); @Override - public native void setInteractive(boolean interactive); + public native void setNonInteractiveDisplays(int[] displayIds); @Override public native void reloadCalibration(); diff --git a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java index 41313fa1fb2c..ef1220fb1786 100644 --- a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java +++ b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java @@ -33,9 +33,6 @@ final class HardwareKeyboardShortcutController { @GuardedBy("ImfLock.class") private final ArrayList<InputMethodSubtypeHandle> mSubtypeHandles = new ArrayList<>(); - HardwareKeyboardShortcutController() { - } - @GuardedBy("ImfLock.class") void update(@NonNull InputMethodSettings settings) { mSubtypeHandles.clear(); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java b/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java index 6cd2493cfdff..fc4c0fc798db 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java @@ -40,6 +40,7 @@ final class InputMethodDeviceConfigs { if (KEY_HIDE_IME_WHEN_NO_EDITOR_FOCUS.equals(name)) { mHideImeWhenNoEditorFocus = properties.getBoolean(name, true /* defaultValue */); + break; } } }; diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java index 214aa1d904fa..49d4332d9e2a 100644 --- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java +++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java @@ -394,6 +394,7 @@ final class ZeroJankProxy implements IInputMethodManagerImpl.Callback { flags), this::offload).get(); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); throw new RuntimeException(e); } catch (ExecutionException e) { throw new RuntimeException(e); diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index 48d24f2e14dd..47f579db604f 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -195,10 +195,9 @@ public final class MediaProjectionManagerService extends SystemService == PackageManager.PERMISSION_GRANTED) { return true; } - boolean operationActive = mAppOps.isOperationActive(AppOpsManager.OP_PROJECT_MEDIA, - mProjectionGrant.uid, - mProjectionGrant.packageName); - if (operationActive) { + if (AppOpsManager.MODE_ALLOWED == mAppOps.noteOpNoThrow(AppOpsManager.OP_PROJECT_MEDIA, + mProjectionGrant.uid, mProjectionGrant.packageName, /* attributionTag= */ null, + "recording lockscreen")) { // Some tools use media projection by granting the OP_PROJECT_MEDIA app // op via a shell command. Those tools can be granted keyguard capture return true; diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java index abb21323f7f0..06f419a785f9 100644 --- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java +++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java @@ -118,6 +118,37 @@ public final class NotificationAttentionHelper { Intent.ACTION_MANAGED_PROFILE_AVAILABLE, new Pair<>(Intent.EXTRA_QUIET_MODE, false) ); + // Bits 1, 2, 3, 4 are already taken by: beep|buzz|blink|cooldown + static final int MUTE_REASON_NOT_MUTED = 0; + static final int MUTE_REASON_NOT_AUDIBLE = 1 << 5; + static final int MUTE_REASON_SILENT_UPDATE = 1 << 6; + static final int MUTE_REASON_POST_SILENTLY = 1 << 7; + static final int MUTE_REASON_LISTENER_HINT = 1 << 8; + static final int MUTE_REASON_DND = 1 << 9; + static final int MUTE_REASON_GROUP_ALERT = 1 << 10; + static final int MUTE_REASON_FLAG_SILENT = 1 << 11; + static final int MUTE_REASON_RATE_LIMIT = 1 << 12; + static final int MUTE_REASON_OTHER_INSISTENT_PLAYING = 1 << 13; + static final int MUTE_REASON_SUPPRESSED_BUBBLE = 1 << 14; + static final int MUTE_REASON_COOLDOWN = 1 << 15; + + @IntDef(prefix = { "MUTE_REASON_" }, value = { + MUTE_REASON_NOT_MUTED, + MUTE_REASON_NOT_AUDIBLE, + MUTE_REASON_SILENT_UPDATE, + MUTE_REASON_POST_SILENTLY, + MUTE_REASON_LISTENER_HINT, + MUTE_REASON_DND, + MUTE_REASON_GROUP_ALERT, + MUTE_REASON_FLAG_SILENT, + MUTE_REASON_RATE_LIMIT, + MUTE_REASON_OTHER_INSISTENT_PLAYING, + MUTE_REASON_SUPPRESSED_BUBBLE, + MUTE_REASON_COOLDOWN, + }) + @Retention(RetentionPolicy.SOURCE) + @interface MuteReason {} + private final Context mContext; private final PackageManager mPackageManager; private final TelephonyManager mTelephonyManager; @@ -388,6 +419,7 @@ public final class NotificationAttentionHelper { boolean buzz = false; boolean beep = false; boolean blink = false; + @MuteReason int shouldMuteReason = MUTE_REASON_NOT_MUTED; final String key = record.getKey(); @@ -395,10 +427,6 @@ public final class NotificationAttentionHelper { Log.d(TAG, "buzzBeepBlinkLocked " + record); } - if (isPoliteNotificationFeatureEnabled(record)) { - mStrategy.onNotificationPosted(record); - } - // Should this notification make noise, vibe, or use the LED? final boolean aboveThreshold = mIsAutomotive @@ -443,7 +471,8 @@ public final class NotificationAttentionHelper { boolean vibrateOnly = hasValidVibrate && mNotificationCooldownVibrateUnlocked && mUserPresent; boolean hasAudibleAlert = hasValidSound || hasValidVibrate; - if (hasAudibleAlert && !shouldMuteNotificationLocked(record, signals)) { + shouldMuteReason = shouldMuteNotificationLocked(record, signals, hasAudibleAlert); + if (shouldMuteReason == MUTE_REASON_NOT_MUTED) { if (!sentAccessibilityEvent) { sendAccessibilityEvent(record); sentAccessibilityEvent = true; @@ -541,15 +570,17 @@ public final class NotificationAttentionHelper { } } final int buzzBeepBlinkLoggingCode = - (buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0) | getPoliteBit(record); + (buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0) + | getPoliteBit(record) | shouldMuteReason; if (buzzBeepBlinkLoggingCode > 0) { MetricsLogger.action(record.getLogMaker() .setCategory(MetricsEvent.NOTIFICATION_ALERT) .setType(MetricsEvent.TYPE_OPEN) .setSubtype(buzzBeepBlinkLoggingCode)); EventLogTags.writeNotificationAlert(key, buzz ? 1 : 0, beep ? 1 : 0, blink ? 1 : 0, - getPolitenessState(record)); + getPolitenessState(record), shouldMuteReason); } + if (Flags.politeNotifications()) { // Update last alert time if (buzz || beep) { @@ -594,41 +625,46 @@ public final class NotificationAttentionHelper { mNMP.getNotificationByKey(mVibrateNotificationKey)); } - boolean shouldMuteNotificationLocked(final NotificationRecord record, final Signals signals) { + @MuteReason int shouldMuteNotificationLocked(final NotificationRecord record, + final Signals signals, boolean hasAudibleAlert) { + // Suppressed because no audible alert + if (!hasAudibleAlert) { + return MUTE_REASON_NOT_AUDIBLE; + } // Suppressed because it's a silent update final Notification notification = record.getNotification(); if (record.isUpdate && (notification.flags & FLAG_ONLY_ALERT_ONCE) != 0) { - return true; + return MUTE_REASON_SILENT_UPDATE; } // Suppressed because a user manually unsnoozed something (or similar) if (record.shouldPostSilently()) { - return true; + return MUTE_REASON_POST_SILENTLY; } // muted by listener final String disableEffects = disableNotificationEffects(record, signals.listenerHints); if (disableEffects != null) { ZenLog.traceDisableEffects(record, disableEffects); - return true; + return MUTE_REASON_LISTENER_HINT; } // suppressed due to DND if (record.isIntercepted()) { - return true; + return MUTE_REASON_DND; } // Suppressed because another notification in its group handles alerting if (record.getSbn().isGroup()) { if (notification.suppressAlertingDueToGrouping()) { - return true; + return MUTE_REASON_GROUP_ALERT; } } // Suppressed because notification was explicitly flagged as silent if (android.service.notification.Flags.notificationSilentFlag()) { if (notification.isSilent()) { - return true; + return MUTE_REASON_FLAG_SILENT; } } @@ -636,12 +672,12 @@ public final class NotificationAttentionHelper { final String pkg = record.getSbn().getPackageName(); if (mUsageStats.isAlertRateLimited(pkg)) { Slog.e(TAG, "Muting recently noisy " + record.getKey()); - return true; + return MUTE_REASON_RATE_LIMIT; } // A different looping ringtone, such as an incoming call is playing if (isCurrentlyInsistent() && !isInsistentUpdate(record)) { - return true; + return MUTE_REASON_OTHER_INSISTENT_PLAYING; } // Suppressed since it's a non-interruptive update to a bubble-suppressed notification @@ -650,11 +686,23 @@ public final class NotificationAttentionHelper { if (record.isUpdate && !record.isInterruptive() && isBubbleOrOverflowed && record.getNotification().getBubbleMetadata() != null) { if (record.getNotification().getBubbleMetadata().isNotificationSuppressed()) { - return true; + return MUTE_REASON_SUPPRESSED_BUBBLE; } } - return false; + if (isPoliteNotificationFeatureEnabled(record)) { + // Notify the politeness strategy that an alerting notification is posted + if (!isInsistentUpdate(record)) { + mStrategy.onNotificationPosted(record); + } + + // Suppress if politeness is muted and it's not an update for insistent + if (getPolitenessState(record) == PolitenessStrategy.POLITE_STATE_MUTED) { + return MUTE_REASON_COOLDOWN; + } + } + + return MUTE_REASON_NOT_MUTED; } private boolean isLoopingRingtoneNotification(final NotificationRecord playingRecord) { @@ -1201,12 +1249,6 @@ public final class NotificationAttentionHelper { mApplyPerPackage = applyPerPackage; } - boolean shouldIgnoreNotification(final NotificationRecord record) { - // Ignore auto-group summaries => don't count them as app-posted notifications - // for the cooldown budget - return (record.getSbn().isGroup() && GroupHelper.isAggregatedGroup(record)); - } - /** * Get the key that determines the grouping for the cooldown behavior. * @@ -1358,10 +1400,6 @@ public final class NotificationAttentionHelper { @Override public void onNotificationPosted(final NotificationRecord record) { - if (shouldIgnoreNotification(record)) { - return; - } - long timeSinceLastNotif = System.currentTimeMillis() - getLastNotificationUpdateTimeMs(record); @@ -1434,10 +1472,6 @@ public final class NotificationAttentionHelper { @Override void onNotificationPosted(NotificationRecord record) { if (isAvalancheActive()) { - if (shouldIgnoreNotification(record)) { - return; - } - long timeSinceLastNotif = System.currentTimeMillis() - getLastNotificationUpdateTimeMs(record); diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index c3a714b0eef0..655f2e4596aa 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -2579,6 +2579,7 @@ public class NotificationManagerService extends SystemService { mNotificationChannelLogger, mAppOps, mUserProfiles, + mUgmInternal, mShowReviewPermissionsNotification, Clock.systemUTC()); mRankingHelper = new RankingHelper(getContext(), mRankingHandler, mPreferencesHelper, @@ -6247,6 +6248,7 @@ public class NotificationManagerService extends SystemService { int callingUid = Binder.getCallingUid(); @ZenModeConfig.ConfigOrigin int origin = computeZenOrigin(fromUser); + boolean isSystemCaller = isCallerSystemOrSystemUiOrShell(); boolean shouldApplyAsImplicitRule = android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid); @@ -6283,11 +6285,33 @@ public class NotificationManagerService extends SystemService { policy.priorityCallSenders, policy.priorityMessageSenders, policy.suppressedVisualEffects, currPolicy.priorityConversationSenders); } + int newVisualEffects = calculateSuppressedVisualEffects( policy, currPolicy, applicationInfo.targetSdkVersion); - policy = new Policy(policy.priorityCategories, - policy.priorityCallSenders, policy.priorityMessageSenders, - newVisualEffects, policy.priorityConversationSenders); + + if (android.app.Flags.modesUi()) { + // 1. Callers should not modify STATE_CHANNELS_BYPASSING_DND, which is + // internally calculated and only indicates whether channels that want to bypass + // DND _exist_. + // 2. Only system callers should modify STATE_PRIORITY_CHANNELS_BLOCKED because + // it is @hide. + // 3. If the policy has been modified by the targetSdkVersion checks above then + // it has lost its state flags and that's fine (STATE_PRIORITY_CHANNELS_BLOCKED + // didn't exist until V). + int newState = Policy.STATE_UNSET; + if (isSystemCaller && policy.state != Policy.STATE_UNSET) { + newState = Policy.policyState( + currPolicy.hasPriorityChannels(), + policy.allowPriorityChannels()); + } + policy = new Policy(policy.priorityCategories, + policy.priorityCallSenders, policy.priorityMessageSenders, + newVisualEffects, newState, policy.priorityConversationSenders); + } else { + policy = new Policy(policy.priorityCategories, + policy.priorityCallSenders, policy.priorityMessageSenders, + newVisualEffects, policy.priorityConversationSenders); + } if (shouldApplyAsImplicitRule) { mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy); @@ -6672,13 +6696,7 @@ public class NotificationManagerService extends SystemService { final Uri originalSoundUri = (originalChannel != null) ? originalChannel.getSound() : null; if (soundUri != null && !Objects.equals(originalSoundUri, soundUri)) { - Binder.withCleanCallingIdentity(() -> { - mUgmInternal.checkGrantUriPermission(sourceUid, null, - ContentProvider.getUriWithoutUserId(soundUri), - Intent.FLAG_GRANT_READ_URI_PERMISSION, - ContentProvider.getUserIdFromUri(soundUri, - UserHandle.getUserId(sourceUid))); - }); + PermissionHelper.grantUriPermission(mUgmInternal, soundUri, sourceUid); } } diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index b9f0968b5864..3ba93845a290 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -1493,14 +1493,23 @@ public final class NotificationRecord { final Notification notification = getNotification(); notification.visitUris((uri) -> { - visitGrantableUri(uri, false, false); + if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) { + visitGrantableUri(uri, false, false); + } else { + oldVisitGrantableUri(uri, false, false); + } }); if (notification.getChannelId() != null) { NotificationChannel channel = getChannel(); if (channel != null) { - visitGrantableUri(channel.getSound(), (channel.getUserLockedFields() - & NotificationChannel.USER_LOCKED_SOUND) != 0, true); + if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) { + visitGrantableUri(channel.getSound(), (channel.getUserLockedFields() + & NotificationChannel.USER_LOCKED_SOUND) != 0, true); + } else { + oldVisitGrantableUri(channel.getSound(), (channel.getUserLockedFields() + & NotificationChannel.USER_LOCKED_SOUND) != 0, true); + } } } } finally { @@ -1516,7 +1525,7 @@ public final class NotificationRecord { * {@link #mGrantableUris}. Otherwise, this will either log or throw * {@link SecurityException} depending on target SDK of enqueuing app. */ - private void visitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) { + private void oldVisitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) { if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return; if (mGrantableUris != null && mGrantableUris.contains(uri)) { @@ -1555,6 +1564,45 @@ public final class NotificationRecord { } } + /** + * Note the presence of a {@link Uri} that should have permission granted to + * whoever will be rendering it. + * <p> + * If the enqueuing app has the ability to grant access, it will be added to + * {@link #mGrantableUris}. Otherwise, this will either log or throw + * {@link SecurityException} depending on target SDK of enqueuing app. + */ + private void visitGrantableUri(Uri uri, boolean userOverriddenUri, + boolean isSound) { + if (mGrantableUris != null && mGrantableUris.contains(uri)) { + return; // already verified this URI + } + + final int sourceUid = getSbn().getUid(); + try { + PermissionHelper.grantUriPermission(mUgmInternal, uri, sourceUid); + + if (mGrantableUris == null) { + mGrantableUris = new ArraySet<>(); + } + mGrantableUris.add(uri); + } catch (SecurityException e) { + if (!userOverriddenUri) { + if (isSound) { + mSound = Settings.System.DEFAULT_NOTIFICATION_URI; + Log.w(TAG, "Replacing " + uri + " from " + sourceUid + ": " + e.getMessage()); + } else { + if (mTargetSdkVersion >= Build.VERSION_CODES.P) { + throw e; + } else { + Log.w(TAG, + "Ignoring " + uri + " from " + sourceUid + ": " + e.getMessage()); + } + } + } + } + } + public LogMaker getLogMaker(long now) { LogMaker lm = getSbn().getLogMaker() .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_CHANNEL_IMPORTANCE, mImportance) diff --git a/services/core/java/com/android/server/notification/PermissionHelper.java b/services/core/java/com/android/server/notification/PermissionHelper.java index b6f48890c528..1464d481311a 100644 --- a/services/core/java/com/android/server/notification/PermissionHelper.java +++ b/services/core/java/com/android/server/notification/PermissionHelper.java @@ -25,19 +25,25 @@ import android.Manifest; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.companion.virtual.VirtualDeviceManager; +import android.content.ContentProvider; +import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; +import android.net.Uri; import android.os.Binder; import android.os.RemoteException; +import android.os.UserHandle; import android.permission.IPermissionManager; import android.util.ArrayMap; import android.util.Pair; import android.util.Slog; import com.android.internal.util.ArrayUtils; +import com.android.server.uri.UriGrantsManagerInternal; import java.util.Collections; import java.util.HashSet; @@ -58,7 +64,7 @@ public final class PermissionHelper { private final IPermissionManager mPermManager; public PermissionHelper(Context context, IPackageManager packageManager, - IPermissionManager permManager) { + IPermissionManager permManager) { mContext = context; mPackageManager = packageManager; mPermManager = permManager; @@ -298,6 +304,19 @@ public final class PermissionHelper { return false; } + static void grantUriPermission(final UriGrantsManagerInternal ugmInternal, Uri uri, + int sourceUid) { + if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return; + + Binder.withCleanCallingIdentity(() -> { + // This will throw a SecurityException if the caller can't grant. + ugmInternal.checkGrantUriPermission(sourceUid, null, + ContentProvider.getUriWithoutUserId(uri), + Intent.FLAG_GRANT_READ_URI_PERMISSION, + ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid))); + }); + } + public static class PackagePermission { public final String packageName; public final @UserIdInt int userId; diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 85c395781d0a..9e70f815dff9 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -94,6 +94,7 @@ import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.notification.PermissionHelper.PackagePermission; +import com.android.server.uri.UriGrantsManagerInternal; import org.json.JSONArray; import org.json.JSONException; @@ -219,6 +220,7 @@ public class PreferencesHelper implements RankingConfig { private final NotificationChannelLogger mNotificationChannelLogger; private final AppOpsManager mAppOps; private final ManagedServices.UserProfiles mUserProfiles; + private final UriGrantsManagerInternal mUgmInternal; private SparseBooleanArray mBadgingEnabled; private SparseBooleanArray mBubblesEnabled; @@ -239,6 +241,7 @@ public class PreferencesHelper implements RankingConfig { ZenModeHelper zenHelper, PermissionHelper permHelper, PermissionManager permManager, NotificationChannelLogger notificationChannelLogger, AppOpsManager appOpsManager, ManagedServices.UserProfiles userProfiles, + UriGrantsManagerInternal ugmInternal, boolean showReviewPermissionsNotification, Clock clock) { mContext = context; mZenModeHelper = zenHelper; @@ -249,6 +252,7 @@ public class PreferencesHelper implements RankingConfig { mNotificationChannelLogger = notificationChannelLogger; mAppOps = appOpsManager; mUserProfiles = userProfiles; + mUgmInternal = ugmInternal; mShowReviewPermissionsNotification = showReviewPermissionsNotification; mIsMediaNotificationFilteringEnabled = context.getResources() .getBoolean(R.bool.config_quickSettingsShowMediaPlayer); @@ -1169,6 +1173,13 @@ public class PreferencesHelper implements RankingConfig { } clearLockedFieldsLocked(channel); + // Verify that the app has permission to read the sound Uri + // Only check for new channels, as regular apps can only set sound + // before creating. See: {@link NotificationChannel#setSound} + if (Flags.notificationVerifyChannelSoundUri()) { + PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid); + } + channel.setImportanceLockedByCriticalDeviceFunction( r.defaultAppLockedImportance || r.fixedImportance); diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index be3adc142fa4..0b34177d7413 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -144,6 +144,13 @@ flag { } flag { + name: "notification_minimalism" + namespace: "systemui" + description: "Minimize the notifications to show on the lockscreen." + bug: "330387368" +} + +flag { name: "notification_force_group_singletons" namespace: "systemui" description: "This flag enables forced auto-grouping singleton groups" @@ -163,3 +170,13 @@ flag { description: "This flag enables sound uri with vibration source" bug: "358524009" } + +flag { + name: "notification_verify_channel_sound_uri" + namespace: "systemui" + description: "Verify Uri permission for sound when creating a notification channel" + bug: "337775777" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/core/java/com/android/server/pdb/PersistentDataBlockService.java b/services/core/java/com/android/server/pdb/PersistentDataBlockService.java index 8410cff74265..fe9a85989cc9 100644 --- a/services/core/java/com/android/server/pdb/PersistentDataBlockService.java +++ b/services/core/java/com/android/server/pdb/PersistentDataBlockService.java @@ -171,7 +171,6 @@ public class PersistentDataBlockService extends SystemService { static final int MAX_DATA_BLOCK_SIZE = 1024 * 100; public static final int DIGEST_SIZE_BYTES = 32; - private static final String OEM_UNLOCK_PROP = "sys.oem_unlock_allowed"; private static final String FLASH_LOCK_PROP = "ro.boot.flash.locked"; private static final String FLASH_LOCK_LOCKED = "1"; private static final String FLASH_LOCK_UNLOCKED = "0"; @@ -275,7 +274,6 @@ public class PersistentDataBlockService extends SystemService { enforceChecksumValidity(); if (mFrpEnforced) { automaticallyDeactivateFrpIfPossible(); - setOemUnlockEnabledProperty(doGetOemUnlockEnabled()); setOldSettingForBackworkCompatibility(mFrpActive); } else { formatIfOemUnlockEnabled(); @@ -303,10 +301,6 @@ public class PersistentDataBlockService extends SystemService { } } - private void setOemUnlockEnabledProperty(boolean oemUnlockEnabled) { - setProperty(OEM_UNLOCK_PROP, oemUnlockEnabled ? "1" : "0"); - } - @Override public void onBootPhase(int phase) { // Wait for initialization in onStart to finish @@ -342,7 +336,6 @@ public class PersistentDataBlockService extends SystemService { formatPartitionLocked(true); } } - setOemUnlockEnabledProperty(enabled); } private void enforceOemUnlockReadPermission() { @@ -814,17 +807,9 @@ public class PersistentDataBlockService extends SystemService { channel.force(true); } catch (IOException e) { Slog.e(TAG, "unable to access persistent partition", e); - return; - } finally { - setOemUnlockEnabledProperty(enabled); } } - @VisibleForTesting - void setProperty(String name, String value) { - SystemProperties.set(name, value); - } - private boolean doGetOemUnlockEnabled() { DataInputStream inputStream; try { diff --git a/services/core/java/com/android/server/pinner/PinRangeSource.java b/services/core/java/com/android/server/pinner/PinRangeSource.java new file mode 100644 index 000000000000..5f9641122294 --- /dev/null +++ b/services/core/java/com/android/server/pinner/PinRangeSource.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.pinner; + +/* package */ abstract class PinRangeSource { + /** + * Retrieve a range to pin. + * + * @param outPinRange Receives the pin region + * @return True if we filled in outPinRange or false if we're out of pin entries + */ + abstract boolean read(PinnerService.PinRange outPinRange); +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java b/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java new file mode 100644 index 000000000000..d6fc48790883 --- /dev/null +++ b/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java @@ -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.server.pinner; + +/* package */ class PinRangeSourceStatic extends PinRangeSource { + private final int mPinStart; + private final int mPinLength; + private boolean mDone = false; + + PinRangeSourceStatic(int pinStart, int pinLength) { + mPinStart = pinStart; + mPinLength = pinLength; + } + + @Override + boolean read(PinnerService.PinRange outPinRange) { + outPinRange.start = mPinStart; + outPinRange.length = mPinLength; + boolean done = mDone; + mDone = true; + return !done; + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/pinner/PinRangeSourceStream.java b/services/core/java/com/android/server/pinner/PinRangeSourceStream.java new file mode 100644 index 000000000000..79900b9de463 --- /dev/null +++ b/services/core/java/com/android/server/pinner/PinRangeSourceStream.java @@ -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.server.pinner; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; + +/* package */ final class PinRangeSourceStream extends PinRangeSource { + private final DataInputStream mStream; + private boolean mDone = false; + + PinRangeSourceStream(InputStream stream) { + mStream = new DataInputStream(stream); + } + + @Override + boolean read(PinnerService.PinRange outPinRange) { + if (!mDone) { + try { + outPinRange.start = mStream.readInt(); + outPinRange.length = mStream.readInt(); + } catch (IOException ex) { + mDone = true; + } + } + return !mDone; + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/pinner/PinnedFile.java b/services/core/java/com/android/server/pinner/PinnedFile.java new file mode 100644 index 000000000000..a8de344d10af --- /dev/null +++ b/services/core/java/com/android/server/pinner/PinnedFile.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 com.android.server.pinner; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; + +@VisibleForTesting +public final class PinnedFile implements AutoCloseable { + private long mAddress; + final long mapSize; + final String fileName; + public final long bytesPinned; + + // Whether this file was pinned using a pinlist + boolean used_pinlist; + + // User defined group name for pinner accounting + String groupName = ""; + ArrayList<PinnedFile> pinnedDeps = new ArrayList<>(); + + public PinnedFile(long address, long mapSize, String fileName, long bytesPinned) { + mAddress = address; + this.mapSize = mapSize; + this.fileName = fileName; + this.bytesPinned = bytesPinned; + } + + @Override + public void close() { + if (mAddress >= 0) { + PinnerUtils.safeMunmap(mAddress, mapSize); + mAddress = -1; + } + for (PinnedFile dep : pinnedDeps) { + if (dep != null) { + dep.close(); + } + } + } + + @Override + public void finalize() { + close(); + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/PinnerService.java b/services/core/java/com/android/server/pinner/PinnerService.java index ef03888d6620..d7ac5203ff53 100644 --- a/services/core/java/com/android/server/PinnerService.java +++ b/services/core/java/com/android/server/pinner/PinnerService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,14 @@ * limitations under the License. */ -package com.android.server; +package com.android.server.pinner; import static android.app.ActivityManager.UID_OBSERVER_ACTIVE; import static android.app.ActivityManager.UID_OBSERVER_GONE; import static android.os.Process.SYSTEM_UID; +import static com.android.server.flags.Flags.pinGlobalQuota; import static com.android.server.flags.Flags.pinWebview; -import static com.android.server.flags.Flags.skipHomeArtPins; import android.annotation.EnforcePermission; import android.annotation.IntDef; @@ -49,6 +49,7 @@ import android.os.Handler; import android.os.HandlerExecutor; import android.os.Looper; import android.os.Message; +import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; @@ -72,13 +73,13 @@ import com.android.internal.app.ResolverActivity; import com.android.internal.os.BackgroundThread; import com.android.internal.util.DumpUtils; import com.android.internal.util.function.pooled.PooledLambda; +import com.android.server.LocalServices; +import com.android.server.SystemService; import com.android.server.wm.ActivityTaskManagerInternal; import dalvik.system.DexFile; import dalvik.system.VMRuntime; -import java.io.Closeable; -import java.io.DataInputStream; import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; @@ -110,8 +111,7 @@ public final class PinnerService extends SystemService { private static final String PIN_META_FILENAME = "pinlist.meta"; private static final int PAGE_SIZE = (int) Os.sysconf(OsConstants._SC_PAGESIZE); private static final int MATCH_FLAGS = PackageManager.MATCH_DEFAULT_ONLY - | PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE; + | PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE; private static final int KEY_CAMERA = 0; private static final int KEY_HOME = 1; @@ -126,6 +126,8 @@ public final class PinnerService extends SystemService { public static final String ANON_REGION_STAT_NAME = "[anon]"; + private static final String SYSTEM_GROUP_NAME = "system"; + @IntDef({KEY_CAMERA, KEY_HOME, KEY_ASSISTANT}) @Retention(RetentionPolicy.SOURCE) public @interface AppKey {} @@ -139,7 +141,8 @@ public final class PinnerService extends SystemService { private final UserManager mUserManager; /** The list of the statically pinned files. */ - @GuardedBy("this") private final ArrayMap<String, PinnedFile> mPinnedFiles = new ArrayMap<>(); + @GuardedBy("this") + private final ArrayMap<String, PinnedFile> mPinnedFiles = new ArrayMap<>(); /** The list of the pinned apps. This is a map from {@link AppKey} to a pinned app. */ @GuardedBy("this") @@ -159,8 +162,8 @@ public final class PinnerService extends SystemService { /** * A set of {@link AppKey} that are configured to be pinned. */ - @GuardedBy("this") - private ArraySet<Integer> mPinKeys; + @GuardedBy("this") private + ArraySet<Integer> mPinKeys; // Note that we don't use the `_BOOT` namespace for anonymous pinnings, as we want // them to be responsive to dynamic flag changes for experimentation. @@ -180,14 +183,23 @@ public final class PinnerService extends SystemService { private final boolean mConfiguredToPinAssistant; private final int mConfiguredWebviewPinBytes; + // This is the percentage of total device memory that will be used to set the global quota. + private final int mConfiguredMaxPinnedMemoryPercentage; + + // This is the global pinner quota that can be pinned. + private long mConfiguredMaxPinnedMemory; + + // This is the currently pinned memory. + private long mCurrentPinnedMemory = 0; + private BinderService mBinderService; private PinnerHandler mPinnerHandler = null; private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - // If an app has updated, update pinned files accordingly. - if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())) { + // If an app has updated, update pinned files accordingly. + if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())) { Uri packageUri = intent.getData(); String packageName = packageUri.getSchemeSpecificPart(); ArraySet<String> updatedPackages = new ArraySet<>(); @@ -210,7 +222,7 @@ public final class PinnerService extends SystemService { /** Utility class for testing. */ @VisibleForTesting - static class Injector { + public static class Injector { protected DeviceConfigInterface getDeviceConfigInterface() { return DeviceConfigInterface.REAL; } @@ -219,9 +231,9 @@ public final class PinnerService extends SystemService { service.publishBinderService("pinner", binderService); } - protected PinnedFile pinFileInternal(String fileToPin, - int maxBytesToPin, boolean attemptPinIntrospection) { - return PinnerService.pinFileInternal(fileToPin, maxBytesToPin, attemptPinIntrospection); + protected PinnedFile pinFileInternal(PinnerService service, String fileToPin, + long maxBytesToPin, boolean attemptPinIntrospection) { + return service.pinFileInternal(fileToPin, maxBytesToPin, attemptPinIntrospection); } } @@ -230,7 +242,7 @@ public final class PinnerService extends SystemService { } @VisibleForTesting - PinnerService(Context context, Injector injector) { + public PinnerService(Context context, Injector injector) { super(context); mContext = context; @@ -244,6 +256,9 @@ public final class PinnerService extends SystemService { com.android.internal.R.bool.config_pinnerAssistantApp); mConfiguredWebviewPinBytes = context.getResources().getInteger( com.android.internal.R.integer.config_pinnerWebviewPinBytes); + mConfiguredMaxPinnedMemoryPercentage = context.getResources().getInteger( + com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage); + mPinKeys = createPinKeys(); mPinnerHandler = new PinnerHandler(BackgroundThread.get().getLooper()); @@ -261,10 +276,8 @@ public final class PinnerService extends SystemService { registerUidListener(); registerUserSetupCompleteListener(); - mDeviceConfigInterface.addOnPropertiesChangedListener( - DEVICE_CONFIG_NAMESPACE_ANON_SIZE, - new HandlerExecutor(mPinnerHandler), - mDeviceConfigAnonSizeListener); + mDeviceConfigInterface.addOnPropertiesChangedListener(DEVICE_CONFIG_NAMESPACE_ANON_SIZE, + new HandlerExecutor(mPinnerHandler), mDeviceConfigAnonSizeListener); } @Override @@ -272,6 +285,10 @@ public final class PinnerService extends SystemService { if (DEBUG) { Slog.i(TAG, "Starting PinnerService"); } + mConfiguredMaxPinnedMemory = + (Process.getTotalMemory() + * Math.clamp(mConfiguredMaxPinnedMemoryPercentage, 0, 100)) + / 100; mBinderService = new BinderService(); mInjector.publishBinderService(this, mBinderService); publishLocalService(PinnerService.class, this); @@ -348,7 +365,7 @@ public final class PinnerService extends SystemService { protected PinnedFileStats(int uid, PinnedFile file) { this.uid = uid; this.filename = file.fileName.substring(file.fileName.lastIndexOf('/') + 1); - this.sizeKb = file.bytesPinned / 1024; + this.sizeKb = (int) file.bytesPinned / 1024; } } @@ -358,20 +375,11 @@ public final class PinnerService extends SystemService { private void handlePinOnStart() { // Files to pin come from the overlay and can be specified per-device config String[] filesToPin = mContext.getResources().getStringArray( - com.android.internal.R.array.config_defaultPinnerServiceFiles); + com.android.internal.R.array.config_defaultPinnerServiceFiles); // Continue trying to pin each file even if we fail to pin some of them for (String fileToPin : filesToPin) { - PinnedFile pf = mInjector.pinFileInternal(fileToPin, Integer.MAX_VALUE, - /*attemptPinIntrospection=*/false); - if (pf == null) { - Slog.e(TAG, "Failed to pin file = " + fileToPin); - continue; - } - synchronized (this) { - mPinnedFiles.put(pf.fileName, pf); - } - pf.groupName = "system"; - pinOptimizedDexDependencies(pf, Integer.MAX_VALUE, null); + pinFile(fileToPin, Integer.MAX_VALUE, /*appInfo=*/null, /*groupName=*/SYSTEM_GROUP_NAME, + true); } refreshPinAnonConfig(); @@ -383,10 +391,9 @@ public final class PinnerService extends SystemService { * regular home app. */ private void registerUserSetupCompleteListener() { - Uri userSetupCompleteUri = Settings.Secure.getUriFor( - Settings.Secure.USER_SETUP_COMPLETE); - mContext.getContentResolver().registerContentObserver(userSetupCompleteUri, - false, new ContentObserver(null) { + Uri userSetupCompleteUri = Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE); + mContext.getContentResolver().registerContentObserver( + userSetupCompleteUri, false, new ContentObserver(null) { @Override public void onChange(boolean selfChange, Uri uri) { if (userSetupCompleteUri.equals(uri)) { @@ -409,7 +416,7 @@ public final class PinnerService extends SystemService { } @Override - public void onUidActive(int uid) { + public void onUidActive(int uid) { mPinnerHandler.sendMessage(PooledLambda.obtainMessage( PinnerService::handleUidActive, PinnerService.this, uid)); } @@ -423,7 +430,6 @@ public final class PinnerService extends SystemService { updateActiveState(uid, false /* active */); int key; synchronized (this) { - // In case we have a pending repin, repin now. See mPendingRepin for more information. key = mPendingRepin.getOrDefault(uid, -1); if (key == -1) { @@ -491,8 +497,8 @@ public final class PinnerService extends SystemService { private ApplicationInfo getCameraInfo(int userHandle) { Intent cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA); - ApplicationInfo info = getApplicationInfoForIntent(cameraIntent, userHandle, - false /* defaultToSystemApp */); + ApplicationInfo info = getApplicationInfoForIntent( + cameraIntent, userHandle, false /* defaultToSystemApp */); // If the STILL_IMAGE_CAMERA intent doesn't resolve, try the _SECURE intent. // We don't use _SECURE first because it will never get set on a device @@ -501,16 +507,16 @@ public final class PinnerService extends SystemService { // preference using this intent. if (info == null) { cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE); - info = getApplicationInfoForIntent(cameraIntent, userHandle, - false /* defaultToSystemApp */); + info = getApplicationInfoForIntent( + cameraIntent, userHandle, false /* defaultToSystemApp */); } // If the _SECURE intent doesn't resolve, try the original intent but request // the system app for camera if there was more than one result. if (info == null) { cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA); - info = getApplicationInfoForIntent(cameraIntent, userHandle, - true /* defaultToSystemApp */); + info = getApplicationInfoForIntent( + cameraIntent, userHandle, true /* defaultToSystemApp */); } return info; } @@ -525,14 +531,14 @@ public final class PinnerService extends SystemService { return getApplicationInfoForIntent(intent, userHandle, true); } - private ApplicationInfo getApplicationInfoForIntent(Intent intent, int userHandle, - boolean defaultToSystemApp) { + private ApplicationInfo getApplicationInfoForIntent( + Intent intent, int userHandle, boolean defaultToSystemApp) { if (intent == null) { return null; } - ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivityAsUser(intent, - MATCH_FLAGS, userHandle); + ResolveInfo resolveInfo = + mContext.getPackageManager().resolveActivityAsUser(intent, MATCH_FLAGS, userHandle); // If this intent can resolve to only one app, choose that one. // Otherwise, if we've requested to default to the system app, return it; @@ -547,12 +553,11 @@ public final class PinnerService extends SystemService { } if (defaultToSystemApp) { - List<ResolveInfo> infoList = mContext.getPackageManager() - .queryIntentActivitiesAsUser(intent, MATCH_FLAGS, userHandle); + List<ResolveInfo> infoList = mContext.getPackageManager().queryIntentActivitiesAsUser( + intent, MATCH_FLAGS, userHandle); ApplicationInfo systemAppInfo = null; for (ResolveInfo info : infoList) { - if ((info.activityInfo.applicationInfo.flags - & ApplicationInfo.FLAG_SYSTEM) != 0) { + if ((info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { if (systemAppInfo == null) { systemAppInfo = info.activityInfo.applicationInfo; } else { @@ -568,13 +573,13 @@ public final class PinnerService extends SystemService { } private void sendPinAppsMessage(int userHandle) { - mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinApps, this, - userHandle)); + mPinnerHandler.sendMessage( + PooledLambda.obtainMessage(PinnerService::pinApps, this, userHandle)); } private void sendPinAppsWithUpdatedKeysMessage(int userHandle) { - mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinAppsWithUpdatedKeys, - this, userHandle)); + mPinnerHandler.sendMessage(PooledLambda.obtainMessage( + PinnerService::pinAppsWithUpdatedKeys, this, userHandle)); } private void sendUnpinAppsMessage() { mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::unpinApps, this)); @@ -586,8 +591,7 @@ public final class PinnerService extends SystemService { // phenotype property is not set. boolean shouldPinCamera = mConfiguredToPinCamera && mDeviceConfigInterface.getBoolean(DeviceConfig.NAMESPACE_RUNTIME_NATIVE_BOOT, - "pin_camera", - SystemProperties.getBoolean("pinner.pin_camera", true)); + "pin_camera", SystemProperties.getBoolean("pinner.pin_camera", true)); if (shouldPinCamera) { pinKeys.add(KEY_CAMERA); } else if (DEBUG) { @@ -626,8 +630,9 @@ public final class PinnerService extends SystemService { synchronized (this) { // This code path demands preceding unpinApps() call. if (!mPinnedApps.isEmpty()) { - Slog.e(TAG, "Attempted to update a list of apps, " - + "but apps were already pinned. Skipping."); + Slog.e(TAG, + "Attempted to update a list of apps, " + + "but apps were already pinned. Skipping."); return; } @@ -646,8 +651,8 @@ public final class PinnerService extends SystemService { * @see #pinApp(int, int, boolean) */ private void sendPinAppMessage(int key, int userHandle, boolean force) { - mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinApp, this, - key, userHandle, force)); + mPinnerHandler.sendMessage( + PooledLambda.obtainMessage(PinnerService::pinApp, this, key, userHandle, force)); } /** @@ -667,10 +672,10 @@ public final class PinnerService extends SystemService { } return; } - unpinApp(key); ApplicationInfo info = getInfoForKey(key, userHandle); + unpinApp(key); if (info != null) { - pinApp(key, info); + pinAppInternal(key, info); } } @@ -682,9 +687,7 @@ public final class PinnerService extends SystemService { private int getUidForKey(@AppKey int key) { synchronized (this) { PinnedApp existing = mPinnedApps.get(key); - return existing != null && existing.active - ? existing.uid - : -1; + return existing != null && existing.active ? existing.uid : -1; } } @@ -727,11 +730,8 @@ public final class PinnerService extends SystemService { * Handle any changes in the anon region pinner config. */ private void refreshPinAnonConfig() { - long newPinAnonSize = - mDeviceConfigInterface.getLong( - DEVICE_CONFIG_NAMESPACE_ANON_SIZE, - DEVICE_CONFIG_KEY_ANON_SIZE, - DEFAULT_ANON_SIZE); + long newPinAnonSize = mDeviceConfigInterface.getLong( + DEVICE_CONFIG_NAMESPACE_ANON_SIZE, DEVICE_CONFIG_KEY_ANON_SIZE, DEFAULT_ANON_SIZE); newPinAnonSize = Math.max(0, Math.min(newPinAnonSize, MAX_ANON_SIZE)); if (newPinAnonSize != mPinAnonSize) { mPinAnonSize = newPinAnonSize; @@ -765,10 +765,9 @@ public final class PinnerService extends SystemService { try { // Map as SHARED to avoid changing rss.anon for system_server (per /proc/*/status). // The mapping is visible in other rss metrics, and as private dirty in smaps/meminfo. - address = Os.mmap(0, alignedPinSize, - OsConstants.PROT_READ | OsConstants.PROT_WRITE, - OsConstants.MAP_SHARED | OsConstants.MAP_ANONYMOUS, - new FileDescriptor(), /*offset=*/0); + address = Os.mmap(0, alignedPinSize, OsConstants.PROT_READ | OsConstants.PROT_WRITE, + OsConstants.MAP_SHARED | OsConstants.MAP_ANONYMOUS, new FileDescriptor(), + /*offset=*/0); Unsafe tempUnsafe = null; Class<sun.misc.Unsafe> clazz = sun.misc.Unsafe.class; @@ -794,14 +793,14 @@ public final class PinnerService extends SystemService { return; } finally { if (address >= 0) { - safeMunmap(address, alignedPinSize); + PinnerUtils.safeMunmap(address, alignedPinSize); } } } private void unpinAnonRegion() { if (mPinAnonAddress != 0) { - safeMunmap(mPinAnonAddress, mCurrentlyPinnedAnonSize); + PinnerUtils.safeMunmap(mPinAnonAddress, mCurrentlyPinnedAnonSize); } mPinAnonAddress = 0; mCurrentlyPinnedAnonSize = 0; @@ -824,12 +823,20 @@ public final class PinnerService extends SystemService { } /** + * Retrieves remaining quota for pinner service, once it reaches 0 it will no longer + * pin any file. + */ + private long getAvailableGlobalQuota() { + return mConfiguredMaxPinnedMemory - mCurrentPinnedMemory; + } + + /** * Pins an application. * * @param key The key of the app to pin. * @param appInfo The corresponding app info. */ - private void pinApp(@AppKey int key, @Nullable ApplicationInfo appInfo) { + private void pinAppInternal(@AppKey int key, @Nullable ApplicationInfo appInfo) { if (appInfo == null) { return; } @@ -839,7 +846,6 @@ public final class PinnerService extends SystemService { mPinnedApps.put(key, pinnedApp); } - // pin APK final int pinSizeLimit = getSizeLimitForKey(key); List<String> apks = new ArrayList<>(); @@ -851,36 +857,31 @@ public final class PinnerService extends SystemService { } } - int apkPinSizeLimit = pinSizeLimit; - - boolean shouldSkipArtPins = key == KEY_HOME && skipHomeArtPins(); + long apkPinSizeLimit = pinSizeLimit; - for (String apk: apks) { + for (String apk : apks) { if (apkPinSizeLimit <= 0) { Slog.w(TAG, "Reached to the pin size limit. Skipping: " + apk); // Continue instead of break to print all skipped APK names. continue; } - PinnedFile pf = mInjector.pinFileInternal(apk, apkPinSizeLimit, /*attemptPinIntrospection=*/true); + String pinGroup = getNameForKey(key); + boolean shouldPinDeps = apk.equals(appInfo.sourceDir); + PinnedFile pf = pinFile(apk, apkPinSizeLimit, appInfo, pinGroup, shouldPinDeps); if (pf == null) { Slog.e(TAG, "Failed to pin " + apk); continue; } - pf.groupName = getNameForKey(key); if (DEBUG) { Slog.i(TAG, "Pinned " + pf.fileName); } synchronized (this) { pinnedApp.mFiles.add(pf); - mPinnedFiles.put(pf.fileName, pf); } apkPinSizeLimit -= pf.bytesPinned; - if (apk.equals(appInfo.sourceDir) && !shouldSkipArtPins) { - pinOptimizedDexDependencies(pf, Integer.MAX_VALUE, appInfo); - } } } @@ -892,19 +893,23 @@ public final class PinnerService extends SystemService { * that related to the file but not within itself. * * @param fileToPin File to pin - * @param maxBytesToPin maximum quota allowed for pinning - * @return total bytes that were pinned. + * @param bytesRequestedToPin maximum bytes requested to pin for {@code fileToPin}. + * @param pinOptimizedDeps whether optimized dependencies such as odex,vdex, etc be pinned. + * Note: {@code bytesRequestedToPin} limit will not apply to optimized + * dependencies pinned, only global quotas will apply instead. + * @return pinned file */ - public int pinFile(String fileToPin, int maxBytesToPin, @Nullable ApplicationInfo appInfo, - @Nullable String groupName) { + public PinnedFile pinFile(String fileToPin, long bytesRequestedToPin, + @Nullable ApplicationInfo appInfo, @Nullable String groupName, + boolean pinOptimizedDeps) { PinnedFile existingPin; - synchronized(this) { + synchronized (this) { existingPin = mPinnedFiles.get(fileToPin); } if (existingPin != null) { - if (existingPin.bytesPinned == maxBytesToPin) { + if (existingPin.bytesPinned == bytesRequestedToPin) { // Duplicate pin requesting same amount of bytes, lets just bail out. - return 0; + return null; } else { // User decided to pin a different amount of bytes than currently pinned // so this is a valid pin request. Unpin the previous version before repining. @@ -915,26 +920,38 @@ public final class PinnerService extends SystemService { } } + long remainingQuota = getAvailableGlobalQuota(); + + if (pinGlobalQuota()) { + if (remainingQuota <= 0) { + Slog.w(TAG, "Reached pin quota, skipping file: " + fileToPin); + return null; + } + bytesRequestedToPin = Math.min(bytesRequestedToPin, remainingQuota); + } + boolean isApk = fileToPin.endsWith(".apk"); - int bytesPinned = 0; - PinnedFile pf = mInjector.pinFileInternal(fileToPin, maxBytesToPin, + + PinnedFile pf = mInjector.pinFileInternal(this, fileToPin, bytesRequestedToPin, /*attemptPinIntrospection=*/isApk); if (pf == null) { Slog.e(TAG, "Failed to pin file = " + fileToPin); - return 0; + return null; } pf.groupName = groupName != null ? groupName : ""; - bytesPinned += pf.bytesPinned; - maxBytesToPin -= bytesPinned; + mCurrentPinnedMemory += pf.bytesPinned; synchronized (this) { mPinnedFiles.put(pf.fileName, pf); } - if (maxBytesToPin > 0) { - pinOptimizedDexDependencies(pf, maxBytesToPin, appInfo); + + if (pinOptimizedDeps) { + mCurrentPinnedMemory += + pinOptimizedDexDependencies(pf, getAvailableGlobalQuota(), appInfo); } - return bytesPinned; + + return pf; } /** @@ -945,13 +962,13 @@ public final class PinnerService extends SystemService { * to null it will use the default supported ABI by the device. * @return total bytes pinned. */ - private int pinOptimizedDexDependencies( - PinnedFile pinnedFile, int maxBytesToPin, @Nullable ApplicationInfo appInfo) { + private long pinOptimizedDexDependencies( + PinnedFile pinnedFile, long maxBytesToPin, @Nullable ApplicationInfo appInfo) { if (pinnedFile == null) { return 0; } - int bytesPinned = 0; + long bytesPinned = 0; if (pinnedFile.fileName.endsWith(".jar") | pinnedFile.fileName.endsWith(".apk")) { String abi = null; if (appInfo != null) { @@ -974,7 +991,7 @@ public final class PinnerService extends SystemService { // Unpin if it was already pinned prior to re-pinning. unpinFile(file); - PinnedFile df = mInjector.pinFileInternal(file, maxBytesToPin, + PinnedFile df = mInjector.pinFileInternal(this, file, maxBytesToPin, /*attemptPinIntrospection=*/false); if (df == null) { Slog.i(TAG, "Failed to pin ART file = " + file); @@ -992,7 +1009,8 @@ public final class PinnerService extends SystemService { return bytesPinned; } - /** mlock length bytes of fileToPin in memory + /** + * mlock length bytes of fileToPin in memory * * If attemptPinIntrospection is true, then treat the file to pin as a zip file and * look for a "pinlist.meta" file in the archive root directory. The structure of this @@ -1029,8 +1047,8 @@ public final class PinnerService extends SystemService { * zip in order to extract the * @return Pinned memory resource owner thing or null on error */ - private static PinnedFile pinFileInternal( - String fileToPin, int maxBytesToPin, boolean attemptPinIntrospection) { + private PinnedFile pinFileInternal( + String fileToPin, long maxBytesToPin, boolean attemptPinIntrospection) { if (DEBUG) { Slog.d(TAG, "pin file: " + fileToPin + " use-pinlist: " + attemptPinIntrospection); } @@ -1054,8 +1072,8 @@ public final class PinnerService extends SystemService { } return pinnedFile; } finally { - safeClose(pinRangeStream); - safeClose(fileAsZip); // Also closes any streams we've opened + PinnerUtils.safeClose(pinRangeStream); + PinnerUtils.safeClose(fileAsZip); // Also closes any streams we've opened } } @@ -1068,11 +1086,8 @@ public final class PinnerService extends SystemService { try { zip = new ZipFile(fileName); } catch (IOException ex) { - Slog.w(TAG, - String.format( - "could not open \"%s\" as zip: pinning as blob", - fileName), - ex); + Slog.w(TAG, String.format("could not open \"%s\" as zip: pinning as blob", fileName), + ex); } return zip; } @@ -1112,9 +1127,9 @@ public final class PinnerService extends SystemService { pinMetaStream = zipFile.getInputStream(pinMetaEntry); } catch (IOException ex) { Slog.w(TAG, - String.format("error reading pin metadata \"%s\": pinning as blob", - fileName), - ex); + String.format( + "error reading pin metadata \"%s\": pinning as blob", fileName), + ex); } } else { Slog.w(TAG, @@ -1124,57 +1139,6 @@ public final class PinnerService extends SystemService { return pinMetaStream; } - private static abstract class PinRangeSource { - /** Retrive a range to pin. - * - * @param outPinRange Receives the pin region - * @return True if we filled in outPinRange or false if we're out of pin entries - */ - abstract boolean read(PinRange outPinRange); - } - - private static final class PinRangeSourceStatic extends PinRangeSource { - private final int mPinStart; - private final int mPinLength; - private boolean mDone = false; - - PinRangeSourceStatic(int pinStart, int pinLength) { - mPinStart = pinStart; - mPinLength = pinLength; - } - - @Override - boolean read(PinRange outPinRange) { - outPinRange.start = mPinStart; - outPinRange.length = mPinLength; - boolean done = mDone; - mDone = true; - return !done; - } - } - - private static final class PinRangeSourceStream extends PinRangeSource { - private final DataInputStream mStream; - private boolean mDone = false; - - PinRangeSourceStream(InputStream stream) { - mStream = new DataInputStream(stream); - } - - @Override - boolean read(PinRange outPinRange) { - if (!mDone) { - try { - outPinRange.start = mStream.readInt(); - outPinRange.length = mStream.readInt(); - } catch (IOException ex) { - mDone = true; - } - } - return !mDone; - } - } - /** * Helper for pinFile. * @@ -1185,25 +1149,20 @@ public final class PinnerService extends SystemService { * @return PinnedFile or null on error */ private static PinnedFile pinFileRanges( - String fileToPin, - int maxBytesToPin, - PinRangeSource pinRangeSource) - { + String fileToPin, long maxBytesToPin, PinRangeSource pinRangeSource) { FileDescriptor fd = new FileDescriptor(); long address = -1; - int mapSize = 0; + long mapSize = 0; try { int openFlags = (OsConstants.O_RDONLY | OsConstants.O_CLOEXEC); fd = Os.open(fileToPin, openFlags, 0); mapSize = (int) Math.min(Os.fstat(fd).st_size, Integer.MAX_VALUE); - address = Os.mmap(0, mapSize, - OsConstants.PROT_READ, - OsConstants.MAP_SHARED, - fd, /*offset=*/0); + address = Os.mmap( + 0, mapSize, OsConstants.PROT_READ, OsConstants.MAP_SHARED, fd, /*offset=*/0); PinRange pinRange = new PinRange(); - int bytesPinned = 0; + long bytesPinned = 0; // We pin at page granularity, so make sure the limit is page-aligned if (maxBytesToPin % PAGE_SIZE != 0) { @@ -1211,10 +1170,10 @@ public final class PinnerService extends SystemService { } while (bytesPinned < maxBytesToPin && pinRangeSource.read(pinRange)) { - int pinStart = pinRange.start; - int pinLength = pinRange.length; - pinStart = clamp(0, pinStart, mapSize); - pinLength = clamp(0, pinLength, mapSize - pinStart); + long pinStart = pinRange.start; + long pinLength = pinRange.length; + pinStart = PinnerUtils.clamp(0, pinStart, mapSize); + pinLength = PinnerUtils.clamp(0, pinLength, mapSize - pinStart); pinLength = Math.min(maxBytesToPin - bytesPinned, pinLength); // mlock doesn't require the region to be page-aligned, but we snap the @@ -1229,14 +1188,13 @@ public final class PinnerService extends SystemService { if (pinLength % PAGE_SIZE != 0) { pinLength += PAGE_SIZE - pinLength % PAGE_SIZE; } - pinLength = clamp(0, pinLength, maxBytesToPin - bytesPinned); + pinLength = PinnerUtils.clamp(0, pinLength, maxBytesToPin - bytesPinned); if (pinLength > 0) { if (DEBUG) { Slog.d(TAG, - String.format( - "pinning at %s %s bytes of %s", - pinStart, pinLength, fileToPin)); + String.format("pinning at %s %s bytes of %s", pinStart, pinLength, + fileToPin)); } Os.mlock(address + pinStart, pinLength); } @@ -1244,15 +1202,15 @@ public final class PinnerService extends SystemService { } PinnedFile pinnedFile = new PinnedFile(address, mapSize, fileToPin, bytesPinned); - address = -1; // Ownership transferred + address = -1; // Ownership transferred return pinnedFile; } catch (ErrnoException ex) { Slog.e(TAG, "Could not pin file " + fileToPin, ex); return null; } finally { - safeClose(fd); + PinnerUtils.safeClose(fd); if (address >= 0) { - safeMunmap(address, mapSize); + PinnerUtils.safeMunmap(address, mapSize); } } } @@ -1273,81 +1231,50 @@ public final class PinnerService extends SystemService { } } - public void unpinFile(String filename) { + /** + * Unpin a file and its optimized dependencies. + * + * @param filename file to unpin. + * @return number of bytes unpinned, 0 in case of failure or nothing to unpin. + */ + public long unpinFile(String filename) { PinnedFile pinnedFile; synchronized (this) { pinnedFile = mPinnedFiles.get(filename); } if (pinnedFile == null) { // File not pinned, nothing to do. - return; + return 0; } + long unpinnedBytes = pinnedFile.bytesPinned; pinnedFile.close(); synchronized (this) { if (DEBUG) { Slog.d(TAG, "Unpinned file: " + filename); } + mCurrentPinnedMemory -= pinnedFile.bytesPinned; + mPinnedFiles.remove(pinnedFile.fileName); for (PinnedFile dep : pinnedFile.pinnedDeps) { if (dep == null) { continue; } + unpinnedBytes -= dep.bytesPinned; + mCurrentPinnedMemory -= dep.bytesPinned; mPinnedFiles.remove(dep.fileName); if (DEBUG) { Slog.d(TAG, "Unpinned dependency: " + dep.fileName); } } } - } - private static int clamp(int min, int value, int max) { - return Math.max(min, Math.min(value, max)); - } - - private static void safeMunmap(long address, long mapSize) { - try { - Os.munmap(address, mapSize); - } catch (ErrnoException ex) { - Slog.w(TAG, "ignoring error in unmap", ex); - } - } - - /** - * Close FD, swallowing irrelevant errors. - */ - private static void safeClose(@Nullable FileDescriptor fd) { - if (fd != null && fd.valid()) { - try { - Os.close(fd); - } catch (ErrnoException ex) { - // Swallow the exception: non-EBADF errors in close(2) - // indicate deferred paging write errors, which we - // don't care about here. The underlying file - // descriptor is always closed. - if (ex.errno == OsConstants.EBADF) { - throw new AssertionError(ex); - } - } - } - } - - /** - * Close closeable thing, swallowing errors. - */ - private static void safeClose(@Nullable Closeable thing) { - if (thing != null) { - try { - thing.close(); - } catch (IOException ex) { - Slog.w(TAG, "ignoring error closing resource: " + thing, ex); - } - } + return unpinnedBytes; } public List<PinnedFileStat> getPinnerStats() { ArrayList<PinnedFileStat> stats = new ArrayList<>(); Collection<PinnedFile> pinnedFiles; - synchronized(this) { + synchronized (this) { pinnedFiles = mPinnedFiles.values(); } for (PinnedFile pf : pinnedFiles) { @@ -1355,8 +1282,8 @@ public final class PinnerService extends SystemService { stats.add(stat); } if (mCurrentlyPinnedAnonSize > 0) { - stats.add(new PinnedFileStat(ANON_REGION_STAT_NAME, - mCurrentlyPinnedAnonSize, ANON_REGION_STAT_NAME)); + stats.add(new PinnedFileStat( + ANON_REGION_STAT_NAME, mCurrentlyPinnedAnonSize, ANON_REGION_STAT_NAME)); } return stats; } @@ -1364,71 +1291,124 @@ public final class PinnerService extends SystemService { public final class BinderService extends IPinnerService.Stub { @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; + if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) + return; HashSet<PinnedFile> shownPins = new HashSet<>(); - HashSet<String> groups = new HashSet<>(); - final int bytesPerMB = 1024 * 1024; + HashSet<String> shownGroups = new HashSet<>(); + HashSet<String> groupsToPrint = new HashSet<>(); + final double bytesPerMB = 1024 * 1024; + pw.format("Pinner Configs:\n"); + pw.format(" Total Pinner quota: %d%% of total device memory\n", + mConfiguredMaxPinnedMemoryPercentage); + pw.format(" Maximum Pinner quota: %d bytes (%.2f MB)\n", mConfiguredMaxPinnedMemory, + mConfiguredMaxPinnedMemory / bytesPerMB); + pw.format(" Max Home App Pin Bytes (without deps): %d\n", mConfiguredHomePinBytes); + pw.format("\nPinned Files:\n"); synchronized (PinnerService.this) { long totalSize = 0; + + // We print apps separately from regular pins as they contain extra information that + // other pins do not. for (int key : mPinnedApps.keySet()) { PinnedApp app = mPinnedApps.get(key); pw.print(getNameForKey(key)); - pw.print(" uid="); pw.print(app.uid); - pw.print(" active="); pw.print(app.active); + pw.print(" uid="); + pw.print(app.uid); + pw.print(" active="); + pw.print(app.active); + + if (!app.mFiles.isEmpty()) { + shownGroups.add(app.mFiles.getFirst().groupName); + } pw.println(); + long bytesPinnedForApp = 0; + long bytesPinnedForAppDeps = 0; for (PinnedFile pf : mPinnedApps.get(key).mFiles) { pw.print(" "); - pw.format("%s pinned:%d bytes (%d MB) pinlist:%b\n", pf.fileName, + pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b\n", pf.fileName, pf.bytesPinned, pf.bytesPinned / bytesPerMB, pf.used_pinlist); totalSize += pf.bytesPinned; + bytesPinnedForApp += pf.bytesPinned; shownPins.add(pf); for (PinnedFile dep : pf.pinnedDeps) { pw.print(" "); - pw.format("%s pinned:%d bytes (%d MB) pinlist:%b (Dependency)\n", dep.fileName, - dep.bytesPinned, dep.bytesPinned / bytesPerMB, dep.used_pinlist); + pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b (Dependency)\n", + dep.fileName, dep.bytesPinned, dep.bytesPinned / bytesPerMB, + dep.used_pinlist); totalSize += dep.bytesPinned; + bytesPinnedForAppDeps += dep.bytesPinned; shownPins.add(dep); } } + long bytesPinnedForAppAndDeps = bytesPinnedForApp + bytesPinnedForAppDeps; + pw.format("Total Pinned = %d (%.2f MB) [App=%d (%.2f MB), " + + "Dependencies=%d (%.2f MB)]\n\n", + bytesPinnedForAppAndDeps, bytesPinnedForAppAndDeps / bytesPerMB, + bytesPinnedForApp, bytesPinnedForApp / bytesPerMB, + bytesPinnedForAppDeps, bytesPinnedForAppDeps / bytesPerMB); } pw.println(); for (PinnedFile pinnedFile : mPinnedFiles.values()) { - if (!groups.contains(pinnedFile.groupName)) { - groups.add(pinnedFile.groupName); + if (!groupsToPrint.contains(pinnedFile.groupName) + && !shownGroups.contains(pinnedFile.groupName)) { + groupsToPrint.add(pinnedFile.groupName); } } - boolean firstPinInGroup = true; - for (String group : groups) { + + // Print all the non app groups. + for (String group : groupsToPrint) { List<PinnedFile> groupPins = getAllPinsForGroup(group); + pw.print("\nGroup:" + group); + long bytesPinnedForGroupNoDeps = 0; + long bytesPinnedForGroupDeps = 0; + pw.println(); for (PinnedFile pinnedFile : groupPins) { if (shownPins.contains(pinnedFile)) { - // Already showed in the dump and accounted for, skip. + // Already displayed and accounted for, skip. continue; } - if (firstPinInGroup) { - firstPinInGroup = false; - // Ensure we only print when there are pins for groups not yet shown - // in the pinned app section. - pw.print("Group:" + group); - pw.println(); - } - pw.format(" %s pinned:%d bytes (%d MB) pinlist:%b\n", pinnedFile.fileName, - pinnedFile.bytesPinned, pinnedFile.bytesPinned / bytesPerMB, - pinnedFile.used_pinlist); + pw.format(" %s pinned: %d bytes (%.2f MB) pinlist:%b\n", + pinnedFile.fileName, pinnedFile.bytesPinned, + pinnedFile.bytesPinned / bytesPerMB, pinnedFile.used_pinlist); totalSize += pinnedFile.bytesPinned; + bytesPinnedForGroupNoDeps += pinnedFile.bytesPinned; + shownPins.add(pinnedFile); + for (PinnedFile dep : pinnedFile.pinnedDeps) { + if (shownPins.contains(dep)) { + // Already displayed and accounted for, skip. + continue; + } + pw.print(" "); + pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b (Dependency)\n", + dep.fileName, dep.bytesPinned, dep.bytesPinned / bytesPerMB, + dep.used_pinlist); + totalSize += dep.bytesPinned; + bytesPinnedForGroupDeps += dep.bytesPinned; + shownPins.add(dep); + } } + long bytesPinnedForGroup = bytesPinnedForGroupNoDeps + bytesPinnedForGroupDeps; + pw.format("Total Pinned = %d (%.2f MB) [Main=%d (%.2f MB), " + + "Dependencies=%d (%.2f MB)]\n\n", + bytesPinnedForGroup, bytesPinnedForGroup / bytesPerMB, + bytesPinnedForGroupNoDeps, bytesPinnedForGroupNoDeps / bytesPerMB, + bytesPinnedForGroupDeps, bytesPinnedForGroupDeps / bytesPerMB); } pw.println(); if (mPinAnonAddress != 0) { - pw.format("Pinned anon region: %d (%d MB)\n", mCurrentlyPinnedAnonSize, mCurrentlyPinnedAnonSize / bytesPerMB); + pw.format("Pinned anon region: %d (%.2f MB)\n", mCurrentlyPinnedAnonSize, + mCurrentlyPinnedAnonSize / bytesPerMB); totalSize += mCurrentlyPinnedAnonSize; } - pw.format("Total pinned: %s bytes (%s MB)\n", totalSize, totalSize / bytesPerMB); + pw.format("Total pinned: %d bytes (%.2f MB)\n", totalSize, totalSize / bytesPerMB); + pw.format("Available Pinner quota: %d bytes (%.2f MB)\n", getAvailableGlobalQuota(), + getAvailableGlobalQuota() / bytesPerMB); pw.println(); if (!mPendingRepin.isEmpty()) { pw.print("Pending repin: "); for (int key : mPendingRepin.values()) { - pw.print(getNameForKey(key)); pw.print(' '); + pw.print(getNameForKey(key)); + pw.print(' '); } pw.println(); } @@ -1462,8 +1442,9 @@ public final class PinnerService extends SystemService { repin(); break; default: - printError(out, String.format( - "Unknown pinner command: %s. Supported commands: repin", command)); + printError(out, + String.format("Unknown pinner command: %s. Supported commands: repin", + command)); resultReceiver.send(-1, null); return; } @@ -1479,46 +1460,6 @@ public final class PinnerService extends SystemService { } } - @VisibleForTesting - public static final class PinnedFile implements AutoCloseable { - private long mAddress; - final int mapSize; - final String fileName; - final int bytesPinned; - - // Whether this file was pinned using a pinlist - boolean used_pinlist; - - // User defined group name for pinner accounting - String groupName = ""; - ArrayList<PinnedFile> pinnedDeps = new ArrayList<>(); - - PinnedFile(long address, int mapSize, String fileName, int bytesPinned) { - mAddress = address; - this.mapSize = mapSize; - this.fileName = fileName; - this.bytesPinned = bytesPinned; - } - - @Override - public void close() { - if (mAddress >= 0) { - safeMunmap(mAddress, mapSize); - mAddress = -1; - } - for (PinnedFile dep : pinnedDeps) { - if (dep != null) { - dep.close(); - } - } - } - - @Override - public void finalize() { - close(); - } - } - final static class PinRange { int start; int length; @@ -1528,7 +1469,6 @@ public final class PinnerService extends SystemService { * Represents an app that was pinned. */ private final class PinnedApp { - /** * The uid of the package being pinned. This stays constant while the package stays * installed. @@ -1557,11 +1497,9 @@ public final class PinnerService extends SystemService { @Override public void handleMessage(Message msg) { switch (msg.what) { - case PIN_ONSTART_MSG: - { + case PIN_ONSTART_MSG: { handlePinOnStart(); - } - break; + } break; default: super.handleMessage(msg); diff --git a/services/core/java/com/android/server/pinner/PinnerUtils.java b/services/core/java/com/android/server/pinner/PinnerUtils.java new file mode 100644 index 000000000000..a836a83dedab --- /dev/null +++ b/services/core/java/com/android/server/pinner/PinnerUtils.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pinner; + +import android.annotation.Nullable; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; +import android.util.Slog; + +import java.io.Closeable; +import java.io.FileDescriptor; +import java.io.IOException; + +/* package */ final class PinnerUtils { + private static final String TAG = "PinnerUtils"; + + public static long clamp(long min, long value, long max) { + return Math.max(min, Math.min(value, max)); + } + + public static void safeMunmap(long address, long mapSize) { + try { + Os.munmap(address, mapSize); + } catch (ErrnoException ex) { + Slog.w(TAG, "ignoring error in unmap", ex); + } + } + + /** + * Close FD, swallowing irrelevant errors. + */ + public static void safeClose(@Nullable FileDescriptor fd) { + if (fd != null && fd.valid()) { + try { + Os.close(fd); + } catch (ErrnoException ex) { + // Swallow the exception: non-EBADF errors in close(2) + // indicate deferred paging write errors, which we + // don't care about here. The underlying file + // descriptor is always closed. + if (ex.errno == OsConstants.EBADF) { + throw new AssertionError(ex); + } + } + } + } + + /** + * Close closeable thing, swallowing errors. + */ + public static void safeClose(@Nullable Closeable thing) { + if (thing != null) { + try { + thing.close(); + } catch (IOException ex) { + Slog.w(TAG, "ignoring error closing resource: " + thing, ex); + } + } + } +} diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java index 9ecc7b9a805d..1569fa0aa8d7 100644 --- a/services/core/java/com/android/server/pm/DexOptHelper.java +++ b/services/core/java/com/android/server/pm/DexOptHelper.java @@ -70,13 +70,13 @@ import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.IndentingPrintWriter; import com.android.server.LocalManagerRegistry; import com.android.server.LocalServices; -import com.android.server.PinnerService; import com.android.server.art.ArtManagerLocal; import com.android.server.art.DexUseManagerLocal; import com.android.server.art.ReasonMapping; import com.android.server.art.model.ArtFlags; import com.android.server.art.model.DexoptParams; import com.android.server.art.model.DexoptResult; +import com.android.server.pinner.PinnerService; import com.android.server.pm.PackageDexOptimizer.DexOptResult; import com.android.server.pm.dex.DexManager; import com.android.server.pm.dex.DexoptOptions; diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 1316df16027f..b1b1637c890b 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -50,6 +50,7 @@ import android.app.PendingIntent; import android.app.admin.DevicePolicyEventLogger; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManagerInternal; +import android.app.role.RoleManager; import android.content.Context; import android.content.Intent; import android.content.IntentSender; @@ -201,6 +202,9 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements Manifest.permission.USE_FULL_SCREEN_INTENT ); + private static final String ROLE_SYSTEM_APP_PROTECTION_SERVICE = + "android.app.role.SYSTEM_APP_PROTECTION_SERVICE"; + final PackageArchiver mPackageArchiver; private final Context mContext; @@ -1454,6 +1458,12 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements .createEvent(DevicePolicyEnums.UNINSTALL_PACKAGE) .setAdmin(callerPackageName) .write(); + } else if (isSystemAppProtectionRoleHolder(snapshot, userId, callingUid)) { + // Allow the SYSTEM_APP_PROTECTION_SERVICE role holder to silently uninstall, with a + // clean calling identity to get DELETE_PACKAGES permission + Binder.withCleanCallingIdentity(() -> + mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags) + ); } else { ApplicationInfo appInfo = snapshot.getApplicationInfo(callerPackageName, 0, userId); if (appInfo.targetSdkVersion >= Build.VERSION_CODES.P) { @@ -1475,6 +1485,29 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } } + private Boolean isSystemAppProtectionRoleHolder( + @NonNull Computer snapshot, int userId, int callingUid) { + if (!Flags.deletePackagesSilentlyBackport()) { + return false; + } + String holderPackageName = Binder.withCleanCallingIdentity(() -> { + RoleManager roleManager = mPm.mContext.getSystemService(RoleManager.class); + if (roleManager == null) { + return null; + } + List<String> holders = roleManager.getRoleHoldersAsUser( + ROLE_SYSTEM_APP_PROTECTION_SERVICE, UserHandle.of(userId)); + if (holders.isEmpty()) { + return null; + } + return holders.get(0); + }); + if (holderPackageName == null) { + return false; + } + return snapshot.getPackageUid(holderPackageName, /* flags= */ 0, userId) == callingUid; + } + @Override public void uninstallExistingPackage(VersionedPackage versionedPackage, String callerPackageName, IntentSender statusReceiver, int userId) { diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index df9f7fb3d6e5..5fc3e332b95c 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -1015,8 +1015,7 @@ public class PermissionManagerService extends IPermissionManager.Stub { permission, attributionSource, message, forDataDelivery, startDataDelivery, fromDatasource, attributedOp); // Finish any started op if some step in the attribution chain failed. - if (startDataDelivery && result != PermissionChecker.PERMISSION_GRANTED - && result != PermissionChecker.PERMISSION_SOFT_DENIED) { + if (startDataDelivery && result != PermissionChecker.PERMISSION_GRANTED) { if (attributedOp == AppOpsManager.OP_NONE) { finishDataDelivery(AppOpsManager.permissionToOpCode(permission), attributionSource.asState(), fromDatasource); diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java index 303828f94e8a..0cdf537b3455 100644 --- a/services/core/java/com/android/server/power/Notifier.java +++ b/services/core/java/com/android/server/power/Notifier.java @@ -53,6 +53,7 @@ import android.telephony.TelephonyManager; import android.util.EventLog; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.view.WindowManagerPolicyConstants; import com.android.internal.annotations.VisibleForTesting; @@ -512,8 +513,17 @@ public class Notifier { } // Start input as soon as we start waking up or going to sleep. - mInputManagerInternal.setInteractive(interactive); mInputMethodManagerInternal.setInteractive(interactive); + if (!mFlags.isPerDisplayWakeByTouchEnabled()) { + // Since wakefulness is a global property in original logic, all displays should + // be set to the same interactive state, matching system's global wakefulness + SparseBooleanArray displayInteractivities = new SparseBooleanArray(); + int[] displayIds = mDisplayManagerInternal.getDisplayIds().toArray(); + for (int displayId : displayIds) { + displayInteractivities.put(displayId, interactive); + } + mInputManagerInternal.setDisplayInteractivities(displayInteractivities); + } // Notify battery stats. try { 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 c6ef89dcff69..fd60e06c0762 100644 --- a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java +++ b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java @@ -42,6 +42,11 @@ public class PowerManagerFlags { Flags::improveWakelockLatency ); + private final FlagState mPerDisplayWakeByTouch = new FlagState( + Flags.FLAG_PER_DISPLAY_WAKE_BY_TOUCH, + Flags::perDisplayWakeByTouch + ); + /** Returns whether early-screen-timeout-detector is enabled on not. */ public boolean isEarlyScreenTimeoutDetectorEnabled() { return mEarlyScreenTimeoutDetectorFlagState.isEnabled(); @@ -55,6 +60,13 @@ public class PowerManagerFlags { } /** + * @return Whether per-display wake by touch is enabled or not. + */ + public boolean isPerDisplayWakeByTouchEnabled() { + return mPerDisplayWakeByTouch.isEnabled(); + } + + /** * dumps all flagstates * @param pw printWriter */ @@ -62,6 +74,7 @@ public class PowerManagerFlags { pw.println("PowerManagerFlags:"); pw.println(" " + mEarlyScreenTimeoutDetectorFlagState); pw.println(" " + mImproveWakelockLatency); + pw.println(" " + mPerDisplayWakeByTouch); } private static class FlagState { 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 3581b2fad1df..9cf3bb6df3db 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 @@ -17,4 +17,12 @@ flag { description: "Feature flag for tracking the optimizations to improve the latency of acquiring and releasing a wakelock." bug: "339590565" is_fixed_read_only: true -}
\ No newline at end of file +} + +flag { + name: "per_display_wake_by_touch" + namespace: "power" + description: "Feature flag to enable per-display wake by touch" + bug: "343295183" + is_fixed_read_only: true +} diff --git a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java index f047f564538d..ab630eef4644 100644 --- a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java +++ b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java @@ -17,6 +17,7 @@ package com.android.server.power.stats.wakeups; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM; +import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_BLUETOOTH; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SENSOR; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER; @@ -63,6 +64,7 @@ public class CpuWakeupStats { private static final String SUBSYSTEM_SOUND_TRIGGER_STRING = "Sound_trigger"; private static final String SUBSYSTEM_SENSOR_STRING = "Sensor"; private static final String SUBSYSTEM_CELLULAR_DATA_STRING = "Cellular_data"; + private static final String SUBSYSTEM_BLUETOOTH_STRING = "Bluetooth"; private static final String TRACE_TRACK_WAKEUP_ATTRIBUTION = "wakeup_attribution"; private static final long WAKEUP_WRITE_DELAY_MS = TimeUnit.SECONDS.toMillis(30); @@ -512,6 +514,8 @@ public class CpuWakeupStats { return CPU_WAKEUP_SUBSYSTEM_SENSOR; case SUBSYSTEM_CELLULAR_DATA_STRING: return CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA; + case SUBSYSTEM_BLUETOOTH_STRING: + return CPU_WAKEUP_SUBSYSTEM_BLUETOOTH; } return CPU_WAKEUP_SUBSYSTEM_UNKNOWN; } @@ -528,6 +532,8 @@ public class CpuWakeupStats { return SUBSYSTEM_SENSOR_STRING; case CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA: return SUBSYSTEM_CELLULAR_DATA_STRING; + case CPU_WAKEUP_SUBSYSTEM_BLUETOOTH: + return SUBSYSTEM_BLUETOOTH_STRING; case CPU_WAKEUP_SUBSYSTEM_UNKNOWN: return "Unknown"; } diff --git a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java index f78c4488cbfb..d206c66ed09a 100644 --- a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java +++ b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java @@ -99,6 +99,7 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve // True if needing to roll back only rebootless apexes when native crash happens private boolean mTwoPhaseRollbackEnabled; + /** @hide */ @VisibleForTesting public RollbackPackageHealthObserver(Context context, ApexManager apexManager) { mContext = context; @@ -123,7 +124,7 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve } } - RollbackPackageHealthObserver(Context context) { + public RollbackPackageHealthObserver(@NonNull Context context) { this(context, ApexManager.getInstance()); } @@ -239,8 +240,8 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve return false; } - @Override + @NonNull public String getUniqueIdentifier() { return NAME; } @@ -251,7 +252,7 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve } @Override - public boolean mayObservePackage(String packageName) { + public boolean mayObservePackage(@NonNull String packageName) { if (getAvailableRollbacks().isEmpty()) { return false; } @@ -281,12 +282,14 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve * This may cause {@code packages} to be rolled back if they crash too freqeuntly. */ @AnyThread - void startObservingHealth(List<String> packages, long durationMs) { + @NonNull + public void startObservingHealth(@NonNull List<String> packages, @NonNull long durationMs) { PackageWatchdog.getInstance(mContext).startObservingHealth(this, packages, durationMs); } @AnyThread - void notifyRollbackAvailable(RollbackInfo rollback) { + @NonNull + public void notifyRollbackAvailable(@NonNull RollbackInfo rollback) { mHandler.post(() -> { // Enable two-phase rollback when a rebootless apex rollback is made available. // We assume the rebootless apex is stable and is less likely to be the cause @@ -314,7 +317,7 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve * to check for native crashes and mitigate them if needed. */ @AnyThread - void onBootCompletedAsync() { + public void onBootCompletedAsync() { mHandler.post(()->onBootCompleted()); } diff --git a/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java b/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java index 79560ce27919..9cfed02f9355 100644 --- a/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java +++ b/services/core/java/com/android/server/rollback/WatchdogRollbackLogger.java @@ -51,6 +51,7 @@ import java.util.List; /** * This class handles the logic for logging Watchdog-triggered rollback events. + * @hide */ public final class WatchdogRollbackLogger { private static final String TAG = "WatchdogRollbackLogger"; diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java index b35a0a772ff2..74c1124e1f16 100644 --- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java @@ -216,13 +216,13 @@ import com.android.role.RoleManagerLocal; import com.android.server.BinderCallsStatsService; import com.android.server.LocalManagerRegistry; import com.android.server.LocalServices; -import com.android.server.PinnerService; -import com.android.server.PinnerService.PinnedFileStats; import com.android.server.SystemService; import com.android.server.SystemServiceManager; import com.android.server.am.MemoryStatUtil.MemoryStat; import com.android.server.health.HealthServiceWrapper; import com.android.server.notification.NotificationManagerService; +import com.android.server.pinner.PinnerService; +import com.android.server.pinner.PinnerService.PinnedFileStats; import com.android.server.pm.UserManagerInternal; import com.android.server.power.stats.KernelWakelockReader; import com.android.server.power.stats.KernelWakelockStats; diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java index c120fc7d82f5..6aed00e0e32b 100644 --- a/services/core/java/com/android/server/vibrator/VibratorController.java +++ b/services/core/java/com/android/server/vibrator/VibratorController.java @@ -57,8 +57,7 @@ final class VibratorController { // for a snippet of the current known vibrator state/info. private volatile VibratorInfo mVibratorInfo; private volatile boolean mVibratorInfoLoadSuccessful; - private volatile boolean mIsVibrating; - private volatile boolean mIsUnderExternalControl; + private volatile VibratorState mCurrentState; private volatile float mCurrentAmplitude; /** @@ -75,6 +74,11 @@ final class VibratorController { void onComplete(int vibratorId, long vibrationId); } + /** Representation of the vibrator state based on the interactions through this controller. */ + private enum VibratorState { + IDLE, VIBRATING, UNDER_EXTERNAL_CONTROL + } + VibratorController(int vibratorId, OnVibrationCompleteListener listener) { this(vibratorId, listener, new NativeWrapper()); } @@ -87,6 +91,7 @@ final class VibratorController { VibratorInfo.Builder vibratorInfoBuilder = new VibratorInfo.Builder(vibratorId); mVibratorInfoLoadSuccessful = mNativeWrapper.getInfo(vibratorInfoBuilder); mVibratorInfo = vibratorInfoBuilder.build(); + mCurrentState = VibratorState.IDLE; if (!mVibratorInfoLoadSuccessful) { Slog.e(TAG, @@ -106,7 +111,7 @@ final class VibratorController { return false; } // Notify its callback after new client registered. - notifyStateListener(listener, mIsVibrating); + notifyStateListener(listener, isVibrating(mCurrentState)); } return true; } finally { @@ -166,7 +171,7 @@ final class VibratorController { * automatically notified to any registered {@link IVibratorStateListener} on change. */ public boolean isVibrating() { - return mIsVibrating; + return isVibrating(mCurrentState); } /** @@ -184,11 +189,6 @@ final class VibratorController { return mCurrentAmplitude; } - /** Return {@code true} if this vibrator is under external control, false otherwise. */ - public boolean isUnderExternalControl() { - return mIsUnderExternalControl; - } - /** * Check against this vibrator capabilities. * @@ -214,7 +214,7 @@ final class VibratorController { /** * Set the vibrator control to be external or not, based on given flag. * - * <p>This will affect the state of {@link #isUnderExternalControl()}. + * <p>This will affect the state of {@link #isVibrating()}. */ public void setExternalControl(boolean externalControl) { Trace.traceBegin(TRACE_TAG_VIBRATOR, @@ -224,9 +224,11 @@ final class VibratorController { if (!mVibratorInfo.hasCapability(IVibrator.CAP_EXTERNAL_CONTROL)) { return; } + VibratorState newState = + externalControl ? VibratorState.UNDER_EXTERNAL_CONTROL : VibratorState.IDLE; synchronized (mLock) { - mIsUnderExternalControl = externalControl; mNativeWrapper.setExternalControl(externalControl); + updateStateAndNotifyListenersLocked(newState); } } finally { Trace.traceEnd(TRACE_TAG_VIBRATOR); @@ -264,7 +266,7 @@ final class VibratorController { if (mVibratorInfo.hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL)) { mNativeWrapper.setAmplitude(amplitude); } - if (mIsVibrating) { + if (mCurrentState == VibratorState.VIBRATING) { mCurrentAmplitude = amplitude; } } @@ -289,7 +291,7 @@ final class VibratorController { long duration = mNativeWrapper.on(milliseconds, vibrationId); if (duration > 0) { mCurrentAmplitude = -1; - notifyListenerOnVibrating(true); + updateStateAndNotifyListenersLocked(VibratorState.VIBRATING); } return duration; } @@ -319,7 +321,7 @@ final class VibratorController { vendorEffect.getAdaptiveScale(), vibrationId); if (duration > 0) { mCurrentAmplitude = -1; - notifyListenerOnVibrating(true); + updateStateAndNotifyListenersLocked(VibratorState.VIBRATING); } return duration; } finally { @@ -346,7 +348,7 @@ final class VibratorController { prebaked.getEffectStrength(), vibrationId); if (duration > 0) { mCurrentAmplitude = -1; - notifyListenerOnVibrating(true); + updateStateAndNotifyListenersLocked(VibratorState.VIBRATING); } return duration; } @@ -374,7 +376,7 @@ final class VibratorController { long duration = mNativeWrapper.compose(primitives, vibrationId); if (duration > 0) { mCurrentAmplitude = -1; - notifyListenerOnVibrating(true); + updateStateAndNotifyListenersLocked(VibratorState.VIBRATING); } return duration; } @@ -402,7 +404,7 @@ final class VibratorController { long duration = mNativeWrapper.composePwle(primitives, braking, vibrationId); if (duration > 0) { mCurrentAmplitude = -1; - notifyListenerOnVibrating(true); + updateStateAndNotifyListenersLocked(VibratorState.VIBRATING); } return duration; } @@ -422,7 +424,7 @@ final class VibratorController { synchronized (mLock) { mNativeWrapper.off(); mCurrentAmplitude = 0; - notifyListenerOnVibrating(false); + updateStateAndNotifyListenersLocked(VibratorState.IDLE); } } finally { Trace.traceEnd(TRACE_TAG_VIBRATOR); @@ -443,9 +445,8 @@ final class VibratorController { return "VibratorController{" + "mVibratorInfo=" + mVibratorInfo + ", mVibratorInfoLoadSuccessful=" + mVibratorInfoLoadSuccessful - + ", mIsVibrating=" + mIsVibrating + + ", mCurrentState=" + mCurrentState.name() + ", mCurrentAmplitude=" + mCurrentAmplitude - + ", mIsUnderExternalControl=" + mIsUnderExternalControl + ", mVibratorStateListeners count=" + mVibratorStateListeners.getRegisteredCallbackCount() + '}'; @@ -454,8 +455,7 @@ final class VibratorController { void dump(IndentingPrintWriter pw) { pw.println("Vibrator (id=" + mVibratorInfo.getId() + "):"); pw.increaseIndent(); - pw.println("isVibrating = " + mIsVibrating); - pw.println("isUnderExternalControl = " + mIsUnderExternalControl); + pw.println("currentState = " + mCurrentState.name()); pw.println("currentAmplitude = " + mCurrentAmplitude); pw.println("vibratorInfoLoadSuccessful = " + mVibratorInfoLoadSuccessful); pw.println("vibratorStateListener size = " @@ -464,14 +464,19 @@ final class VibratorController { pw.decreaseIndent(); } + /** + * Updates current vibrator state and notify listeners if {@link #isVibrating()} result changed. + */ @GuardedBy("mLock") - private void notifyListenerOnVibrating(boolean isVibrating) { - if (mIsVibrating != isVibrating) { - mIsVibrating = isVibrating; + private void updateStateAndNotifyListenersLocked(VibratorState state) { + boolean previousIsVibrating = isVibrating(mCurrentState); + final boolean newIsVibrating = isVibrating(state); + mCurrentState = state; + if (previousIsVibrating != newIsVibrating) { // The broadcast method is safe w.r.t. register/unregister listener methods, but lock // is required here to guarantee delivery order. mVibratorStateListeners.broadcast( - listener -> notifyStateListener(listener, isVibrating)); + listener -> notifyStateListener(listener, newIsVibrating)); } } @@ -483,6 +488,11 @@ final class VibratorController { } } + /** Returns true only if given state is not {@link VibratorState#IDLE}. */ + private static boolean isVibrating(VibratorState state) { + return state != VibratorState.IDLE; + } + /** Wrapper around the static-native methods of {@link VibratorController} for tests. */ @VisibleForTesting public static class NativeWrapper { diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 95c648334327..07473d10b217 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -809,17 +809,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { mCurrentExternalVibration.getDebugInfo().dump(proto, VibratorManagerServiceDumpProto.CURRENT_EXTERNAL_VIBRATION); } - - boolean isVibrating = false; - boolean isUnderExternalControl = false; for (int i = 0; i < mVibrators.size(); i++) { proto.write(VibratorManagerServiceDumpProto.VIBRATOR_IDS, mVibrators.keyAt(i)); - isVibrating |= mVibrators.valueAt(i).isVibrating(); - isUnderExternalControl |= mVibrators.valueAt(i).isUnderExternalControl(); } - proto.write(VibratorManagerServiceDumpProto.IS_VIBRATING, isVibrating); - proto.write(VibratorManagerServiceDumpProto.VIBRATOR_UNDER_EXTERNAL_CONTROL, - isUnderExternalControl); } mVibratorManagerRecords.dump(proto); mVibratorControlService.dump(proto); diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 4754ffb5cf6e..946b61ad5fd9 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -1468,7 +1468,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub || change == PACKAGE_TEMPORARY_CHANGE) { changed = true; if (doit) { - Slog.w(TAG, "Wallpaper uninstalled, removing: " + Slog.e(TAG, "Wallpaper uninstalled, removing: " + wallpaper.getComponent()); clearWallpaperLocked(wallpaper.mWhich, wallpaper.userId, false, null); } @@ -1491,7 +1491,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE); } catch (NameNotFoundException e) { - Slog.w(TAG, "Wallpaper component gone, removing: " + Slog.e(TAG, "Wallpaper component gone, removing: " + wallpaper.getComponent()); clearWallpaperLocked(wallpaper.mWhich, wallpaper.userId, false, null); } diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java index 67401530763b..ab5316f46d78 100644 --- a/services/core/java/com/android/server/webkit/SystemImpl.java +++ b/services/core/java/com/android/server/webkit/SystemImpl.java @@ -41,7 +41,8 @@ import android.webkit.WebViewZygote; import com.android.internal.util.XmlUtils; import com.android.server.LocalServices; -import com.android.server.PinnerService; +import com.android.server.pinner.PinnedFile; +import com.android.server.pinner.PinnerService; import org.xmlpull.v1.XmlPullParserException; @@ -318,8 +319,9 @@ public class SystemImpl implements SystemInterface { if (webviewPinQuota <= 0) { break; } - int bytesPinned = pinnerService.pinFile(apk, webviewPinQuota, appInfo, PIN_GROUP); - webviewPinQuota -= bytesPinned; + PinnedFile pf = pinnerService.pinFile( + apk, webviewPinQuota, appInfo, PIN_GROUP, /*pinOptimizedDeps=*/true); + webviewPinQuota -= pf.bytesPinned; } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 12d733fc8c1a..14e9180022a8 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -2154,7 +2154,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } mAtmService.mPackageConfigPersister.updateConfigIfNeeded(this, mUserId, packageName); - mActivityRecordInputSink = new ActivityRecordInputSink(this, sourceRecord); + final boolean appOptInTouchPassThrough = + options != null && options.isAllowPassThroughOnTouchOutside(); + mActivityRecordInputSink = new ActivityRecordInputSink( + this, sourceRecord, appOptInTouchPassThrough); mAppActivityEmbeddingSplitsEnabled = isAppActivityEmbeddingSplitsEnabled(); mAllowUntrustedEmbeddingStateSharing = getAllowUntrustedEmbeddingStateSharingProperty(); @@ -3171,14 +3174,23 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return getWindowConfiguration().canReceiveKeys() && !mWaitForEnteringPinnedMode; } - boolean isResizeable() { - return isResizeable(/* checkPictureInPictureSupport */ true); + /** + * Returns {@code true} if the fixed orientation, aspect ratio, resizability of this activity + * will be ignored. + */ + boolean isUniversalResizeable() { + return mWmService.mConstants.mIgnoreActivityOrientationRequest + && info.applicationInfo.category != ApplicationInfo.CATEGORY_GAME + // If the user preference respects aspect ratio, then it becomes non-resizable. + && !mAppCompatController.getAppCompatOverrides().getAppCompatAspectRatioOverrides() + .shouldApplyUserMinAspectRatioOverride(); } - boolean isResizeable(boolean checkPictureInPictureSupport) { + boolean isResizeable() { return mAtmService.mForceResizableActivities || ActivityInfo.isResizeableMode(info.resizeMode) - || (info.supportsPictureInPicture() && checkPictureInPictureSupport) + || info.supportsPictureInPicture() + || isUniversalResizeable() // If the activity can be embedded, it should inherit the bounds of task fragment. || isEmbedded(); } @@ -8162,11 +8174,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @Override @ActivityInfo.ScreenOrientation protected int getOverrideOrientation() { - final int candidateOrientation; - if (!mWmService.mConstants.mIgnoreActivityOrientationRequest - || info.applicationInfo.category == ApplicationInfo.CATEGORY_GAME) { - candidateOrientation = super.getOverrideOrientation(); - } else { + int candidateOrientation = super.getOverrideOrientation(); + if (isUniversalResizeable() && ActivityInfo.isFixedOrientation(candidateOrientation)) { candidateOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; } return mAppCompatController.getOrientationPolicy() @@ -10025,7 +10034,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } StringBuilder sb = new StringBuilder(128); sb.append("ActivityRecord{"); - sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(System.identityHashCode(this)); sb.append(" u"); sb.append(mUserId); sb.append(' '); diff --git a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java index 1a197875ba31..fa5beca31ec1 100644 --- a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java +++ b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java @@ -16,13 +16,18 @@ package com.android.server.wm; +import android.app.ActivityOptions; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; +import android.os.Build; import android.os.InputConfig; import android.view.InputWindowHandle; import android.view.SurfaceControl; import android.view.WindowManager; +import com.android.window.flags.Flags; + /** * Creates a InputWindowHandle that catches all touches that would otherwise pass through an * Activity. @@ -35,6 +40,21 @@ 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 + * {@link ActivityOptions#setAllowPassThroughOnTouchOutside}, which allows the to-be-launched + * cross-uid overlaying activity and other activities in that app to pass through touches. The + * activity needs to ensure that it trusts the overlaying app and its content is not vulnerable + * to UI redressing attacks. + * + * @see ActivityOptions#setAllowPassThroughOnTouchOutside + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT) + static final long ENABLE_OVERLAY_TOUCH_PASS_THROUGH_OPT_IN_ENFORCEMENT = 358129114L; + private final ActivityRecord mActivityRecord; private final boolean mIsCompatEnabled; private final String mName; @@ -42,13 +62,24 @@ class ActivityRecordInputSink { private InputWindowHandleWrapper mInputWindowHandleWrapper; private SurfaceControl mSurfaceControl; - ActivityRecordInputSink(ActivityRecord activityRecord, ActivityRecord sourceRecord) { + ActivityRecordInputSink(ActivityRecord activityRecord, ActivityRecord sourceRecord, + boolean appOptInTouchPassThrough) { mActivityRecord = activityRecord; mIsCompatEnabled = CompatChanges.isChangeEnabled(ENABLE_TOUCH_OPAQUE_ACTIVITIES, mActivityRecord.getUid()); mName = Integer.toHexString(System.identityHashCode(this)) + " ActivityRecordInputSink " + mActivityRecord.mActivityComponent.flattenToShortString(); - if (sourceRecord != null) { + + if (sourceRecord == null) { + return; + } + // If the source activity has target sdk 36+, it is required to opt in to receive + // pass-through touches from the overlaying activity. + final boolean isTouchPassThroughOptInEnforced = CompatChanges.isChangeEnabled( + ENABLE_OVERLAY_TOUCH_PASS_THROUGH_OPT_IN_ENFORCEMENT, + sourceRecord.getUid()); + if (!Flags.touchPassThroughOptIn() || !isTouchPassThroughOptInEnforced + || appOptInTouchPassThrough) { sourceRecord.mAllowedTouchUid = mActivityRecord.getUid(); } } diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index d29ff540e391..2ba300a71e38 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -100,6 +100,7 @@ import android.app.ProfilerInfo; import android.app.WaitResult; import android.app.WindowConfiguration; import android.compat.annotation.ChangeId; +import android.compat.annotation.Disabled; import android.compat.annotation.EnabledSince; import android.content.IIntentSender; import android.content.Intent; @@ -182,7 +183,7 @@ class ActivityStarter { * Feature flag for go/activity-security rules */ @ChangeId - @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) + @Disabled static final long ASM_RESTRICTIONS = 230590090L; private final ActivityTaskManagerService mService; @@ -1028,6 +1029,7 @@ class ActivityStarter { if (requestCode >= 0 && !sourceRecord.finishing) { resultRecord = sourceRecord; } + request.logMessage.append(" (sr=" + System.identityHashCode(sourceRecord) + ")"); } } diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java index 51ef87dcab1b..6946b6a71fab 100644 --- a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java @@ -114,9 +114,6 @@ class AppCompatAspectRatioPolicy { return mTransparentPolicy.getInheritedMinAspectRatio(); } final ActivityInfo info = mActivityRecord.info; - if (info.applicationInfo == null) { - return info.getMinAspectRatio(); - } final AppCompatAspectRatioOverrides aspectRatioOverrides = mAppCompatOverrides.getAppCompatAspectRatioOverrides(); if (aspectRatioOverrides.shouldApplyUserMinAspectRatioOverride()) { @@ -128,6 +125,9 @@ class AppCompatAspectRatioPolicy { mActivityRecord); if (!aspectRatioOverrides.shouldOverrideMinAspectRatio() && !shouldOverrideMinAspectRatioForCamera) { + if (mActivityRecord.isUniversalResizeable()) { + return 0; + } return info.getMinAspectRatio(); } @@ -170,6 +170,9 @@ class AppCompatAspectRatioPolicy { if (mTransparentPolicy.isRunning()) { return mTransparentPolicy.getInheritedMaxAspectRatio(); } + if (mActivityRecord.isUniversalResizeable()) { + return 0; + } return mActivityRecord.info.getMaxAspectRatio(); } diff --git a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java index 241390c12818..fbf9478b4fd9 100644 --- a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java @@ -170,7 +170,7 @@ class AppCompatCameraOverrides { * </ul> */ boolean shouldApplyFreeformTreatmentForCameraCompat() { - return Flags.cameraCompatForFreeform() && !isChangeEnabled(mActivityRecord, + return Flags.enableCameraCompatForDesktopWindowing() && !isChangeEnabled(mActivityRecord, OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT); } diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java index 67bfd7605128..5338c01666fe 100644 --- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java @@ -48,8 +48,9 @@ class AppCompatCameraPolicy { // without the need to restart the device. final boolean needsDisplayRotationCompatPolicy = wmService.mAppCompatConfiguration.isCameraCompatTreatmentEnabledAtBuildTime(); - final boolean needsCameraCompatFreeformPolicy = Flags.cameraCompatForFreeform() - && DesktopModeHelper.canEnterDesktopMode(wmService.mContext); + final boolean needsCameraCompatFreeformPolicy = + Flags.enableCameraCompatForDesktopWindowing() + && DesktopModeHelper.canEnterDesktopMode(wmService.mContext); if (needsDisplayRotationCompatPolicy || needsCameraCompatFreeformPolicy) { mCameraStateMonitor = new CameraStateMonitor(displayContent, wmService.mH); mActivityRefresher = new ActivityRefresher(wmService, wmService.mH); diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 2259b5a5b08c..515f148ac2ff 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -1120,7 +1120,9 @@ public class BackgroundActivityStartController { @Nullable Task targetTask, int launchFlags, int balCode, int callingUid, int realCallingUid, TaskDisplayArea preferredTaskDisplayArea) { // BAL Exception allowed in all cases - if (balCode == BAL_ALLOW_ALLOWLISTED_UID) { + if (balCode == BAL_ALLOW_ALLOWLISTED_UID + || (android.security.Flags.asmReintroduceGracePeriod() + && balCode == BAL_ALLOW_GRACE_PERIOD)) { return true; } @@ -1173,10 +1175,15 @@ public class BackgroundActivityStartController { ArrayList<Task> visibleTasks = displayArea.getVisibleTasks(); for (int i = 0; i < visibleTasks.size(); i++) { Task task = visibleTasks.get(i); - if (visibleTasks.size() == 1 && task.isActivityTypeHomeOrRecents()) { - bas.optedIn(task.getTopMostActivity()); - } else { + if (android.security.Flags.asmReintroduceGracePeriod()) { bas = checkTopActivityForAsm(task, callingUid, /*sourceRecord*/null, bas); + } else { + if (visibleTasks.size() == 1 && task.isActivityTypeHomeOrRecents()) { + bas.optedIn(task.getTopMostActivity()); + } else { + bas = checkTopActivityForAsm( + task, callingUid, /*sourceRecord*/null, bas); + } } } } diff --git a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java index e3232e08749e..d6caa1a248b4 100644 --- a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java +++ b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java @@ -124,7 +124,7 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa */ @VisibleForTesting boolean shouldApplyFreeformTreatmentForCameraCompat(@NonNull ActivityRecord activity) { - return Flags.cameraCompatForFreeform() && !activity.info.isChangeEnabled( + return Flags.enableCameraCompatForDesktopWindowing() && !activity.info.isChangeEnabled( ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT); } diff --git a/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java index 192469183a54..6e6f76aaa48c 100644 --- a/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java +++ b/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java @@ -188,10 +188,6 @@ public class DesktopAppCompatAspectRatioPolicy { } final ActivityInfo info = mActivityRecord.info; - if (info.applicationInfo == null) { - return info.getMinAspectRatio(); - } - final AppCompatAspectRatioOverrides aspectRatioOverrides = mAppCompatOverrides.getAppCompatAspectRatioOverrides(); if (shouldApplyUserMinAspectRatioOverride(task)) { @@ -203,6 +199,9 @@ public class DesktopAppCompatAspectRatioPolicy { && dc.mAppCompatCameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord); if (!aspectRatioOverrides.shouldOverrideMinAspectRatio() && !shouldOverrideMinAspectRatioForCamera) { + if (mActivityRecord.isUniversalResizeable()) { + return 0; + } return info.getMinAspectRatio(); } @@ -246,6 +245,9 @@ public class DesktopAppCompatAspectRatioPolicy { if (mTransparentPolicy.isRunning()) { return mTransparentPolicy.getInheritedMaxAspectRatio(); } + if (mActivityRecord.isUniversalResizeable()) { + return 0; + } return mActivityRecord.info.getMaxAspectRatio(); } diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index e6f6215b5f7f..1ac0bb0e41c6 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -2156,6 +2156,11 @@ public class DisplayPolicy { } mDecorInsets.invalidate(); mDecorInsets.mInfoForRotation[rotation].set(newInfo); + if (!mService.mDisplayEnabled) { + // There could be other pending changes during booting. It might be better to let the + // clients receive the new states earlier. + return true; + } return !sameConfigFrame; } diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java index ddbfd70ea4c4..d7dc4597c508 100644 --- a/services/core/java/com/android/server/wm/InputMonitor.java +++ b/services/core/java/com/android/server/wm/InputMonitor.java @@ -222,7 +222,8 @@ final class InputMonitor { UserHandle clientUser) { final InputConsumerImpl existingConsumer = getInputConsumer(name); if (existingConsumer != null && existingConsumer.mClientUser.equals(clientUser)) { - throw new IllegalStateException("Existing input consumer found with name: " + name + destroyInputConsumer(existingConsumer.mToken); + Slog.w(TAG_WM, "Replacing existing input consumer found with name: " + name + ", display: " + mDisplayId + ", user: " + clientUser); } diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index 9da848aa05d8..bf623b2e2105 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -24,6 +24,8 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; @@ -2040,10 +2042,15 @@ class RecentTasks { final boolean isOtherUndefinedMode = otherWindowingMode == WINDOWING_MODE_UNDEFINED; // An activity type and windowing mode is compatible if they are the exact same type/mode, - // or if one of the type/modes is undefined + // or if one of the type/modes is undefined. This is with the exception of + // freeform/fullscreen where both modes are assumed to be compatible with each other. final boolean isCompatibleType = activityType == otherActivityType || isUndefinedType || isOtherUndefinedType; final boolean isCompatibleMode = windowingMode == otherWindowingMode + || (windowingMode == WINDOWING_MODE_FREEFORM + && otherWindowingMode == WINDOWING_MODE_FULLSCREEN) + || (windowingMode == WINDOWING_MODE_FULLSCREEN + && otherWindowingMode == WINDOWING_MODE_FREEFORM) || isUndefinedMode || isOtherUndefinedMode; return isCompatibleType && isCompatibleMode; diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 86bb75ab3f8c..edbc32827c1a 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -66,6 +66,7 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_LOCKTASK; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS; +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN; import static com.android.server.wm.ActivityRecord.State.PAUSED; import static com.android.server.wm.ActivityRecord.State.PAUSING; import static com.android.server.wm.ActivityRecord.State.RESUMED; @@ -1474,7 +1475,7 @@ class Task extends TaskFragment { // The starting window should keep covering its task when a pure TaskFragment is added // because its bounds may not fill the task. final ActivityRecord top = getTopMostActivity(); - if (top != null) { + if (top != null && !top.hasFixedRotationTransform()) { top.associateStartingWindowWithTaskIfNeeded(); } } @@ -4706,8 +4707,13 @@ class Task extends TaskFragment { // If the moveToFront is a part of finishing transition, then make sure // the z-order of tasks are up-to-date. if (topActivity.mTransitionController.inFinishingTransition(topActivity)) { - Transition.assignLayers(taskDisplayArea, - taskDisplayArea.getPendingTransaction()); + final SurfaceControl.Transaction tx = + taskDisplayArea.getPendingTransaction(); + Transition.assignLayers(taskDisplayArea, tx); + final SurfaceControl leash = topActivity.getFixedRotationLeash(); + if (leash != null) { + tx.setLayer(leash, topActivity.getLastLayer()); + } } } } @@ -6177,6 +6183,8 @@ class Task extends TaskFragment { void maybeApplyLastRecentsAnimationTransaction() { if (mLastRecentsAnimationTransaction != null) { + ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN, + "Applying last recents animation transaction."); final SurfaceControl.Transaction tx = getPendingTransaction(); if (mLastRecentsAnimationOverlay != null) { tx.reparent(mLastRecentsAnimationOverlay, mSurfaceControl); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index b8f47cce6005..942634704ff5 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -9007,7 +9007,9 @@ public class WindowManagerService extends IWindowManager.Stub final boolean isInputTargetNotFocused = mFocusedInputTarget != t && mFocusedInputTarget != null; - if (!isInputTargetNotFocused) { + final boolean isTouchOnFocusedDisplay = mFocusedInputTarget != null + && t.getDisplayId() == mFocusedInputTarget.getDisplayId(); + if (!(isInputTargetNotFocused && isTouchOnFocusedDisplay)) { return false; } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 476443aa2050..f35f2b30c5d4 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -799,7 +799,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } } finally { if (deferTransitionReady) { - chain.mTransition.continueTransitionReady(); + if (chain.mTransition.isCollecting()) { + chain.mTransition.continueTransitionReady(); + } else { + Slog.wtf(TAG, "Too late, transition : " + chain.mTransition.getSyncId() + + " state: " + chain.mTransition.getState() + " is not collecting"); + } } mService.mTaskSupervisor.setDeferRootVisibilityUpdate(false /* deferUpdate */); if (deferResume) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 4c4b4f65edf5..0e3ab63aefb9 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1969,6 +1969,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP boolean isReadyForDisplay() { final boolean parentAndClientVisible = !isParentWindowHidden() && mViewVisibility == View.VISIBLE; + // TODO(b/338426357): Remove this once the last target using legacy transitions is moved to + // shell transitions + if (!mTransitionController.isShellTransitionsEnabled()) { + return mHasSurface && isVisibleByPolicy() && !mDestroying + && ((parentAndClientVisible && mToken.isVisible()) + || isAnimating(TRANSITION | PARENTS)); + } return mHasSurface && isVisibleByPolicy() && !mDestroying && mToken.isVisible() && (parentAndClientVisible || isAnimating(TRANSITION | PARENTS)); } diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 5cd117b512d4..efca90217e83 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -56,6 +56,7 @@ #include <nativehelper/ScopedPrimitiveArray.h> #include <nativehelper/ScopedUtfChars.h> #include <server_configurable_flags/get_flags.h> +#include <ui/LogicalDisplayId.h> #include <ui/Region.h> #include <utils/Log.h> #include <utils/Looper.h> @@ -64,6 +65,7 @@ #include <atomic> #include <cinttypes> +#include <map> #include <vector> #include "android_hardware_display_DisplayViewport.h" @@ -343,7 +345,7 @@ public: void setTouchpadRightClickZoneEnabled(bool enabled); void setInputDeviceEnabled(uint32_t deviceId, bool enabled); void setShowTouches(bool enabled); - void setInteractive(bool interactive); + void setNonInteractiveDisplays(const std::set<ui::LogicalDisplayId>& displayIds); void reloadCalibration(); void reloadPointerIcons(); void requestPointerCapture(const sp<IBinder>& windowToken, bool enabled); @@ -508,9 +510,11 @@ private: // Keycodes to be remapped. std::map<int32_t /* fromKeyCode */, int32_t /* toKeyCode */> keyRemapping{}; + + // Displays which are non-interactive. + std::set<ui::LogicalDisplayId> nonInteractiveDisplays; } mLocked GUARDED_BY(mLock); - std::atomic<bool> mInteractive; void updateInactivityTimeoutLocked(); void handleInterceptActions(jint wmActions, nsecs_t when, uint32_t& policyFlags); void ensureSpriteControllerLocked(); @@ -524,12 +528,13 @@ private: void forEachPointerControllerLocked(std::function<void(PointerController&)> apply) REQUIRES(mLock); PointerIcon loadPointerIcon(JNIEnv* env, ui::LogicalDisplayId displayId, PointerIconStyle type); + bool isDisplayInteractive(ui::LogicalDisplayId displayId); static inline JNIEnv* jniEnv() { return AndroidRuntime::getJNIEnv(); } }; NativeInputManager::NativeInputManager(jobject serviceObj, const sp<Looper>& looper) - : mLooper(looper), mInteractive(true) { + : mLooper(looper) { JNIEnv* env = jniEnv(); mServiceObj = env->NewGlobalRef(serviceObj); @@ -547,9 +552,13 @@ NativeInputManager::~NativeInputManager() { void NativeInputManager::dump(std::string& dump) { dump += "Input Manager State:\n"; - dump += StringPrintf(INDENT "Interactive: %s\n", toString(mInteractive.load())); { // acquire lock std::scoped_lock _l(mLock); + auto logicalDisplayIdToString = [](const ui::LogicalDisplayId& displayId) { + return std::to_string(displayId.val()); + }; + dump += StringPrintf(INDENT "Display not interactive: %s\n", + dumpSet(mLocked.nonInteractiveDisplays, streamableToString).c_str()); dump += StringPrintf(INDENT "System UI Lights Out: %s\n", toString(mLocked.systemUiLightsOut)); dump += StringPrintf(INDENT "Pointer Speed: %" PRId32 "\n", mLocked.pointerSpeed); @@ -1476,8 +1485,10 @@ void NativeInputManager::requestPointerCapture(const sp<IBinder>& windowToken, b mInputManager->getDispatcher().requestPointerCapture(windowToken, enabled); } -void NativeInputManager::setInteractive(bool interactive) { - mInteractive = interactive; +void NativeInputManager::setNonInteractiveDisplays( + const std::set<ui::LogicalDisplayId>& displayIds) { + std::scoped_lock _l(mLock); + mLocked.nonInteractiveDisplays = displayIds; } void NativeInputManager::reloadCalibration() { @@ -1606,7 +1617,7 @@ void NativeInputManager::interceptKeyBeforeQueueing(const KeyEvent& keyEvent, // - Ignore untrusted events and pass them along. // - Ask the window manager what to do with normal events and trusted injected events. // - For normal events wake and brighten the screen if currently off or dim. - const bool interactive = mInteractive.load(); + const bool interactive = isDisplayInteractive(keyEvent.getDisplayId()); if (interactive) { policyFlags |= POLICY_FLAG_INTERACTIVE; } @@ -1644,7 +1655,7 @@ void NativeInputManager::interceptMotionBeforeQueueing(ui::LogicalDisplayId disp // - No special filtering for injected events required at this time. // - Filter normal events based on screen state. // - For normal events brighten (but do not wake) the screen if currently dim. - const bool interactive = mInteractive.load(); + const bool interactive = isDisplayInteractive(displayId); if (interactive) { policyFlags |= POLICY_FLAG_INTERACTIVE; } @@ -1683,6 +1694,24 @@ void NativeInputManager::handleInterceptActions(jint wmActions, nsecs_t when, } } +bool NativeInputManager::isDisplayInteractive(ui::LogicalDisplayId displayId) { + // If an input event doesn't have an associated id, use the default display id + if (displayId == ui::LogicalDisplayId::INVALID) { + displayId = ui::LogicalDisplayId::DEFAULT; + } + + { // acquire lock + std::scoped_lock _l(mLock); + + auto it = mLocked.nonInteractiveDisplays.find(displayId); + if (it != mLocked.nonInteractiveDisplays.end()) { + return false; + } + } // release lock + + return true; +} + nsecs_t NativeInputManager::interceptKeyBeforeDispatching(const sp<IBinder>& token, const KeyEvent& keyEvent, uint32_t policyFlags) { @@ -2372,10 +2401,17 @@ static void nativeSetShowTouches(JNIEnv* env, jobject nativeImplObj, jboolean en im->setShowTouches(enabled); } -static void nativeSetInteractive(JNIEnv* env, jobject nativeImplObj, jboolean interactive) { +static void nativeSetNonInteractiveDisplays(JNIEnv* env, jobject nativeImplObj, + jintArray displayIds) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); - im->setInteractive(interactive); + const std::vector displayIdsVec = getIntArray(env, displayIds); + std::set<ui::LogicalDisplayId> logicalDisplayIds; + for (int displayId : displayIdsVec) { + logicalDisplayIds.emplace(ui::LogicalDisplayId{displayId}); + } + + im->setNonInteractiveDisplays(logicalDisplayIds); } static void nativeReloadCalibration(JNIEnv* env, jobject nativeImplObj) { @@ -3021,7 +3057,7 @@ static const JNINativeMethod gInputManagerMethods[] = { (void*)nativeSetShouldNotifyTouchpadHardwareState}, {"setTouchpadRightClickZoneEnabled", "(Z)V", (void*)nativeSetTouchpadRightClickZoneEnabled}, {"setShowTouches", "(Z)V", (void*)nativeSetShowTouches}, - {"setInteractive", "(Z)V", (void*)nativeSetInteractive}, + {"setNonInteractiveDisplays", "([I)V", (void*)nativeSetNonInteractiveDisplays}, {"reloadCalibration", "()V", (void*)nativeReloadCalibration}, {"vibrate", "(I[J[III)V", (void*)nativeVibrate}, {"vibrateCombined", "(I[JLandroid/util/SparseArray;II)V", (void*)nativeVibrateCombined}, diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd index 0eafb59bdeac..a07facf79423 100644 --- a/services/core/xsd/display-device-config/display-device-config.xsd +++ b/services/core/xsd/display-device-config/display-device-config.xsd @@ -512,8 +512,6 @@ <xs:annotation name="final"/> </xs:element> </xs:sequence> - <!-- valid value of interpolation if specified: linear --> - <xs:attribute name="interpolation" type="xs:string" use="optional"/> </xs:complexType> <xs:complexType name="brightnessPoint"> diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt index 355b0ab15a62..5309263ed87c 100644 --- a/services/core/xsd/display-device-config/schema/current.txt +++ b/services/core/xsd/display-device-config/schema/current.txt @@ -91,8 +91,6 @@ package com.android.server.display.config { public class ComprehensiveBrightnessMap { ctor public ComprehensiveBrightnessMap(); method @NonNull public final java.util.List<com.android.server.display.config.BrightnessPoint> getBrightnessPoint(); - method public String getInterpolation(); - method public void setInterpolation(String); } public class Density { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index b6a4481902ab..4e89b85305d1 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -4152,8 +4152,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } private void checkAllUsersAreAffiliatedWithDevice() { - Preconditions.checkCallAuthorization(areAllUsersAffiliatedWithDeviceLocked(), - "operation not allowed when device has unaffiliated users"); + synchronized (getLockObject()) { + Preconditions.checkCallAuthorization(areAllUsersAffiliatedWithDeviceLocked(), + "operation not allowed when device has unaffiliated users"); + } } @Override @@ -11362,7 +11364,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (mOwners.hasDeviceOwner()) { return false; } - + final ComponentName profileOwner = getProfileOwnerAsUser(userId); if (profileOwner == null) { return false; @@ -11371,7 +11373,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (isManagedProfile(userId)) { return false; } - + return true; } private void enforceCanQueryLockTaskLocked(ComponentName who, String callerPackageName) { @@ -18213,6 +18215,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return false; } + @GuardedBy("getLockObject()") private boolean areAllUsersAffiliatedWithDeviceLocked() { return mInjector.binderWithCleanCallingIdentity(() -> { final List<UserInfo> userInfos = mUserManager.getAliveUsers(); @@ -18310,10 +18313,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final CallerIdentity caller = getCallerIdentity(admin, packageName); if (isPermissionCheckFlagEnabled()) { - Preconditions.checkCallAuthorization(isOrganizationOwnedDeviceWithManagedProfile() - || areAllUsersAffiliatedWithDeviceLocked()); - enforcePermission(MANAGE_DEVICE_POLICY_SECURITY_LOGGING, caller.getPackageName(), - UserHandle.USER_ALL); + synchronized (getLockObject()) { + Preconditions.checkCallAuthorization(isOrganizationOwnedDeviceWithManagedProfile() + || areAllUsersAffiliatedWithDeviceLocked()); + enforcePermission(MANAGE_DEVICE_POLICY_SECURITY_LOGGING, caller.getPackageName(), + UserHandle.USER_ALL); + } } else { if (admin != null) { Preconditions.checkCallAuthorization( @@ -18325,8 +18330,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { isCallerDelegate(caller, DELEGATION_SECURITY_LOGGING)); } - Preconditions.checkCallAuthorization(isOrganizationOwnedDeviceWithManagedProfile() - || areAllUsersAffiliatedWithDeviceLocked()); + synchronized (getLockObject()) { + Preconditions.checkCallAuthorization(isOrganizationOwnedDeviceWithManagedProfile() + || areAllUsersAffiliatedWithDeviceLocked()); + } } DevicePolicyEventLogger @@ -24540,7 +24547,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } }); } - + + @GuardedBy("getLockObject()") private void migrateUserControlDisabledPackagesLocked() { Binder.withCleanCallingIdentity(() -> { List<UserInfo> users = mUserManager.getUsers(); diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 5cf260adece6..ce6f1ecc9463 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -205,6 +205,7 @@ import com.android.server.os.SchedulingPolicyService; import com.android.server.pdb.PersistentDataBlockService; import com.android.server.people.PeopleService; import com.android.server.permission.access.AccessCheckingService; +import com.android.server.pinner.PinnerService; import com.android.server.pm.ApexManager; import com.android.server.pm.ApexSystemServiceInfo; import com.android.server.pm.BackgroundInstallControlService; diff --git a/services/profcollect/src/com/android/server/profcollect/Utils.java b/services/profcollect/src/com/android/server/profcollect/Utils.java index 850880256cfa..b4e254442a19 100644 --- a/services/profcollect/src/com/android/server/profcollect/Utils.java +++ b/services/profcollect/src/com/android/server/profcollect/Utils.java @@ -19,6 +19,7 @@ package com.android.server.profcollect; import static com.android.server.profcollect.ProfcollectForwardingService.LOG_TAG; import android.os.RemoteException; +import android.os.ServiceSpecificException; import android.provider.DeviceConfig; import android.util.Log; @@ -42,7 +43,7 @@ public final class Utils { BackgroundThread.get().getThreadHandler().post(() -> { try { mIProfcollect.trace_system(eventName); - } catch (RemoteException e) { + } catch (RemoteException | ServiceSpecificException e) { Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage()); } }); @@ -56,7 +57,7 @@ public final class Utils { BackgroundThread.get().getThreadHandler().postDelayed(() -> { try { mIProfcollect.trace_system(eventName); - } catch (RemoteException e) { + } catch (RemoteException | ServiceSpecificException e) { Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage()); } }, delayMs); @@ -73,10 +74,10 @@ public final class Utils { mIProfcollect.trace_process(eventName, processName, durationMs); - } catch (RemoteException e) { + } catch (RemoteException | ServiceSpecificException e) { Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage()); } }); return true; } -}
\ No newline at end of file +} diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt index b21c34905bad..2144785ed8fd 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt @@ -94,6 +94,7 @@ class PackageStateTest { ParsedService::getIntents, ParsedService::getProperties, Intent::getCategories, + Intent::getExtraIntentKeys, PackageUserState::getDisabledComponents, PackageUserState::getEnabledComponents, PackageUserState::getSharedLibraryOverlayPaths, diff --git a/services/tests/RemoteProvisioningServiceTests/Android.bp b/services/tests/RemoteProvisioningServiceTests/Android.bp index 19c913620760..3a73c3954d52 100644 --- a/services/tests/RemoteProvisioningServiceTests/Android.bp +++ b/services/tests/RemoteProvisioningServiceTests/Android.bp @@ -31,7 +31,6 @@ android_test { "service-rkp.impl", "services.core", "truth", - "truth-java8-extension", ], test_suites: [ "device-tests", diff --git a/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java b/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java index 007c0db1b731..a1616c676dbd 100644 --- a/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java +++ b/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java @@ -17,7 +17,6 @@ package com.android.server.security.rkp; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth8.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; diff --git a/services/tests/appfunctions/src/android/app/appfunctions/GenericDocumentWrapperTest.kt b/services/tests/appfunctions/src/android/app/appfunctions/GenericDocumentWrapperTest.kt new file mode 100644 index 000000000000..413eb314c41d --- /dev/null +++ b/services/tests/appfunctions/src/android/app/appfunctions/GenericDocumentWrapperTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.appfunctions + +import android.app.appsearch.GenericDocument +import android.os.Parcel +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + + +@RunWith(JUnit4::class) +class GenericDocumentWrapperTest { + + @Test + fun parcelUnparcel() { + val doc = + GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "") + .setPropertyLong("test", 42) + .build() + val wrapper = GenericDocumentWrapper(doc) + + val recovered = parcelUnparcel(wrapper) + + assertThat(recovered.value.getPropertyLong("test")).isEqualTo(42) + } + + @Test + fun parcelUnparcel_afterGetValue() { + val doc = + GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "") + .setPropertyLong("test", 42) + .build() + val wrapper = GenericDocumentWrapper(doc) + assertThat(wrapper.value.getPropertyLong("test")).isEqualTo(42) + + val recovered = parcelUnparcel(wrapper) + + assertThat(recovered.value.getPropertyLong("test")).isEqualTo(42) + } + + + @Test + fun getValue() { + val doc = + GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "") + .setPropertyLong("test", 42) + .build() + val wrapper = GenericDocumentWrapper(doc) + + assertThat(wrapper.value.getPropertyLong("test")).isEqualTo(42) + } + + private fun parcelUnparcel(obj: GenericDocumentWrapper): GenericDocumentWrapper { + val parcel = Parcel.obtain() + try { + obj.writeToParcel(parcel, 0) + parcel.setDataPosition(0) + return GenericDocumentWrapper.CREATOR.createFromParcel(parcel) + } finally { + parcel.recycle() + } + } +}
\ No newline at end of file diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java index fd05b26c320b..8e1be9a777fd 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -392,7 +392,7 @@ public final class DisplayDeviceConfigTest { public void testInvalidLuxThrottling() throws Exception { setupDisplayDeviceConfigFromDisplayConfigFile( getContent(getInvalidLuxThrottling(), getValidProxSensor(), - /* includeIdleMode= */ true, /* enableEvenDimmer */ false)); + /* includeIdleMode= */ true, /* enableEvenDimmer= */ false)); Map<DisplayDeviceConfig.BrightnessLimitMapType, Map<Float, Float>> luxThrottlingData = mDisplayDeviceConfig.getLuxThrottlingData(); @@ -600,7 +600,7 @@ public final class DisplayDeviceConfigTest { public void testProximitySensorWithEmptyValuesFromDisplayConfig() throws IOException { setupDisplayDeviceConfigFromDisplayConfigFile( getContent(getValidLuxThrottling(), getProxSensorWithEmptyValues(), - /* includeIdleMode= */ true, /* enableEvenDimmer */ false)); + /* includeIdleMode= */ true, /* enableEvenDimmer= */ false)); assertNull(mDisplayDeviceConfig.getProximitySensor()); } @@ -608,7 +608,7 @@ public final class DisplayDeviceConfigTest { public void testProximitySensorWithRefreshRatesFromDisplayConfig() throws IOException { setupDisplayDeviceConfigFromDisplayConfigFile( getContent(getValidLuxThrottling(), getValidProxSensorWithRefreshRateAndVsyncRate(), - /* includeIdleMode= */ true, /* enableEvenDimmer */ false)); + /* includeIdleMode= */ true, /* enableEvenDimmer= */ false)); assertEquals("test_proximity_sensor", mDisplayDeviceConfig.getProximitySensor().type); assertEquals("Test Proximity Sensor", @@ -803,7 +803,7 @@ public final class DisplayDeviceConfigTest { @Test public void testBrightnessRamps_IdleFallsBackToConfigInteractive() throws IOException { setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(), - getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false)); + getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ false)); assertEquals(mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(), 3000); assertEquals(mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis(), 2000); @@ -820,14 +820,14 @@ public final class DisplayDeviceConfigTest { @Test public void testBrightnessCapForWearBedtimeMode() throws IOException { setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(), - getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false)); + getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ false)); assertEquals(0.1f, mDisplayDeviceConfig.getBrightnessCapForWearBedtimeMode(), ZERO_DELTA); } @Test public void testAutoBrightnessBrighteningLevels() throws IOException { setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(), - getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false)); + getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ false)); assertArrayEquals(new float[]{0.0f, 80}, mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux( @@ -890,7 +890,7 @@ public final class DisplayDeviceConfigTest { when(mFlags.areAutoBrightnessModesEnabled()).thenReturn(false); setupDisplayDeviceConfigFromConfigResourceFile(); setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(), - getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false)); + getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ false)); assertArrayEquals(new float[]{brightnessIntToFloat(50), brightnessIntToFloat(100), brightnessIntToFloat(150)}, @@ -929,7 +929,7 @@ public final class DisplayDeviceConfigTest { when(mFlags.isEvenDimmerEnabled()).thenReturn(true); when(mResources.getBoolean(R.bool.config_evenDimmerEnabled)).thenReturn(true); setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(), - getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ true)); + getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ true)); assertTrue(mDisplayDeviceConfig.isEvenDimmerAvailable()); assertEquals(0.01f, mDisplayDeviceConfig.getBacklightFromBrightness(0.002f), ZERO_DELTA); @@ -1365,7 +1365,7 @@ public final class DisplayDeviceConfigTest { private String getContent() { return getContent(getValidLuxThrottling(), getValidProxSensor(), - /* includeIdleMode= */ true, false); + /* includeIdleMode= */ true, /* enableEvenDimmer= */ false); } private String getContent(String brightnessCapConfig, String proxSensor, diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java index 120cc84193cd..f5bed999d5a0 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -29,8 +29,10 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -45,6 +47,7 @@ import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.util.Spline; import android.view.Display; import android.view.DisplayAddress; import android.view.SurfaceControl; @@ -59,6 +62,7 @@ import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.internal.R; import com.android.server.LocalServices; import com.android.server.display.LocalDisplayAdapter.BacklightAdapter; +import com.android.server.display.color.ColorDisplayService; import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.mode.DisplayModeDirector; import com.android.server.display.notifications.DisplayNotificationManager; @@ -119,6 +123,8 @@ public class LocalDisplayAdapterTest { private DisplayManagerFlags mFlags; @Mock private DisplayPowerController mMockedDisplayPowerController; + @Mock + private ColorDisplayService.ColorDisplayServiceInternal mMockedColorDisplayServiceInternal; private Handler mHandler; @@ -133,6 +139,11 @@ public class LocalDisplayAdapterTest { private Injector mInjector; @Mock + private DisplayDeviceConfig mMockDisplayDeviceConfig; + @Mock + private BacklightAdapter mMockBacklightAdapter; + + @Mock private LocalDisplayAdapter.SurfaceControlProxy mSurfaceControlProxy; private static final float[] DISPLAY_RANGE_NITS = { 2.685f, 478.5f }; private static final int[] BACKLIGHT_RANGE = { 1, 255 }; @@ -150,6 +161,9 @@ public class LocalDisplayAdapterTest { doReturn(mMockedResources).when(mMockedContext).getResources(); LocalServices.removeServiceForTest(LightsManager.class); LocalServices.addService(LightsManager.class, mMockedLightsManager); + LocalServices.removeServiceForTest(ColorDisplayService.ColorDisplayServiceInternal.class); + LocalServices.addService(ColorDisplayService.ColorDisplayServiceInternal.class, + mMockedColorDisplayServiceInternal); mInjector = new Injector(); when(mSurfaceControlProxy.getBootDisplayModeSupport()).thenReturn(true); mAdapter = new LocalDisplayAdapter(mMockedSyncRoot, mMockedContext, mHandler, @@ -211,7 +225,15 @@ public class LocalDisplayAdapterTest { when(mMockedResources.getIntArray( com.android.internal.R.array.config_autoBrightnessLcdBacklightValues)) .thenReturn(new int[]{}); + + when(mMockedColorDisplayServiceInternal.fetchEvenDimmerSpline(3)).thenReturn( + new Spline.LinearSpline( + new float[]{2f, 3.0f, 500f, 2000f}, + new float[]{100, 0, 0, 0})); + when(mMockDisplayDeviceConfig.isEvenDimmerAvailable()).thenReturn(true); + doReturn(true).when(mFlags).isDisplayOffloadEnabled(); + doReturn(true).when(mFlags).isEvenDimmerEnabled(); initDisplayOffloadSession(); } @@ -222,6 +244,122 @@ public class LocalDisplayAdapterTest { } } + @Test + public void testEvenDimmer() throws InterruptedException { + // Set up + FakeDisplay display = new FakeDisplay(PORT_A); + setUpDisplay(display); + updateAvailableDisplays(); + mAdapter.registerLocked(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + assertThat(mListener.addedDisplays.size()).isEqualTo(1); + DisplayDevice displayDevice = mListener.addedDisplays.get(0); + + // brightness|backlight| nits | strength + // 0.5 | 0.45 | 600 | 0 // initial setup value + // 0.4 | 0.35 | 500 | 0 // normal range value + // 0.31 | 0.2 | 3 | 0 // transition point + // 0.16 | 0.125 | 2.5 | 50 // mid point of even dimmer + // 0.1 | 0.05 | 2 | 100 // bottom of even dimmer range + // 0.05 | 0.01 | 1 | 100+ // beyond strength=100 range (should still return 100) + when(mMockDisplayDeviceConfig.getEvenDimmerTransitionPoint()).thenReturn(0.31f); + when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.5f)).thenReturn(0.45f); + when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.4f)).thenReturn(0.35f); + when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.31f)).thenReturn(0.2f); + when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.16f)).thenReturn(0.125f); + when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.1f)).thenReturn(0.05f); + when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.05f)).thenReturn(0.01f); + when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.45f)).thenReturn(600f); + when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.35f)).thenReturn(500f); + when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.2f)).thenReturn(3f); + when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.125f)).thenReturn(2.5f); + when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.05f)).thenReturn(2f); + when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.01f)).thenReturn(1f); + + // initialise brightness to 0.5 + Runnable changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, + 0.5f, 0.5f, null); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + changeStateRunnable.run(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + verify(mSurfaceControlProxy).setDisplayPowerMode(any(), anyInt()); + verify(mMockBacklightAdapter).setBacklight(anyFloat(), anyFloat(), anyFloat(), anyFloat()); + verify(mMockedColorDisplayServiceInternal).applyEvenDimmerColorChanges(eq(false), eq(0)); + verify(mMockedColorDisplayServiceInternal).fetchEvenDimmerSpline(eq(3.0f)); + + // set up normal brightness range + changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.4f, 0.4f, + null); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + changeStateRunnable.run(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + // verify normal brightness range + verify(mMockBacklightAdapter).setBacklight(0.35f, 500f, 0.35f, 500f); + verify(mMockedColorDisplayServiceInternal, + times(1)) // no more, since the strength is the same + .applyEvenDimmerColorChanges(eq(false), eq(0)); + verify(mMockedColorDisplayServiceInternal, times(2)).fetchEvenDimmerSpline(eq(3.0f)); + + // set up even dimmer edge range + changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.31f, + 0.31f, null); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + changeStateRunnable.run(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + // verify even dimmer edge range + verify(mMockBacklightAdapter).setBacklight(0.2f, 3f, 0.2f, 3f); + // verify no more times, since the strength and enabled-ness is the same + verify(mMockedColorDisplayServiceInternal, times(1)).applyEvenDimmerColorChanges(eq(false), + eq(0)); + verify(mMockedColorDisplayServiceInternal, times(3)).fetchEvenDimmerSpline(eq(3.0f)); + + // set up mid point of even dimmer range + changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.16f, + 0.16f, null); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + changeStateRunnable.run(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + // verify within even dimmer range + verify(mMockBacklightAdapter).setBacklight(0.125f, 2.5f, 0.125f, 2.5f); + verify(mMockedColorDisplayServiceInternal).applyEvenDimmerColorChanges(eq(true), eq(50)); + verify(mMockedColorDisplayServiceInternal, times(4)).fetchEvenDimmerSpline(eq(3.0f)); + + // set up within even dimmer range + changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.1f, 0.1f, + null); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + changeStateRunnable.run(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + // verify within even dimmer range + verify(mMockBacklightAdapter).setBacklight(0.05f, 2f, 0.05f, 2f); + verify(mMockedColorDisplayServiceInternal).applyEvenDimmerColorChanges(eq(true), eq(100)); + verify(mMockedColorDisplayServiceInternal, times(5)).fetchEvenDimmerSpline(eq(3.0f)); + + // set up below even dimmer range + changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.05f, + 0.05f, null); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + changeStateRunnable.run(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + // verify within even dimmer range + verify(mMockBacklightAdapter).setBacklight(0.01f, 1f, 0.01f, 1f); + // ensure no greater than 100 strength is returned, therefore not called again. + verify(mMockedColorDisplayServiceInternal).applyEvenDimmerColorChanges(eq(true), eq(100)); + verify(mMockedColorDisplayServiceInternal, times(6)).fetchEvenDimmerSpline(eq(3.0f)); + + // set up return to normal range + changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.4f, 0.4f, + null); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + changeStateRunnable.run(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + // verify return to normal range + verify(mMockBacklightAdapter, times(2)).setBacklight(0.35f, 500f, 0.35f, 500f); + verify(mMockedColorDisplayServiceInternal, times(2)).applyEvenDimmerColorChanges(eq(false), + anyInt()); + verify(mMockedColorDisplayServiceInternal, times(7)).fetchEvenDimmerSpline(eq(3.0f)); + } + /** * Confirm that display is marked as private when it is listed in * com.android.internal.R.array.config_localPrivateDisplayPorts. @@ -1461,15 +1599,16 @@ public class LocalDisplayAdapterTest { return mSurfaceControlProxy; } - // Instead of using DisplayDeviceConfig.create(context, physicalDisplayId, isFirstDisplay) - // we should use DisplayDeviceConfig.create(context, isFirstDisplay) for the test to ensure - // that real device DisplayDeviceConfig is not loaded for FakeDisplay and we are getting - // consistent behaviour. Please also note that context passed to this method, is - // mMockContext and values will be loaded from mMockResources. @Override public DisplayDeviceConfig createDisplayDeviceConfig(Context context, long physicalDisplayId, boolean isFirstDisplay, DisplayManagerFlags flags) { - return DisplayDeviceConfig.create(context, isFirstDisplay, flags); + return mMockDisplayDeviceConfig; + } + + @Override + public BacklightAdapter getBacklightAdapter(IBinder displayToken, boolean isFirstDisplay, + LocalDisplayAdapter.SurfaceControlProxy surfaceControlProxy) { + return mMockBacklightAdapter; } } diff --git a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java index fc4d8d871fd5..07029268661e 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java @@ -16,6 +16,9 @@ package com.android.server.power; +import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP; +import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE; + import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; @@ -31,11 +34,13 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +import android.app.ActivityManagerInternal; import android.app.AppOpsManager; import android.content.Context; import android.content.res.Resources; import android.hardware.SensorManager; import android.hardware.display.AmbientDisplayConfiguration; +import android.hardware.display.DisplayManagerInternal; import android.os.BatteryStats; import android.os.Handler; import android.os.IWakeLockCallback; @@ -48,11 +53,18 @@ import android.os.WorkSource; import android.os.test.TestLooper; import android.provider.Settings; import android.testing.TestableContext; +import android.util.IntArray; +import android.util.SparseBooleanArray; +import android.view.Display; +import android.view.DisplayAddress; +import android.view.DisplayInfo; import androidx.test.InstrumentationRegistry; import com.android.internal.app.IBatteryStats; import com.android.server.LocalServices; +import com.android.server.input.InputManagerInternal; +import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.policy.WindowManagerPolicy; import com.android.server.power.batterysaver.BatterySaverStateMachine; import com.android.server.power.feature.PowerManagerFlags; @@ -71,6 +83,8 @@ import java.util.concurrent.Executor; public class NotifierTest { private static final String SYSTEM_PROPERTY_QUIESCENT = "ro.boot.quiescent"; private static final int USER_ID = 0; + private static final int DISPLAY_PORT = 0xFF; + private static final long DISPLAY_MODEL = 0xEEEEEEEEL; @Mock private BatterySaverStateMachine mBatterySaverStateMachineMock; @Mock private PowerManagerService.NativeWrapper mNativeWrapperMock; @@ -81,10 +95,16 @@ public class NotifierTest { @Mock private InattentiveSleepWarningController mInattentiveSleepWarningControllerMock; @Mock private Vibrator mVibrator; @Mock private StatusBarManagerInternal mStatusBarManagerInternal; + @Mock private InputManagerInternal mInputManagerInternal; + @Mock private InputMethodManagerInternal mInputMethodManagerInternal; + @Mock private DisplayManagerInternal mDisplayManagerInternal; + @Mock private ActivityManagerInternal mActivityManagerInternal; @Mock private WakeLockLog mWakeLockLog; @Mock private IBatteryStats mBatteryStats; + @Mock private WindowManagerPolicy mPolicy; + @Mock private PowerManagerFlags mPowerManagerFlags; @Mock private AppOpsManager mAppOpsManager; @@ -96,6 +116,8 @@ public class NotifierTest { private FakeExecutor mTestExecutor = new FakeExecutor(); private Notifier mNotifier; + private DisplayInfo mDefaultDisplayInfo = new DisplayInfo(); + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -103,11 +125,25 @@ public class NotifierTest { LocalServices.removeServiceForTest(StatusBarManagerInternal.class); LocalServices.addService(StatusBarManagerInternal.class, mStatusBarManagerInternal); + LocalServices.removeServiceForTest(InputManagerInternal.class); + LocalServices.addService(InputManagerInternal.class, mInputManagerInternal); + LocalServices.removeServiceForTest(InputMethodManagerInternal.class); + LocalServices.addService(InputMethodManagerInternal.class, mInputMethodManagerInternal); + + LocalServices.removeServiceForTest(ActivityManagerInternal.class); + LocalServices.addService(ActivityManagerInternal.class, mActivityManagerInternal); + + mDefaultDisplayInfo.address = DisplayAddress.fromPortAndModel(DISPLAY_PORT, DISPLAY_MODEL); + LocalServices.removeServiceForTest(DisplayManagerInternal.class); + LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternal); + mContextSpy = spy(new TestableContext(InstrumentationRegistry.getContext())); mResourcesSpy = spy(mContextSpy.getResources()); when(mContextSpy.getResources()).thenReturn(mResourcesSpy); when(mSystemPropertiesMock.get(eq(SYSTEM_PROPERTY_QUIESCENT), anyString())).thenReturn(""); when(mContextSpy.getSystemService(Vibrator.class)).thenReturn(mVibrator); + when(mDisplayManagerInternal.getDisplayInfo(Display.DEFAULT_DISPLAY)).thenReturn( + mDefaultDisplayInfo); mService = new PowerManagerService(mContextSpy, mInjector); } @@ -232,6 +268,32 @@ public class NotifierTest { } @Test + public void testOnGlobalWakefulnessChangeStarted() throws Exception { + createNotifier(); + // GIVEN system is currently non-interactive + when(mPowerManagerFlags.isPerDisplayWakeByTouchEnabled()).thenReturn(false); + final int displayId1 = 101; + final int displayId2 = 102; + final int[] displayIds = new int[]{displayId1, displayId2}; + when(mDisplayManagerInternal.getDisplayIds()).thenReturn(IntArray.wrap(displayIds)); + mNotifier.onGlobalWakefulnessChangeStarted(WAKEFULNESS_ASLEEP, + PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, /* eventTime= */ 1000); + mTestLooper.dispatchAll(); + + // WHEN a global wakefulness change to interactive starts + mNotifier.onGlobalWakefulnessChangeStarted(WAKEFULNESS_AWAKE, + PowerManager.WAKE_REASON_TAP, /* eventTime= */ 2000); + mTestLooper.dispatchAll(); + + // THEN input is notified of all displays being interactive + final SparseBooleanArray expectedDisplayInteractivities = new SparseBooleanArray(); + expectedDisplayInteractivities.put(displayId1, true); + expectedDisplayInteractivities.put(displayId2, true); + verify(mInputManagerInternal).setDisplayInteractivities(expectedDisplayInteractivities); + verify(mInputMethodManagerInternal).setInteractive(/* interactive= */ true); + } + + @Test public void testOnWakeLockListener_RemoteException_NoRethrow() throws RemoteException { when(mPowerManagerFlags.improveWakelockLatency()).thenReturn(true); createNotifier(); @@ -551,7 +613,7 @@ public class NotifierTest { mContextSpy, mBatteryStats, mInjector.createSuspendBlocker(mService, "testBlocker"), - null, + mPolicy, null, null, mTestExecutor, mPowerManagerFlags, injector); diff --git a/services/tests/powerstatstests/res/xml/irq_device_map_3.xml b/services/tests/powerstatstests/res/xml/irq_device_map_3.xml index fd55428c48df..c3df0785bd9b 100644 --- a/services/tests/powerstatstests/res/xml/irq_device_map_3.xml +++ b/services/tests/powerstatstests/res/xml/irq_device_map_3.xml @@ -32,4 +32,7 @@ <device name="test.sensor.device"> <subsystem>Sensor</subsystem> </device> + <device name="test.bluetooth.device"> + <subsystem>Bluetooth</subsystem> + </device> </irq-device-map>
\ No newline at end of file diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java index 0dc836ba0400..fe4d971face5 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java @@ -17,6 +17,7 @@ package com.android.server.power.stats.wakeups; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM; +import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_BLUETOOTH; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SENSOR; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER; @@ -52,6 +53,7 @@ public class CpuWakeupStatsTest { private static final String KERNEL_REASON_SOUND_TRIGGER_IRQ = "129 test.sound_trigger.device"; private static final String KERNEL_REASON_SENSOR_IRQ = "15 test.sensor.device"; private static final String KERNEL_REASON_CELLULAR_DATA_IRQ = "18 test.cellular_data.device"; + private static final String KERNEL_REASON_BLUETOOTH_IRQ = "19 test.bluetooth.device"; private static final String KERNEL_REASON_UNKNOWN_IRQ = "140 test.unknown.device"; private static final String KERNEL_REASON_UNKNOWN_FORMAT = "free-form-reason test.alarm.device"; private static final String KERNEL_REASON_ALARM_ABNORMAL = "-1 test.alarm.device"; @@ -62,12 +64,14 @@ public class CpuWakeupStatsTest { private static final int TEST_UID_3 = 92261423; private static final int TEST_UID_4 = 56926423; private static final int TEST_UID_5 = 76421423; + private static final int TEST_UID_6 = 62345353; private static final int TEST_PROC_STATE_1 = 72331; private static final int TEST_PROC_STATE_2 = 792351; private static final int TEST_PROC_STATE_3 = 138831; private static final int TEST_PROC_STATE_4 = 23231; private static final int TEST_PROC_STATE_5 = 42; + private static final int TEST_PROC_STATE_6 = 129942; private static final Context sContext = InstrumentationRegistry.getTargetContext(); private final Handler mHandler = Mockito.mock(Handler.class); @@ -79,6 +83,7 @@ public class CpuWakeupStatsTest { obj.mUidProcStates.put(TEST_UID_3, TEST_PROC_STATE_3); obj.mUidProcStates.put(TEST_UID_4, TEST_PROC_STATE_4); obj.mUidProcStates.put(TEST_UID_5, TEST_PROC_STATE_5); + obj.mUidProcStates.put(TEST_UID_6, TEST_PROC_STATE_6); } @Test @@ -118,6 +123,7 @@ public class CpuWakeupStatsTest { CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER, CPU_WAKEUP_SUBSYSTEM_SENSOR, CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA, + CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, }; final String[] kernelReasons = new String[] { @@ -126,10 +132,11 @@ public class CpuWakeupStatsTest { KERNEL_REASON_SOUND_TRIGGER_IRQ, KERNEL_REASON_SENSOR_IRQ, KERNEL_REASON_CELLULAR_DATA_IRQ, + KERNEL_REASON_BLUETOOTH_IRQ, }; final int[] uids = new int[] { - TEST_UID_2, TEST_UID_3, TEST_UID_4, TEST_UID_1, TEST_UID_5 + TEST_UID_2, TEST_UID_3, TEST_UID_4, TEST_UID_1, TEST_UID_5, TEST_UID_6 }; final int[] procStates = new int[] { @@ -137,7 +144,8 @@ public class CpuWakeupStatsTest { TEST_PROC_STATE_3, TEST_PROC_STATE_4, TEST_PROC_STATE_1, - TEST_PROC_STATE_5 + TEST_PROC_STATE_5, + TEST_PROC_STATE_6 }; final int total = subsystems.length; @@ -285,6 +293,40 @@ public class CpuWakeupStatsTest { } @Test + public void bluetoothIrqAttributionSolo() { + final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); + final long wakeupTime = 1236121; + + populateDefaultProcStates(obj); + + obj.noteWakeupTimeAndReason(wakeupTime, 1, KERNEL_REASON_BLUETOOTH_IRQ); + + // Outside the window, so should be ignored. + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, + wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_1); + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, + wakeupTime + obj.mConfig.WAKEUP_MATCHING_WINDOW_MS + 1, TEST_UID_2); + // Should be attributed + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime + 5, TEST_UID_3, + TEST_UID_5); + + final SparseArray<SparseIntArray> attribution = obj.mWakeupAttribution.get(wakeupTime); + assertThat(attribution).isNotNull(); + assertThat(attribution.size()).isEqualTo(1); + assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)).isTrue(); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey( + TEST_UID_1)).isLessThan(0); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey( + TEST_UID_2)).isLessThan(0); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_3)).isEqualTo( + TEST_PROC_STATE_3); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey( + TEST_UID_4)).isLessThan(0); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_5)).isEqualTo( + TEST_PROC_STATE_5); + } + + @Test public void alarmAndWifiIrqAttribution() { final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); final long wakeupTime = 92123210; @@ -400,6 +442,47 @@ public class CpuWakeupStatsTest { } @Test + public void unknownAndBluetoothAttribution() { + final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); + final long wakeupTime = 92123520; + + populateDefaultProcStates(obj); + + obj.noteWakeupTimeAndReason(wakeupTime, 24, + KERNEL_REASON_UNKNOWN_IRQ + ":" + KERNEL_REASON_BLUETOOTH_IRQ); + + // Bluetooth activity + // Outside the window, so should be ignored. + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, + wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_4); + // Should be attributed + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime + 2, TEST_UID_1); + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime - 1, TEST_UID_3, + TEST_UID_5); + + // Unrelated, should be ignored. + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3); + + final SparseArray<SparseIntArray> attribution = obj.mWakeupAttribution.get(wakeupTime); + assertThat(attribution).isNotNull(); + assertThat(attribution.size()).isEqualTo(2); + assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)).isTrue(); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_1)).isEqualTo( + TEST_PROC_STATE_1); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH) + .indexOfKey(TEST_UID_2)).isLessThan(0); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_3)).isEqualTo( + TEST_PROC_STATE_3); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH) + .indexOfKey(TEST_UID_4)).isLessThan(0); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_5)).isEqualTo( + TEST_PROC_STATE_5); + assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isTrue(); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isNull(); + assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_ALARM)).isFalse(); + } + + @Test public void unknownFormatWakeupIgnored() { final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); final long wakeupTime = 72123210; diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index ac1b7c6876f7..cbe6700f4d41 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -52,6 +52,7 @@ android_test { "services.credentials", "services.devicepolicy", "services.flags", + "com.android.server.flags.services-aconfig-java", "services.net", "services.people", "services.supervision", @@ -81,6 +82,7 @@ android_test { // TODO: remove once Android migrates to JUnit 4.12, // which provides assertThrows "testng", + "flag-junit", "junit", "junit-params", "ActivityContext", diff --git a/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java b/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java index ec78bcea7539..c18faef2c028 100644 --- a/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java @@ -31,6 +31,9 @@ import android.content.pm.ResolveInfo; import android.os.Binder; import android.os.Handler; import android.os.Looper; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; import android.provider.DeviceConfigInterface; import android.testing.TestableContext; @@ -43,6 +46,9 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.server.flags.Flags; +import com.android.server.pinner.PinnedFile; +import com.android.server.pinner.PinnerService; import com.android.server.testutils.FakeDeviceConfigInterface; import com.android.server.wm.ActivityTaskManagerInternal; @@ -73,15 +79,18 @@ public class PinnerServiceTest { private static final long WAIT_FOR_PINNER_TIMEOUT = TimeUnit.SECONDS.toMillis(2); + private static final int MEMORY_PERCENTAGE_FOR_QUOTA = 10; + @Rule public TestableContext mContext = new TestableContext(InstrumentationRegistry.getContext(), null); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private final ArraySet<String> mUpdatedPackages = new ArraySet<>(); private ResolveInfo mHomePackageResolveInfo; private FakeDeviceConfigInterface mFakeDeviceConfigInterface; private PinnerService.Injector mInjector; - @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -114,6 +123,8 @@ public class PinnerServiceTest { resources.addOverride(com.android.internal.R.bool.config_pinnerCameraApp, false); resources.addOverride(com.android.internal.R.integer.config_pinnerHomePinBytes, 0); resources.addOverride(com.android.internal.R.bool.config_pinnerAssistantApp, false); + resources.addOverride(com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, + MEMORY_PERCENTAGE_FOR_QUOTA); mFakeDeviceConfigInterface = new FakeDeviceConfigInterface(); setDeviceConfigPinnedAnonSize(0); @@ -138,10 +149,9 @@ public class PinnerServiceTest { } @Override - protected PinnerService.PinnedFile pinFileInternal(String fileToPin, - int maxBytesToPin, boolean attemptPinIntrospection) { - return new PinnerService.PinnedFile(-1, - maxBytesToPin, fileToPin, maxBytesToPin); + protected PinnedFile pinFileInternal(PinnerService service, String fileToPin, + long maxBytesToPin, boolean attemptPinIntrospection) { + return new PinnedFile(-1, maxBytesToPin, fileToPin, maxBytesToPin); } }; } @@ -167,6 +177,12 @@ public class PinnerServiceTest { unpinAnonRegionMethod.invoke(pinnerService); } + private long getGlobalPinQuota(PinnerService service) throws Exception { + Method getQuotaMethod = PinnerService.class.getDeclaredMethod("getAvailableGlobalQuota"); + getQuotaMethod.setAccessible(true); + return (long) getQuotaMethod.invoke(service); + } + private void waitForPinnerService(PinnerService pinnerService) throws NoSuchFieldException, IllegalAccessException { // There's no notification/callback when pinning finished @@ -315,15 +331,121 @@ public class PinnerServiceTest { PinnerService pinnerService = new PinnerService(mContext, mInjector); pinnerService.onStart(); - pinnerService.pinFile("test_file", 4096, null, "my_group"); + pinnerService.pinFile("test_file", 4096, null, "my_group", false); - assertThat(getPinnedSize(pinnerService)).isGreaterThan(0); - assertThat(getTotalPinnedFiles(pinnerService)).isGreaterThan(0); + assertThat(getPinnedSize(pinnerService)).isEqualTo(4096); + assertThat(getTotalPinnedFiles(pinnerService)).isEqualTo(1); + + unpinAll(pinnerService); + } + + @Test + @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA) + public void testPinAllQuota() throws Exception { + PinnerService pinnerService = new PinnerService(mContext, mInjector); + pinnerService.onStart(); + + long quota = getGlobalPinQuota(pinnerService); + + pinnerService.pinFile("test_file", Long.MAX_VALUE, null, "my_group", false); + + assertThat(getPinnedSize(pinnerService)).isEqualTo(quota); unpinAll(pinnerService); } @Test + @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA) + public void testGlobalPinQuotaAsDevicePercentage() throws Exception { + PinnerService pinnerService = new PinnerService(mContext, mInjector); + pinnerService.onStart(); + long origQuota = getGlobalPinQuota(pinnerService); + + long totalMem = android.os.Process.getTotalMemory(); + + // Verify that pin quota is the set percentage of device total memory + assertThat(origQuota).isEqualTo((totalMem * MEMORY_PERCENTAGE_FOR_QUOTA) / 100); + + pinnerService.pinFile("test_file", 4096, null, "my_group", false); + assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(origQuota - 4096); + } + + @Test + @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA) + public void testGlobalPinWhenNoQuota() throws Exception { + TestableResources resources = mContext.getOrCreateTestableResources(); + resources.addOverride( + com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, 0); + + PinnerService pinnerService = new PinnerService(mContext, mInjector); + pinnerService.onStart(); + + // Verify that pin quota is zero + assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(0); + + pinnerService.pinFile("test_file", 4096, null, "my_group", false); + assertThat(getTotalPinnedFiles(pinnerService)).isEqualTo(0); + } + + /** + * This test is temporary, it should be cleaned up when removing the pin_global_quota bugfix + * flag. + */ + @Test + @DisableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA) + public void testGlobalQuotaDisabled() throws Exception { + TestableResources resources = mContext.getOrCreateTestableResources(); + resources.addOverride( + com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, 0); + + PinnerService pinnerService = new PinnerService(mContext, mInjector); + pinnerService.onStart(); + + // The quota parameter exists but it should have no effect on pinning + long quota = getGlobalPinQuota(pinnerService); + + pinnerService.pinFile("test_file", quota + 1, null, "my_group", false); + + // Verify that we can pin past the quota as it is disabled + assertThat(getPinnedSize(pinnerService)).isEqualTo(quota + 1); + } + + @Test + @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA) + public void testUnpinReleasesQuota() throws Exception { + PinnerService pinnerService = new PinnerService(mContext, mInjector); + pinnerService.onStart(); + long origQuota = getGlobalPinQuota(pinnerService); + + // Verify that pin quota exists and is non zero. + assertThat(getGlobalPinQuota(pinnerService)).isGreaterThan(0); + + pinnerService.pinFile("test_file", origQuota, null, "my_group", false); + + // Make sure all the quota was consumed + assertThat(getPinnedSize(pinnerService)).isEqualTo(origQuota); + + // Unpin the file and verify that the quota has been released. + pinnerService.unpinFile("test_file"); + assertThat(getPinnedSize(pinnerService)).isEqualTo(0); + assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(origQuota); + } + + @Test + @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA) + public void testGlobalPinQuotaNegative() throws Exception { + TestableResources resources = mContext.getOrCreateTestableResources(); + resources.addOverride( + com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, -10); + + PinnerService pinnerService = new PinnerService(mContext, mInjector); + pinnerService.onStart(); + + // Verify that pin quota is zero + assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(0); + } + + @Test public void testPinAnonRegion() throws Exception { setDeviceConfigPinnedAnonSize(32768); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java index 6e6d5a870031..8dfd54fe38bc 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java @@ -174,8 +174,8 @@ public class AbstractAccessibilityServiceConnectionTest { @Mock private AccessibilityTrace mMockA11yTrace; @Mock private WindowManagerInternal mMockWindowManagerInternal; @Mock private SystemActionPerformer mMockSystemActionPerformer; - @Mock private IBinder mMockService; - @Mock private IAccessibilityServiceClient mMockServiceInterface; + @Mock private IBinder mMockClientBinder; + @Mock private IAccessibilityServiceClient mMockClient; @Mock private KeyEventDispatcher mMockKeyEventDispatcher; @Mock private IAccessibilityInteractionConnection mMockIA11yInteractionConnection; @Mock private IAccessibilityInteractionConnectionCallback mMockCallback; @@ -247,9 +247,9 @@ public class AbstractAccessibilityServiceConnectionTest { mSpyServiceInfo, SERVICE_ID, mHandler, new Object(), mMockSecurityPolicy, mMockSystemSupport, mMockA11yTrace, mMockWindowManagerInternal, mMockSystemActionPerformer, mMockA11yWindowManager); - // Assume that the service is connected - mServiceConnection.mService = mMockService; - mServiceConnection.mServiceInterface = mMockServiceInterface; + // Assume that the client is connected + mServiceConnection.mClientBinder = mMockClientBinder; + mServiceConnection.mClient = mMockClient; // Update security policy for this service when(mMockSecurityPolicy.checkAccessibilityAccess(mServiceConnection)).thenReturn(true); @@ -273,7 +273,7 @@ public class AbstractAccessibilityServiceConnectionTest { final KeyEvent mockKeyEvent = mock(KeyEvent.class); mServiceConnection.onKeyEvent(mockKeyEvent, sequenceNumber); - verify(mMockServiceInterface).onKeyEvent(mockKeyEvent, sequenceNumber); + verify(mMockClient).onKeyEvent(mockKeyEvent, sequenceNumber); } @Test 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 566feb7e3d80..7481fc8ec46d 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -63,8 +63,11 @@ import static org.mockito.Mockito.when; import android.Manifest; import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.IAccessibilityServiceClient; +import android.annotation.NonNull; import android.app.PendingIntent; import android.app.RemoteAction; +import android.app.admin.DevicePolicyManager; +import android.app.ecm.EnhancedConfirmationManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -212,6 +215,7 @@ public class AccessibilityManagerServiceTest { @Mock private FullScreenMagnificationController mMockFullScreenMagnificationController; @Mock private ProxyManager mProxyManager; @Mock private StatusBarManagerInternal mStatusBarManagerInternal; + @Mock private DevicePolicyManager mDevicePolicyManager; @Spy private IUserInitializationCompleteCallback mUserInitializationCompleteCallback; @Captor private ArgumentCaptor<Intent> mIntentArgumentCaptor; private IAccessibilityManager mA11yManagerServiceOnDevice; @@ -241,6 +245,7 @@ public class AccessibilityManagerServiceTest { UserManagerInternal.class, mMockUserManagerInternal); LocalServices.addService(StatusBarManagerInternal.class, mStatusBarManagerInternal); mInputFilter = mock(FakeInputFilter.class); + mTestableContext.addMockSystemService(DevicePolicyManager.class, mDevicePolicyManager); when(mMockMagnificationController.getMagnificationConnectionManager()).thenReturn( mMockMagnificationConnectionManager); @@ -2160,6 +2165,24 @@ public class AccessibilityManagerServiceTest { .isEqualTo(SOFTWARE); } + @Test + @EnableFlags({android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED, + android.security.Flags.FLAG_EXTEND_ECM_TO_ALL_SETTINGS}) + public void isAccessibilityTargetAllowed_nonSystemUserId_useEcmWithNonSystemUserId() { + String fakePackageName = "FAKE_PACKAGE_NAME"; + int uid = 0; // uid is not used in the actual implementation when flags are on + int userId = mTestableContext.getUserId() + 1234; + when(mDevicePolicyManager.getPermittedAccessibilityServices(userId)).thenReturn( + List.of(fakePackageName)); + Context mockUserContext = mock(Context.class); + mTestableContext.addMockUserContext(userId, mockUserContext); + + mA11yms.isAccessibilityTargetAllowed(fakePackageName, uid, userId); + + verify(mockUserContext).getSystemService(EnhancedConfirmationManager.class); + } + + private Set<String> readStringsFromSetting(String setting) { final Set<String> result = new ArraySet<>(); mA11yms.readColonDelimitedSettingToSet( @@ -2280,6 +2303,7 @@ public class AccessibilityManagerServiceTest { private final Context mMockContext; private final Map<String, List<BroadcastReceiver>> mBroadcastReceivers = new ArrayMap<>(); + private ArrayMap<Integer, Context> mMockUserContexts = new ArrayMap<>(); A11yTestableContext(Context base) { super(base); @@ -2317,6 +2341,19 @@ public class AccessibilityManagerServiceTest { return mMockContext; } + public void addMockUserContext(int userId, Context context) { + mMockUserContexts.put(userId, context); + } + + @Override + @NonNull + public Context createContextAsUser(UserHandle user, int flags) { + if (mMockUserContexts.containsKey(user.getIdentifier())) { + return mMockUserContexts.get(user.getIdentifier()); + } + return super.createContextAsUser(user, flags); + } + Map<String, List<BroadcastReceiver>> getBroadcastReceivers() { return mBroadcastReceivers; } diff --git a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java index b9ce8ad0b018..0c92abce7254 100644 --- a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java @@ -1163,6 +1163,16 @@ public class AccountManagerServiceTest extends AndroidTestCase { verify(mMockAccountManagerResponse).onResult(mBundleCaptor.capture()); Bundle result = mBundleCaptor.getValue(); + Bundle sessionBundle = result.getBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE); + assertNotNull(sessionBundle); + // Assert that session bundle is decrypted and hence data is visible. + assertEquals(AccountManagerServiceTestFixtures.SESSION_DATA_VALUE_1, + sessionBundle.getString(AccountManagerServiceTestFixtures.SESSION_DATA_NAME_1)); + // Assert finishSessionAsUser added calling uid and pid into the sessionBundle + assertTrue(sessionBundle.containsKey(AccountManager.KEY_CALLER_UID)); + assertTrue(sessionBundle.containsKey(AccountManager.KEY_CALLER_PID)); + assertEquals(sessionBundle.getString( + AccountManager.KEY_ANDROID_PACKAGE_NAME), "APCT.package"); // Verify response data assertNull(result.getString(AccountManager.KEY_AUTHTOKEN, null)); @@ -2111,6 +2121,12 @@ public class AccountManagerServiceTest extends AndroidTestCase { result.getString(AccountManager.KEY_ACCOUNT_NAME)); assertEquals(AccountManagerServiceTestFixtures.ACCOUNT_TYPE_1, result.getString(AccountManager.KEY_ACCOUNT_TYPE)); + + Bundle optionBundle = result.getParcelable( + AccountManagerServiceTestFixtures.KEY_OPTIONS_BUNDLE); + // Assert addAccountAsUser added calling uid and pid into the option bundle + assertTrue(optionBundle.containsKey(AccountManager.KEY_CALLER_UID)); + assertTrue(optionBundle.containsKey(AccountManager.KEY_CALLER_PID)); } @SmallTest @@ -3441,52 +3457,6 @@ public class AccountManagerServiceTest extends AndroidTestCase { + (readTotalTime.doubleValue() / readerCount / loopSize)); } - @SmallTest - public void testSanitizeBundle_expectedFields() throws Exception { - Bundle bundle = new Bundle(); - bundle.putString(AccountManager.KEY_ACCOUNT_NAME, "name"); - bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, "type"); - bundle.putString(AccountManager.KEY_AUTHTOKEN, "token"); - bundle.putString(AccountManager.KEY_AUTH_TOKEN_LABEL, "label"); - bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "error message"); - bundle.putString(AccountManager.KEY_PASSWORD, "password"); - bundle.putString(AccountManager.KEY_ACCOUNT_STATUS_TOKEN, "status"); - - bundle.putLong(AbstractAccountAuthenticator.KEY_CUSTOM_TOKEN_EXPIRY, 123L); - bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true); - bundle.putInt(AccountManager.KEY_ERROR_CODE, 456); - - Bundle sanitizedBundle = AccountManagerService.sanitizeBundle(bundle); - assertEquals(sanitizedBundle.getString(AccountManager.KEY_ACCOUNT_NAME), "name"); - assertEquals(sanitizedBundle.getString(AccountManager.KEY_ACCOUNT_TYPE), "type"); - assertEquals(sanitizedBundle.getString(AccountManager.KEY_AUTHTOKEN), "token"); - assertEquals(sanitizedBundle.getString(AccountManager.KEY_AUTH_TOKEN_LABEL), "label"); - assertEquals(sanitizedBundle.getString(AccountManager.KEY_ERROR_MESSAGE), "error message"); - assertEquals(sanitizedBundle.getString(AccountManager.KEY_PASSWORD), "password"); - assertEquals(sanitizedBundle.getString(AccountManager.KEY_ACCOUNT_STATUS_TOKEN), "status"); - - assertEquals(sanitizedBundle.getLong( - AbstractAccountAuthenticator.KEY_CUSTOM_TOKEN_EXPIRY, 0), 123L); - assertEquals(sanitizedBundle.getBoolean(AccountManager.KEY_BOOLEAN_RESULT, false), true); - assertEquals(sanitizedBundle.getInt(AccountManager.KEY_ERROR_CODE, 0), 456); - } - - @SmallTest - public void testSanitizeBundle_filtersUnexpectedFields() throws Exception { - Bundle bundle = new Bundle(); - bundle.putString(AccountManager.KEY_ACCOUNT_NAME, "name"); - bundle.putString("unknown_key", "value"); - Bundle sessionBundle = new Bundle(); - bundle.putBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE, sessionBundle); - - Bundle sanitizedBundle = AccountManagerService.sanitizeBundle(bundle); - - assertEquals(sanitizedBundle.getString(AccountManager.KEY_ACCOUNT_NAME), "name"); - assertFalse(sanitizedBundle.containsKey("unknown_key")); - // It is a valid response from Authenticator which will be accessed using original Bundle - assertFalse(sanitizedBundle.containsKey(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE)); - } - private void waitForCyclicBarrier(CyclicBarrier cyclicBarrier) { try { cyclicBarrier.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java b/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java index 14cb22d7698e..efc2d974a7cc 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java @@ -16,12 +16,20 @@ package com.android.server.biometrics; +import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED; import static android.hardware.biometrics.BiometricManager.Authenticators; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; + +import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricManager; @@ -36,8 +44,12 @@ import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.test.filters.SmallTest; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; @Presubmit @SmallTest @@ -45,6 +57,17 @@ public class UtilsTest { @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Rule + public MockitoRule mockitorule = MockitoJUnit.rule(); + + @Mock + private Context mContext; + + @Before + public void setUp() { + doThrow(SecurityException.class).when(mContext).enforceCallingOrSelfPermission( + eq(SET_BIOMETRIC_DIALOG_ADVANCED), any()); + } @Test public void testCombineAuthenticatorBundles_withKeyDeviceCredential_andKeyAuthenticators() { @@ -162,28 +185,39 @@ public class UtilsTest { @Test public void testIsValidAuthenticatorConfig() { - assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.EMPTY_SET)); + assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.EMPTY_SET)); - assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_STRONG)); + assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.BIOMETRIC_STRONG)); - assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_WEAK)); + assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.BIOMETRIC_WEAK)); - assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.DEVICE_CREDENTIAL)); + assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.DEVICE_CREDENTIAL)); - assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.DEVICE_CREDENTIAL + assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_STRONG)); - assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.DEVICE_CREDENTIAL + assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK)); - assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_CONVENIENCE)); + assertFalse(Utils.isValidAuthenticatorConfig( + mContext, Authenticators.BIOMETRIC_CONVENIENCE)); - assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_CONVENIENCE + assertFalse(Utils.isValidAuthenticatorConfig(mContext, Authenticators.BIOMETRIC_CONVENIENCE | Authenticators.DEVICE_CREDENTIAL)); - assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_MAX_STRENGTH)); + assertFalse(Utils.isValidAuthenticatorConfig( + mContext, Authenticators.BIOMETRIC_MAX_STRENGTH)); + + assertFalse(Utils.isValidAuthenticatorConfig( + mContext, Authenticators.BIOMETRIC_MIN_STRENGTH)); + + assertThrows(SecurityException.class, () -> Utils.isValidAuthenticatorConfig( + mContext, Authenticators.MANDATORY_BIOMETRICS)); + + doNothing().when(mContext).enforceCallingOrSelfPermission( + eq(SET_BIOMETRIC_DIALOG_ADVANCED), any()); - assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_MIN_STRENGTH)); + assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.MANDATORY_BIOMETRICS)); // The rest of the bits are not allowed to integrate with the public APIs for (int i = 8; i < 32; i++) { @@ -192,7 +226,7 @@ public class UtilsTest { || authenticator == Authenticators.MANDATORY_BIOMETRICS) { continue; } - assertFalse(Utils.isValidAuthenticatorConfig(1 << i)); + assertFalse(Utils.isValidAuthenticatorConfig(mContext, 1 << i)); } } diff --git a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java index d071c159d6f5..ae781dcb834a 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java @@ -60,6 +60,7 @@ import android.os.RemoteException; import android.os.ServiceSpecificException; import android.os.UserManager; import android.platform.test.annotations.Presubmit; +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; @@ -130,6 +131,7 @@ public class RebootEscrowManagerTests { private SecretKey mAesKey; private MockInjector mMockInjector; private Handler mHandler; + private Network mNetwork; public interface MockableRebootEscrowInjected { int getBootCount(); @@ -342,6 +344,7 @@ public class RebootEscrowManagerTests { when(mCallbacks.isUserSecure(NONSECURE_SECONDARY_USER_ID)).thenReturn(false); when(mCallbacks.isUserSecure(SECURE_SECONDARY_USER_ID)).thenReturn(true); mInjected = mock(MockableRebootEscrowInjected.class); + mNetwork = mock(Network.class); mMockInjector = new MockInjector( mContext, @@ -351,6 +354,10 @@ public class RebootEscrowManagerTests { mKeyStoreManager, mStorage, mInjected); + mMockInjector.mNetworkConsumer = + (callback) -> { + callback.onAvailable(mNetwork); + }; HandlerThread thread = new HandlerThread("RebootEscrowManagerTest"); thread.start(); mHandler = new Handler(thread.getLooper()); @@ -367,6 +374,10 @@ public class RebootEscrowManagerTests { mKeyStoreManager, mStorage, mInjected); + mMockInjector.mNetworkConsumer = + (callback) -> { + callback.onAvailable(mNetwork); + }; mService = new RebootEscrowManager(mMockInjector, mCallbacks, mStorage, mHandler); } @@ -621,7 +632,7 @@ public class RebootEscrowManagerTests { // pretend reboot happens here when(mInjected.getBootCount()).thenReturn(1); - mService.loadRebootEscrowDataIfAvailable(null); + mService.loadRebootEscrowDataIfAvailable(mHandler); verify(mServiceConnection, never()).unwrap(any(), anyLong()); verify(mCallbacks, never()).onRebootEscrowRestored(anyByte(), any(), anyInt()); } @@ -678,7 +689,7 @@ public class RebootEscrowManagerTests { when(mServiceConnection.unwrap(any(), anyLong())) .thenAnswer(invocation -> invocation.getArgument(0)); - mService.loadRebootEscrowDataIfAvailable(null); + mService.loadRebootEscrowDataIfAvailable(mHandler); verify(mServiceConnection).unwrap(any(), anyLong()); verify(mCallbacks).onRebootEscrowRestored(anyByte(), any(), eq(PRIMARY_USER_ID)); @@ -734,7 +745,7 @@ public class RebootEscrowManagerTests { when(mServiceConnection.unwrap(any(), anyLong())) .thenAnswer(invocation -> invocation.getArgument(0)); - mService.loadRebootEscrowDataIfAvailable(null); + mService.loadRebootEscrowDataIfAvailable(mHandler); verify(mServiceConnection).unwrap(any(), anyLong()); verify(mCallbacks).onRebootEscrowRestored(anyByte(), any(), eq(PRIMARY_USER_ID)); @@ -783,7 +794,7 @@ public class RebootEscrowManagerTests { when(mServiceConnection.unwrap(any(), anyLong())) .thenAnswer(invocation -> invocation.getArgument(0)); - mService.loadRebootEscrowDataIfAvailable(null); + mService.loadRebootEscrowDataIfAvailable(mHandler); verify(mServiceConnection).unwrap(any(), anyLong()); assertTrue(metricsSuccessCaptor.getValue()); verify(mKeyStoreManager).clearKeyStoreEncryptionKey(); @@ -827,7 +838,7 @@ public class RebootEscrowManagerTests { anyInt()); when(mServiceConnection.unwrap(any(), anyLong())).thenThrow(RemoteException.class); - mService.loadRebootEscrowDataIfAvailable(null); + mService.loadRebootEscrowDataIfAvailable(mHandler); verify(mServiceConnection).unwrap(any(), anyLong()); assertFalse(metricsSuccessCaptor.getValue()); assertEquals( @@ -836,6 +847,7 @@ public class RebootEscrowManagerTests { } @Test + @RequiresFlagsDisabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR) public void loadRebootEscrowDataIfAvailable_ServerBasedIoError_RetryFailure() throws Exception { setServerBasedRebootEscrowProvider(); @@ -930,114 +942,6 @@ public class RebootEscrowManagerTests { @Test @RequiresFlagsEnabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR) - public void loadRebootEscrowDataIfAvailable_serverBasedWaitForInternet_success() - throws Exception { - setServerBasedRebootEscrowProvider(); - - when(mInjected.getBootCount()).thenReturn(0); - RebootEscrowListener mockListener = mock(RebootEscrowListener.class); - mService.setRebootEscrowListener(mockListener); - mService.prepareRebootEscrow(); - - clearInvocations(mServiceConnection); - callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID); - verify(mockListener).onPreparedForReboot(eq(true)); - verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong()); - - // Use x -> x for both wrap & unwrap functions. - when(mServiceConnection.wrapBlob(any(), anyLong(), anyLong())) - .thenAnswer(invocation -> invocation.getArgument(0)); - assertEquals(ARM_REBOOT_ERROR_NONE, mService.armRebootEscrowIfNeeded()); - verify(mServiceConnection).wrapBlob(any(), anyLong(), anyLong()); - assertTrue(mStorage.hasRebootEscrowServerBlob()); - - // pretend reboot happens here - when(mInjected.getBootCount()).thenReturn(1); - ArgumentCaptor<Boolean> metricsSuccessCaptor = ArgumentCaptor.forClass(Boolean.class); - doNothing() - .when(mInjected) - .reportMetric( - metricsSuccessCaptor.capture(), - eq(0) /* error code */, - eq(2) /* Server based */, - eq(1) /* attempt count */, - anyInt(), - eq(0) /* vbmeta status */, - anyInt()); - - // load escrow data - when(mServiceConnection.unwrap(any(), anyLong())) - .thenAnswer(invocation -> invocation.getArgument(0)); - Network mockNetwork = mock(Network.class); - mMockInjector.mNetworkConsumer = - (callback) -> { - callback.onAvailable(mockNetwork); - }; - - mService.loadRebootEscrowDataIfAvailable(mHandler); - verify(mServiceConnection).unwrap(any(), anyLong()); - assertTrue(metricsSuccessCaptor.getValue()); - verify(mKeyStoreManager).clearKeyStoreEncryptionKey(); - assertNull(mMockInjector.mNetworkCallback); - } - - @Test - @RequiresFlagsEnabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR) - public void loadRebootEscrowDataIfAvailable_serverBasedWaitForInternetRemoteException_Failure() - throws Exception { - setServerBasedRebootEscrowProvider(); - - when(mInjected.getBootCount()).thenReturn(0); - RebootEscrowListener mockListener = mock(RebootEscrowListener.class); - mService.setRebootEscrowListener(mockListener); - mService.prepareRebootEscrow(); - - clearInvocations(mServiceConnection); - callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID); - verify(mockListener).onPreparedForReboot(eq(true)); - verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong()); - - // Use x -> x for both wrap & unwrap functions. - when(mServiceConnection.wrapBlob(any(), anyLong(), anyLong())) - .thenAnswer(invocation -> invocation.getArgument(0)); - assertEquals(ARM_REBOOT_ERROR_NONE, mService.armRebootEscrowIfNeeded()); - verify(mServiceConnection).wrapBlob(any(), anyLong(), anyLong()); - assertTrue(mStorage.hasRebootEscrowServerBlob()); - - // pretend reboot happens here - when(mInjected.getBootCount()).thenReturn(1); - ArgumentCaptor<Boolean> metricsSuccessCaptor = ArgumentCaptor.forClass(Boolean.class); - ArgumentCaptor<Integer> metricsErrorCodeCaptor = ArgumentCaptor.forClass(Integer.class); - doNothing() - .when(mInjected) - .reportMetric( - metricsSuccessCaptor.capture(), - metricsErrorCodeCaptor.capture(), - eq(2) /* Server based */, - eq(1) /* attempt count */, - anyInt(), - eq(0) /* vbmeta status */, - anyInt()); - - // load escrow data - when(mServiceConnection.unwrap(any(), anyLong())).thenThrow(RemoteException.class); - Network mockNetwork = mock(Network.class); - mMockInjector.mNetworkConsumer = - (callback) -> { - callback.onAvailable(mockNetwork); - }; - - mService.loadRebootEscrowDataIfAvailable(mHandler); - verify(mServiceConnection).unwrap(any(), anyLong()); - assertFalse(metricsSuccessCaptor.getValue()); - assertEquals( - Integer.valueOf(RebootEscrowManager.ERROR_LOAD_ESCROW_KEY), - metricsErrorCodeCaptor.getValue()); - assertNull(mMockInjector.mNetworkCallback); - } - - @Test - @RequiresFlagsEnabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR) public void loadRebootEscrowDataIfAvailable_waitForInternet_networkUnavailable() throws Exception { setServerBasedRebootEscrowProvider(); diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java index abc9ce3fdc36..ee63d5d32ff1 100644 --- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java @@ -38,6 +38,7 @@ 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.ArgumentMatchers.nullable; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; @@ -91,6 +92,7 @@ import org.junit.Before; import org.junit.Rule; 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; @@ -174,8 +176,8 @@ public class MediaProjectionManagerServiceTest { private PackageManager mPackageManager; @Mock private KeyguardManager mKeyguardManager; - @Mock - AppOpsManager mAppOpsManager; + + private AppOpsManager mAppOpsManager; @Mock private IMediaProjectionWatcherCallback mWatcherCallback; @Mock @@ -193,6 +195,7 @@ public class MediaProjectionManagerServiceTest { LocalServices.removeServiceForTest(WindowManagerInternal.class); LocalServices.addService(WindowManagerInternal.class, mWindowManagerInternal); + mAppOpsManager = mockAppOpsManager(); mContext.addMockSystemService(AppOpsManager.class, mAppOpsManager); mContext.addMockSystemService(KeyguardManager.class, mKeyguardManager); mContext.setMockPackageManager(mPackageManager); @@ -206,6 +209,17 @@ public class MediaProjectionManagerServiceTest { mService = new MediaProjectionManagerService(mContext); } + private static AppOpsManager mockAppOpsManager() { + return mock(AppOpsManager.class, invocationOnMock -> { + if (invocationOnMock.getMethod().getName().startsWith("noteOp")) { + // Mockito will return 0 for non-stubbed method which corresponds to MODE_ALLOWED + // and is not what we want. + return AppOpsManager.MODE_IGNORED; + } + return Answers.RETURNS_DEFAULTS.answer(invocationOnMock); + }); + } + @After public void tearDown() { LocalServices.removeServiceForTest(ActivityManagerInternal.class); @@ -305,8 +319,10 @@ public class MediaProjectionManagerServiceTest { public void testCreateProjection_keyguardLocked_AppOpMediaProjection() throws NameNotFoundException { MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); - doReturn(true).when(mAppOpsManager).isOperationActive(eq(AppOpsManager.OP_PROJECT_MEDIA), - eq(projection.uid), eq(projection.packageName)); + doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager) + .noteOpNoThrow(eq(AppOpsManager.OP_PROJECT_MEDIA), + eq(projection.uid), eq(projection.packageName), nullable(String.class), + nullable(String.class)); doReturn(true).when(mKeyguardManager).isKeyguardLocked(); doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission( @@ -1159,7 +1175,7 @@ public class MediaProjectionManagerServiceTest { doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(anyString(), any(ApplicationInfoFlags.class), any(UserHandle.class)); return service.createProjectionInternal(UID, PACKAGE_NAME, - TYPE_MIRRORING, /* isPermanentGrant= */ true, UserHandle.CURRENT); + TYPE_MIRRORING, /* isPermanentGrant= */ false, UserHandle.CURRENT); } // Set up preconditions for starting a projection, with no foreground service requirements. diff --git a/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java b/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java index f91f77a56385..cdfc521dff13 100644 --- a/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java @@ -86,7 +86,6 @@ public class PersistentDataBlockServiceTest { private File mDataBlockFile; private File mFrpSecretFile; private File mFrpSecretTmpFile; - private String mOemUnlockPropertyValue; private boolean mIsUpgradingFromPreV = false; @Mock private UserManager mUserManager; @@ -105,13 +104,6 @@ public class PersistentDataBlockServiceTest { } @Override - void setProperty(String key, String value) { - // Override to capture the value instead of actually setting the property. - assertThat(key).isEqualTo("sys.oem_unlock_allowed"); - mOemUnlockPropertyValue = value; - } - - @Override boolean isUpgradingFromPreVRelease() { return mIsUpgradingFromPreV; } @@ -598,7 +590,6 @@ public class PersistentDataBlockServiceTest { mInterface.setOemUnlockEnabled(true); assertThat(mInterface.getOemUnlockEnabled()).isTrue(); - assertThat(mOemUnlockPropertyValue).isEqualTo("1"); } @Test @@ -635,7 +626,6 @@ public class PersistentDataBlockServiceTest { // The current implementation does not check digest before set or get the oem unlock bit. tamperWithDigest(); mInterface.setOemUnlockEnabled(true); - assertThat(mOemUnlockPropertyValue).isEqualTo("1"); tamperWithDigest(); assertThat(mInterface.getOemUnlockEnabled()).isTrue(); } @@ -676,7 +666,6 @@ public class PersistentDataBlockServiceTest { mInternalInterface.forceOemUnlockEnabled(true); - assertThat(mOemUnlockPropertyValue).isEqualTo("1"); assertThat(readBackingFile(mPdbService.getOemUnlockDataOffset(), 1).array()) .isEqualTo(new byte[] { 1 }); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java index 62e5b9a3dccc..45cd5719cd86 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java @@ -31,6 +31,12 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.media.AudioAttributes.USAGE_NOTIFICATION; import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE; +import static com.android.server.notification.NotificationAttentionHelper.MUTE_REASON_COOLDOWN; +import static com.android.server.notification.NotificationAttentionHelper.MUTE_REASON_FLAG_SILENT; +import static com.android.server.notification.NotificationAttentionHelper.MUTE_REASON_GROUP_ALERT; +import static com.android.server.notification.NotificationAttentionHelper.MUTE_REASON_NOT_MUTED; +import static com.android.server.notification.NotificationAttentionHelper.MUTE_REASON_OTHER_INSISTENT_PLAYING; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; @@ -106,6 +112,7 @@ import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.Notificat import com.android.internal.config.sysui.TestableFlagResolver; import com.android.internal.logging.InstanceIdSequence; import com.android.internal.logging.InstanceIdSequenceFake; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.IntPair; import com.android.server.UiServiceTestCase; import com.android.server.lights.LightsManager; @@ -1276,7 +1283,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { verifyNeverBeep(); assertFalse(r.isInterruptive()); assertEquals(-1, r.getLastAudiblyAlertedMs()); - assertTrue(mAttentionHelper.shouldMuteNotificationLocked(r, DEFAULT_SIGNALS)); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(r, DEFAULT_SIGNALS, + true)).isEqualTo(MUTE_REASON_FLAG_SILENT); } @Test @@ -1295,7 +1303,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { verifyNeverBeep(); assertFalse(r.isInterruptive()); assertEquals(-1, r.getLastAudiblyAlertedMs()); - assertTrue(mAttentionHelper.shouldMuteNotificationLocked(r, DEFAULT_SIGNALS)); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(r, DEFAULT_SIGNALS, + true)).isEqualTo(MUTE_REASON_GROUP_ALERT); } @Test @@ -1861,7 +1870,9 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { verifyBeepLooped(); NotificationRecord interrupter = getBeepyOtherNotification(); - assertTrue(mAttentionHelper.shouldMuteNotificationLocked(interrupter, DEFAULT_SIGNALS)); + assertThat( + mAttentionHelper.shouldMuteNotificationLocked(interrupter, DEFAULT_SIGNALS, + true)).isEqualTo(MUTE_REASON_OTHER_INSISTENT_PLAYING); mAttentionHelper.buzzBeepBlinkLocked(interrupter, DEFAULT_SIGNALS); verifyBeep(1); @@ -1879,16 +1890,16 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { ringtoneChannel.enableVibration(true); NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true); mService.addNotification(ringtoneNotification); - assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, - DEFAULT_SIGNALS)); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, + DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_NOT_MUTED); mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS); verifyBeepLooped(); verifyDelayedVibrateLooped(); Mockito.reset(mVibrator); Mockito.reset(mRingtonePlayer); - assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, - DEFAULT_SIGNALS)); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, + DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_NOT_MUTED); mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS); // beep wasn't reset @@ -1907,8 +1918,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { ringtoneChannel.enableVibration(true); NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true); mService.addNotification(ringtoneNotification); - assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, - DEFAULT_SIGNALS)); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, + DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_NOT_MUTED); mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS); verifyBeepLooped(); verifyDelayedVibrateLooped(); @@ -1930,8 +1941,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { ringtoneChannel.enableVibration(true); NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true); mService.addNotification(ringtoneNotification); - assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, - DEFAULT_SIGNALS)); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, + DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_NOT_MUTED); mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS); verifyBeepLooped(); verifyNeverVibrate(); @@ -1951,14 +1962,15 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build()); ringtoneChannel.enableVibration(true); NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true); - assertFalse(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, - DEFAULT_SIGNALS)); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(ringtoneNotification, + DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_NOT_MUTED); mAttentionHelper.buzzBeepBlinkLocked(ringtoneNotification, DEFAULT_SIGNALS); verifyVibrateLooped(); NotificationRecord interrupter = getBuzzyOtherNotification(); - assertTrue(mAttentionHelper.shouldMuteNotificationLocked(interrupter, DEFAULT_SIGNALS)); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(interrupter, + DEFAULT_SIGNALS, true)).isEqualTo(MUTE_REASON_OTHER_INSISTENT_PLAYING); mAttentionHelper.buzzBeepBlinkLocked(interrupter, DEFAULT_SIGNALS); verifyVibrate(1); @@ -2260,10 +2272,13 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); - mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); - verifyBeepVolume(0.0f); + int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); + assertThat(mAttentionHelper.shouldMuteNotificationLocked(r, DEFAULT_SIGNALS, true)) + .isEqualTo(MUTE_REASON_COOLDOWN); - verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); + verify(mAccessibilityService, times(2)).sendAccessibilityEvent(any(), anyInt()); assertEquals(-1, r.getLastAudiblyAlertedMs()); } @@ -2305,8 +2320,9 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); - mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); - verifyBeepVolume(0.0f); + int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); assertEquals(-1, r3.getLastAudiblyAlertedMs()); @@ -2381,9 +2397,10 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { false, null, Notification.GROUP_ALERT_ALL, false, mUser, "anotherPkg"); // update should beep at 0% volume - mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); + int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); assertEquals(-1, r2.getLastAudiblyAlertedMs()); - verifyBeepVolume(0.0f); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); // Use different package for next notifications NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */, @@ -2392,8 +2409,9 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); - mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); - verifyBeepVolume(0.0f); + buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); assertEquals(-1, r3.getLastAudiblyAlertedMs()); @@ -2493,8 +2511,9 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // Regular notification: should beep at 0% volume NotificationRecord r = getBeepyNotification(); - mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); - verifyBeepVolume(0.0f); + int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); assertEquals(-1, r.getLastAudiblyAlertedMs()); Mockito.reset(mRingtonePlayer); @@ -2525,8 +2544,9 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); - mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); - verifyBeepVolume(0.0f); + buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); // Set important conversation mChannel.setImportantConversation(true); @@ -2751,9 +2771,10 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { Mockito.reset(mRingtonePlayer); // next update at 0% volume - mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); + int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); assertEquals(-1, summary.getLastAudiblyAlertedMs()); - verifyBeepVolume(0.0f); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); } @@ -2823,9 +2844,10 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); - mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); + int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); assertEquals(-1, r2.getLastAudiblyAlertedMs()); - verifyBeepVolume(0.0f); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); // Use different package for next notifications NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */, @@ -2891,6 +2913,94 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { } @Test + public void testBeepVolume_politeNotif_groupAlertSummary() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); + mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); + TestableFlagResolver flagResolver = new TestableFlagResolver(); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); + // NOTIFICATION_COOLDOWN_ALL setting is enabled + Settings.System.putInt(getContext().getContentResolver(), + Settings.System.NOTIFICATION_COOLDOWN_ALL, 1); + initAttentionHelper(flagResolver); + + // child should beep at 0% volume + NotificationRecord child = getBeepyNotificationRecord("a", GROUP_ALERT_SUMMARY); + mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertFalse(child.isInterruptive()); + assertEquals(-1, child.getLastAudiblyAlertedMs()); + Mockito.reset(mRingtonePlayer); + + // child should beep at 0% volume + child = getBeepyNotificationRecord("a", GROUP_ALERT_SUMMARY); + mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertFalse(child.isInterruptive()); + assertEquals(-1, child.getLastAudiblyAlertedMs()); + Mockito.reset(mRingtonePlayer); + + // summary 100% volume (GROUP_ALERT_SUMMARY) + NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_SUMMARY); + summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY; + mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); + assertNotEquals(-1, summary.getLastAudiblyAlertedMs()); + verifyBeepVolume(1.0f); + Mockito.reset(mRingtonePlayer); + + // next update at 50% volume because only summary was tracked as alerting + mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); + assertNotEquals(-1, summary.getLastAudiblyAlertedMs()); + verifyBeepVolume(0.5f); + + verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt()); + } + + @Test + public void testBeepVolume_politeNotif_groupAlertChildren() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); + mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); + TestableFlagResolver flagResolver = new TestableFlagResolver(); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); + // NOTIFICATION_COOLDOWN_ALL setting is enabled + Settings.System.putInt(getContext().getContentResolver(), + Settings.System.NOTIFICATION_COOLDOWN_ALL, 1); + initAttentionHelper(flagResolver); + + // summary 0% volume (GROUP_ALERT_CHILDREN) + NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_CHILDREN); + summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY; + mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertFalse(summary.isInterruptive()); + assertEquals(-1, summary.getLastAudiblyAlertedMs()); + Mockito.reset(mRingtonePlayer); + + // child should beep at 100% volume + NotificationRecord child = getBeepyNotificationRecord("a", GROUP_ALERT_CHILDREN); + mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS); + assertNotEquals(-1, child.getLastAudiblyAlertedMs()); + verifyBeepVolume(1.0f); + Mockito.reset(mRingtonePlayer); + + // child should beep at 50% volume + child = getBeepyNotificationRecord("a", GROUP_ALERT_CHILDREN); + mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS); + assertNotEquals(-1, child.getLastAudiblyAlertedMs()); + verifyBeepVolume(0.5f); + Mockito.reset(mRingtonePlayer); + + // child should beep at 0% volume + mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertTrue(child.isInterruptive()); + assertEquals(-1, child.getLastAudiblyAlertedMs()); + + verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt()); + } + + @Test public void testVibrationIntensity_politeNotif() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); TestableFlagResolver flagResolver = new TestableFlagResolver(); @@ -2914,8 +3024,9 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { Mockito.reset(vibratorHelper); // 2nd update should buzz at 0% intensity - mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); - verify(vibratorHelper, times(1)).scale(any(), eq(0.0f)); + int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); + verifyNeverVibrate(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); } @Test @@ -3007,10 +3118,11 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { // 2nd update should beep at 0% volume Mockito.reset(mRingtonePlayer); - mAttentionHelper.buzzBeepBlinkLocked(r, WORK_PROFILE_SIGNALS); - verifyBeepVolume(0.0f); + int buzzBeepBlink = mAttentionHelper.buzzBeepBlinkLocked(r, WORK_PROFILE_SIGNALS); + verifyNeverBeep(); + assertThat(buzzBeepBlink).isEqualTo(MetricsEvent.ALERT_MUTED | MUTE_REASON_COOLDOWN); - verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); + verify(mAccessibilityService, times(2)).sendAccessibilityEvent(any(), anyInt()); assertEquals(-1, r.getLastAudiblyAlertedMs()); } 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 6c9015d72d5a..bbf2cbdbc145 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -193,6 +193,7 @@ import android.app.Notification.MessagingStyle.Message; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationManager; +import android.app.NotificationManager.Policy; import android.app.PendingIntent; import android.app.Person; import android.app.RemoteInput; @@ -655,7 +656,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mAtm.getTaskToShowPermissionDialogOn(anyString(), anyInt())) .thenReturn(INVALID_TASK_ID); mContext.addMockSystemService(AppOpsManager.class, mock(AppOpsManager.class)); - when(mUm.getProfileIds(eq(mUserId), eq(false))).thenReturn(new int[] { mUserId }); + when(mUm.getProfileIds(eq(mUserId), anyBoolean())).thenReturn(new int[]{mUserId}); + when(mUmInternal.getProfileIds(eq(mUserId), anyBoolean())).thenReturn(new int[]{mUserId}); when(mAmi.getCurrentUserId()).thenReturn(mUserId); when(mPackageManagerClient.hasSystemFeature(FEATURE_TELECOM)).thenReturn(true); @@ -4652,7 +4654,42 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { doThrow(new SecurityException("no access")).when(mUgmInternal) .checkGrantUriPermission(eq(Process.myUid()), any(), eq(soundUri), - anyInt(), eq(Process.myUserHandle().getIdentifier())); + anyInt(), eq(Process.myUserHandle().getIdentifier())); + + mBinderService.updateNotificationChannelFromPrivilegedListener( + null, mPkg, Process.myUserHandle(), updatedNotificationChannel); + + 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 + testUpdateNotificationChannelFromPrivilegedListener_oldSoundNoUriPerm_newSoundHasUriPerm() + throws Exception { + mService.setPreferencesHelper(mPreferencesHelper); + when(mCompanionMgr.getAssociations(mPkg, mUserId)) + .thenReturn(singletonList(mock(AssociationInfo.class))); + when(mPreferencesHelper.getNotificationChannel(eq(mPkg), anyInt(), + eq(mTestNotificationChannel.getId()), anyBoolean())) + .thenReturn(mTestNotificationChannel); + + // Missing Uri permissions for the old channel sound + final Uri oldSoundUri = Settings.System.DEFAULT_NOTIFICATION_URI; + doThrow(new SecurityException("no access")).when(mUgmInternal) + .checkGrantUriPermission(eq(Process.myUid()), any(), eq(oldSoundUri), + anyInt(), eq(Process.myUserHandle().getIdentifier())); + + // Has Uri permissions for the old channel sound + final Uri newSoundUri = Uri.parse("content://media/test/sound/uri"); + final NotificationChannel updatedNotificationChannel = new NotificationChannel( + TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT); + updatedNotificationChannel.setSound(newSoundUri, + updatedNotificationChannel.getAudioAttributes()); mBinderService.updateNotificationChannelFromPrivilegedListener( null, mPkg, Process.myUserHandle(), updatedNotificationChannel); @@ -15936,6 +15973,57 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertThat(updatedRule.getValue().isEnabled()).isFalse(); } + @Test + @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI}) + public void setNotificationPolicy_fromSystemApp_appliesPriorityChannelsAllowed() + throws Exception { + setUpRealZenTest(); + // Start with hasPriorityChannels=true, allowPriorityChannels=true ("default"). + mService.mZenModeHelper.setNotificationPolicy(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. + int stateBlockingPriorityChannels = Policy.policyState(false, false); + mBinderService.setNotificationPolicy(mPkg, + new Policy(1, 0, 0, 0, stateBlockingPriorityChannels, 0), false); + + // hasPriorityChannels is untouched and allowPriorityChannels was updated. + assertThat(mBinderService.getNotificationPolicy(mPkg).priorityCategories).isEqualTo(1); + assertThat(mBinderService.getNotificationPolicy(mPkg).state).isEqualTo( + Policy.policyState(true, false)); + + // Same but setting allowPriorityChannels to true. + int stateAllowingPriorityChannels = Policy.policyState(false, true); + mBinderService.setNotificationPolicy(mPkg, + new Policy(2, 0, 0, 0, stateAllowingPriorityChannels, 0), false); + + assertThat(mBinderService.getNotificationPolicy(mPkg).priorityCategories).isEqualTo(2); + assertThat(mBinderService.getNotificationPolicy(mPkg).state).isEqualTo( + Policy.policyState(true, true)); + } + + @Test + @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI}) + @DisableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) + public void setNotificationPolicy_fromRegularAppThatCanModifyPolicy_ignoresState() + throws Exception { + setUpRealZenTest(); + // Start with hasPriorityChannels=true, allowPriorityChannels=true ("default"). + mService.mZenModeHelper.setNotificationPolicy(new Policy(0, 0, 0, 0, + Policy.policyState(true, true), 0), + ZenModeConfig.ORIGIN_SYSTEM, Process.SYSTEM_UID); + mService.setCallerIsNormalPackage(); + + mBinderService.setNotificationPolicy(mPkg, + new Policy(1, 0, 0, 0, Policy.policyState(false, false), 0), false); + + // Policy was updated but the attempt to change state was ignored (it's a @hide API). + assertThat(mBinderService.getNotificationPolicy(mPkg).priorityCategories).isEqualTo(1); + assertThat(mBinderService.getNotificationPolicy(mPkg).state).isEqualTo( + Policy.policyState(true, true)); + } + /** Prepares for a zen-related test that uses the real {@link ZenModeHelper}. */ private void setUpRealZenTest() throws Exception { when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt())) 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 a0c0df8853f9..d64b9e858c64 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -45,11 +45,13 @@ import static android.app.NotificationManager.IMPORTANCE_MAX; import static android.app.NotificationManager.IMPORTANCE_NONE; import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; import static android.app.NotificationManager.VISIBILITY_NO_OVERRIDE; +import static android.content.ContentResolver.SCHEME_ANDROID_RESOURCE; +import static android.content.ContentResolver.SCHEME_CONTENT; +import static android.content.ContentResolver.SCHEME_FILE; import static android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION; import static android.media.AudioAttributes.USAGE_NOTIFICATION; import static android.os.UserHandle.USER_ALL; import static android.os.UserHandle.USER_SYSTEM; - import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION; import static android.service.notification.Flags.notificationClassification; @@ -59,6 +61,7 @@ import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_P import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED; import static com.android.server.notification.Flags.FLAG_ALL_NOTIFS_NEED_TTL; +import static com.android.server.notification.Flags.FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI; import static com.android.server.notification.Flags.FLAG_PERSIST_INCOMPLETE_RESTORE_DATA; import static com.android.server.notification.NotificationChannelLogger.NotificationChannelEvent.NOTIFICATION_CHANNEL_UPDATED_BY_USER; import static com.android.server.notification.PreferencesHelper.DEFAULT_BUBBLE_PREFERENCE; @@ -84,6 +87,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -369,10 +373,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - false, mClock); + mUgmInternal, false, mClock); mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - false, mClock); + mUgmInternal, false, mClock); resetZenModeHelper(); mAudioAttributes = new AudioAttributes.Builder() @@ -783,7 +787,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testReadXml_oldXml_migrates() throws Exception { mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - /* showReviewPermissionsNotification= */ true, mClock); + mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); String xml = "<ranking version=\"2\">\n" + "<package name=\"" + PKG_N_MR1 + "\" uid=\"" + UID_N_MR1 @@ -919,7 +923,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception { mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - /* showReviewPermissionsNotification= */ true, mClock); + mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); String xml = "<ranking version=\"3\">\n" + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n" @@ -978,7 +982,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testReadXml_newXml_permissionNotificationOff() throws Exception { mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - /* showReviewPermissionsNotification= */ false, mClock); + mUgmInternal, /* showReviewPermissionsNotification= */ false, mClock); String xml = "<ranking version=\"3\">\n" + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n" @@ -1037,7 +1041,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception { mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - /* showReviewPermissionsNotification= */ true, mClock); + mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); String xml = "<ranking version=\"4\">\n" + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n" @@ -1709,7 +1713,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { // simulate load after reboot mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - false, mClock); + mUgmInternal, false, mClock); loadByteArrayXml(baos.toByteArray(), false, USER_ALL); // Trigger 2nd restore pass @@ -1764,7 +1768,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { // simulate load after reboot mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - false, mClock); + mUgmInternal, false, mClock); loadByteArrayXml(xml.getBytes(), false, USER_ALL); // Trigger 2nd restore pass @@ -1842,10 +1846,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - false, mClock); + mUgmInternal, false, mClock); mXmlHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - false, mClock); + mUgmInternal, false, mClock); NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_LOW); @@ -3049,6 +3053,64 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) + public void testCreateChannel_noSoundUriPermission_contentSchemeVerified() { + final Uri sound = Uri.parse(SCHEME_CONTENT + "://media/test/sound/uri"); + + doThrow(new SecurityException("no access")).when(mUgmInternal) + .checkGrantUriPermission(eq(UID_N_MR1), any(), eq(sound), + anyInt(), eq(Process.myUserHandle().getIdentifier())); + + final NotificationChannel channel = new NotificationChannel("id2", "name2", + NotificationManager.IMPORTANCE_DEFAULT); + channel.setSound(sound, mAudioAttributes); + + assertThrows(SecurityException.class, + () -> mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, + true, false, UID_N_MR1, false)); + assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true)) + .isNull(); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) + public void testCreateChannel_noSoundUriPermission_fileSchemaIgnored() { + final Uri sound = Uri.parse(SCHEME_FILE + "://path/sound"); + + doThrow(new SecurityException("no access")).when(mUgmInternal) + .checkGrantUriPermission(eq(UID_N_MR1), any(), any(), + anyInt(), eq(Process.myUserHandle().getIdentifier())); + + final NotificationChannel channel = new NotificationChannel("id2", "name2", + NotificationManager.IMPORTANCE_DEFAULT); + channel.setSound(sound, mAudioAttributes); + + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true) + .getSound()).isEqualTo(sound); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) + public void testCreateChannel_noSoundUriPermission_resourceSchemaIgnored() { + final Uri sound = Uri.parse(SCHEME_ANDROID_RESOURCE + "://resId/sound"); + + doThrow(new SecurityException("no access")).when(mUgmInternal) + .checkGrantUriPermission(eq(UID_N_MR1), any(), any(), + anyInt(), eq(Process.myUserHandle().getIdentifier())); + + final NotificationChannel channel = new NotificationChannel("id2", "name2", + NotificationManager.IMPORTANCE_DEFAULT); + channel.setSound(sound, mAudioAttributes); + + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true) + .getSound()).isEqualTo(sound); + } + + @Test public void testPermanentlyDeleteChannels() throws Exception { NotificationChannel channel1 = new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); 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 0d13be6d5ab2..e8ca8bf8ec63 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java @@ -127,13 +127,13 @@ public class VibratorControllerTest { public void setExternalControl_withCapability_enablesExternalControl() { mockVibratorCapabilities(IVibrator.CAP_EXTERNAL_CONTROL); VibratorController controller = createController(); - assertFalse(controller.isUnderExternalControl()); + assertFalse(controller.isVibrating()); controller.setExternalControl(true); - assertTrue(controller.isUnderExternalControl()); + assertTrue(controller.isVibrating()); controller.setExternalControl(false); - assertFalse(controller.isUnderExternalControl()); + assertFalse(controller.isVibrating()); InOrder inOrderVerifier = inOrder(mNativeWrapperMock); inOrderVerifier.verify(mNativeWrapperMock).setExternalControl(eq(true)); @@ -143,10 +143,10 @@ public class VibratorControllerTest { @Test public void setExternalControl_withNoCapability_ignoresExternalControl() { VibratorController controller = createController(); - assertFalse(controller.isUnderExternalControl()); + assertFalse(controller.isVibrating()); controller.setExternalControl(true); - assertFalse(controller.isUnderExternalControl()); + assertFalse(controller.isVibrating()); verify(mNativeWrapperMock, never()).setExternalControl(anyBoolean()); } @@ -181,6 +181,38 @@ public class VibratorControllerTest { } @Test + public void setAmplitude_vibratorIdle_ignoresAmplitude() { + VibratorController controller = createController(); + assertFalse(controller.isVibrating()); + + controller.setAmplitude(1); + assertEquals(0, controller.getCurrentAmplitude(), /* delta= */ 0); + } + + @Test + public void setAmplitude_vibratorUnderExternalControl_ignoresAmplitude() { + mockVibratorCapabilities(IVibrator.CAP_EXTERNAL_CONTROL); + VibratorController controller = createController(); + controller.setExternalControl(true); + assertTrue(controller.isVibrating()); + + controller.setAmplitude(1); + assertEquals(0, controller.getCurrentAmplitude(), /* delta= */ 0); + } + + @Test + public void setAmplitude_vibratorVibrating_setsAmplitude() { + when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0)); + VibratorController controller = createController(); + controller.on(100, /* vibrationId= */ 1); + assertTrue(controller.isVibrating()); + assertEquals(-1, controller.getCurrentAmplitude(), /* delta= */ 0); + + controller.setAmplitude(1); + assertEquals(1, controller.getCurrentAmplitude(), /* delta= */ 0); + } + + @Test public void on_withDuration_turnsVibratorOn() { when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0)); VibratorController controller = createController(); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java index d99b20c689dd..538c3fc2ddae 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -266,12 +266,13 @@ public class VibratorManagerServiceTest { @After public void tearDown() throws Exception { if (mService != null) { - if (!mPendingVibrations.stream().allMatch(HalVibration::hasEnded)) { - // Cancel any pending vibration from tests. - cancelVibrate(mService); - for (HalVibration vibration : mPendingVibrations) { - vibration.waitForEnd(); - } + // Make sure we have permission to cancel test vibrations, even if the test denied them. + grantPermission(android.Manifest.permission.VIBRATE); + // Cancel any pending vibration from tests, including external vibrations. + cancelVibrate(mService); + // Wait until pending vibrations end asynchronously. + for (HalVibration vibration : mPendingVibrations) { + vibration.waitForEnd(); } // Wait until all vibrators have stopped vibrating, waiting for ramp-down. // Note: if a test is flaky here something is wrong with the vibration finalization. @@ -2242,7 +2243,7 @@ public class VibratorManagerServiceTest { VibratorManagerService service = createSystemReadyService(); VibrationEffect effect = VibrationEffect.createOneShot(10 * TEST_TIMEOUT_MILLIS, 100); - vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS); + HalVibration vibration = vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS); // VibrationThread will start this vibration async, so wait until vibration is triggered. assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); @@ -2255,7 +2256,8 @@ public class VibratorManagerServiceTest { assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); // Vibration is cancelled. - assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); + vibration.waitForEnd(); + assertThat(vibration.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED); assertEquals(Arrays.asList(false, true), mVibratorProviders.get(1).getExternalControlStates()); } @@ -2296,7 +2298,7 @@ public class VibratorManagerServiceTest { VibrationEffect repeatingEffect = VibrationEffect.createWaveform( new long[]{100, 200, 300}, new int[]{128, 255, 255}, 1); - vibrate(service, repeatingEffect, ALARM_ATTRS); + HalVibration repeatingVibration = vibrate(service, repeatingEffect, ALARM_ATTRS); // VibrationThread will start this vibration async, so wait until vibration is triggered. assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); @@ -2308,7 +2310,8 @@ public class VibratorManagerServiceTest { assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); // Vibration is cancelled. - assertTrue(waitUntil(s -> !s.isVibrating(1), service, TEST_TIMEOUT_MILLIS)); + repeatingVibration.waitForEnd(); + assertThat(repeatingVibration.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED); assertEquals(Arrays.asList(false, true), mVibratorProviders.get(1).getExternalControlStates()); } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java index 5787780cef46..4cd75d5ba074 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java @@ -308,6 +308,8 @@ public class ActivityOptionsTest { // KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE case "android.activity.launchCookie": // KEY_LAUNCH_COOKIE case "android:activity.animAbortListener": // KEY_ANIM_ABORT_LISTENER + case "android.activity.allowPassThroughOnTouchOutside": + // KEY_ALLOW_PASS_THROUGH_ON_TOUCH_OUTSIDE // Existing keys break; 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 8227ed915c8e..92205f391f32 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java @@ -230,6 +230,10 @@ class AppCompatActivityRobot { mDisplayContent.setIgnoreOrientationRequest(enabled); } + void setTopActivityOrganizedTask() { + doReturn(mTaskStack.top()).when(mActivityStack.top()).getOrganizedTask(); + } + void setTopTaskInMultiWindowMode(boolean inMultiWindowMode) { doReturn(inMultiWindowMode).when(mTaskStack.top()).inMultiWindowMode(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java index d66c21a77fcd..b91a5b7afe26 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java @@ -28,7 +28,7 @@ import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_V import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM; +import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import android.compat.testing.PlatformCompatChangeRule; import android.platform.test.annotations.DisableFlags; @@ -218,7 +218,7 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase { } @Test - @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testShouldApplyCameraCompatFreeformTreatment_flagIsDisabled_returnsFalse() { runTestScenario((robot) -> { robot.activity().createActivityWithComponentInNewTask(); @@ -229,7 +229,7 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase { @Test @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT}) - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() { runTestScenario((robot) -> { robot.activity().createActivityWithComponentInNewTask(); @@ -240,7 +240,7 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase { @Test @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT}) - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testShouldApplyCameraCompatFreeformTreatment_disabledByOverride_returnsFalse() { runTestScenario((robot) -> { robot.activity().createActivityWithComponentInNewTask(); @@ -250,7 +250,7 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase { } @Test - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() { runTestScenario((robot) -> { robot.activity().createActivityWithComponentInNewTask(); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java index d91b38efd40b..41102d6922da 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java @@ -20,7 +20,7 @@ import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM; +import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -85,7 +85,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { } @Test - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testCameraCompatFreeformPolicy_presentWhenEnabledAndDW() { runTestScenario((robot) -> { robot.allowEnterDesktopMode(/* isAllowed= */ true); @@ -95,7 +95,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { } @Test - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testCameraCompatFreeformPolicy_notPresentWhenNoDW() { runTestScenario((robot) -> { robot.allowEnterDesktopMode(/* isAllowed= */ false); @@ -105,7 +105,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { } @Test - @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testCameraCompatFreeformPolicy_notPresentWhenNoFlag() { runTestScenario((robot) -> { robot.allowEnterDesktopMode(/* isAllowed= */ true); @@ -115,7 +115,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { } @Test - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testCameraCompatFreeformPolicy_notPresentWhenNoFlagAndNoDW() { runTestScenario((robot) -> { robot.allowEnterDesktopMode(/* isAllowed= */ false); @@ -125,7 +125,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { } @Test - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testCameraCompatFreeformPolicy_startedWhenEnabledAndDW() { runTestScenario((robot) -> { robot.allowEnterDesktopMode(/* isAllowed= */ true); @@ -136,7 +136,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { } @Test - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testCameraStateManager_existsWhenCameraCompatFreeformExists() { runTestScenario((robot) -> { robot.allowEnterDesktopMode(true); @@ -147,7 +147,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { } @Test - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testCameraStateManager_startedWhenCameraCompatFreeformExists() { runTestScenario((robot) -> { robot.allowEnterDesktopMode(true); @@ -180,7 +180,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { } @Test - @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testCameraStateManager_doesNotExistWhenNoPolicyExists() { runTestScenario((robot) -> { robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ false); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java index 21fac9bcd1e4..d8373c5dc3d6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java @@ -16,10 +16,14 @@ package com.android.server.wm; +import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE; + import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static org.mockito.Mockito.when; +import android.app.CameraCompatTaskInfo.FreeformCameraCompatMode; +import android.app.TaskInfo; import android.platform.test.annotations.Presubmit; import androidx.annotation.NonNull; @@ -114,6 +118,72 @@ public class AppCompatUtilsTest extends WindowTestsBase { }); } + @Test + public void testTopActivityEligibleForUserAspectRatioButton_eligible() { + runTestScenario((robot) -> { + robot.applyOnActivity((a) -> { + a.createActivityWithComponentInNewTask(); + a.setIgnoreOrientationRequest(true); + }); + robot.conf().enableUserAppAspectRatioSettings(true); + + robot.checkTaskInfoEligibleForUserAspectRatioButton(true); + }); + } + + @Test + public void testTopActivityEligibleForUserAspectRatioButton_disabled_notEligible() { + runTestScenario((robot) -> { + robot.applyOnActivity((a) -> { + a.createActivityWithComponentInNewTask(); + a.setIgnoreOrientationRequest(true); + }); + robot.conf().enableUserAppAspectRatioSettings(false); + + robot.checkTaskInfoEligibleForUserAspectRatioButton(false); + }); + } + + @Test + public void testTopActivityEligibleForUserAspectRatioButton_inSizeCompatMode_notEligible() { + runTestScenario((robot) -> { + robot.applyOnActivity((a) -> { + a.createActivityWithComponentInNewTask(); + a.setIgnoreOrientationRequest(true); + a.setTopActivityOrganizedTask(); + a.setTopActivityInSizeCompatMode(true); + a.setTopActivityVisible(true); + }); + robot.conf().enableUserAppAspectRatioSettings(true); + + robot.checkTaskInfoEligibleForUserAspectRatioButton(false); + }); + } + + @Test + public void testTopActivityEligibleForUserAspectRatioButton_transparentTop_notEligible() { + runTestScenario((robot) -> { + robot.transparentActivity((ta) -> { + ta.launchTransparentActivityInTask(); + ta.activity().setIgnoreOrientationRequest(true); + }); + robot.conf().enableUserAppAspectRatioSettings(true); + + robot.checkTaskInfoEligibleForUserAspectRatioButton(false); + }); + } + + @Test + public void getTaskInfoPropagatesCameraCompatMode() { + runTestScenario((robot) -> { + robot.applyOnActivity(AppCompatActivityRobot::createActivityWithComponentInNewTask); + + robot.setFreeformCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE); + robot.checkTaskInfoFreeformCameraCompatMode( + CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE); + }); + } + /** * Runs a test scenario providing a Robot. */ @@ -125,11 +195,14 @@ public class AppCompatUtilsTest extends WindowTestsBase { private static class AppCompatUtilsRobotTest extends AppCompatRobotBase { private final WindowState mWindowState; + @NonNull + private final AppCompatTransparentActivityRobot mTransparentActivityRobot; AppCompatUtilsRobotTest(@NonNull WindowManagerService wm, @NonNull ActivityTaskManagerService atm, @NonNull ActivityTaskSupervisor supervisor) { super(wm, atm, supervisor); + mTransparentActivityRobot = new AppCompatTransparentActivityRobot(activity()); mWindowState = Mockito.mock(WindowState.class); } @@ -139,6 +212,12 @@ public class AppCompatUtilsTest extends WindowTestsBase { spyOn(activity.mAppCompatController.getAppCompatAspectRatioPolicy()); } + void transparentActivity(@NonNull Consumer<AppCompatTransparentActivityRobot> consumer) { + // We always create at least an opaque activity in a Task. + activity().createNewTaskWithBaseActivity(); + consumer.accept(mTransparentActivityRobot); + } + void setIsLetterboxedForFixedOrientationAndAspectRatio( boolean forFixedOrientationAndAspectRatio) { when(activity().top().mAppCompatController.getAppCompatAspectRatioPolicy() @@ -155,11 +234,30 @@ public class AppCompatUtilsTest extends WindowTestsBase { when(mWindowState.isLetterboxedForDisplayCutout()).thenReturn(displayCutout); } + void setFreeformCameraCompatMode(@FreeformCameraCompatMode int mode) { + activity().top().mAppCompatController.getAppCompatCameraOverrides() + .setFreeformCameraCompatMode(mode); + } + void checkTopActivityLetterboxReason(@NonNull String expected) { Assert.assertEquals(expected, AppCompatUtils.getLetterboxReasonString(activity().top(), mWindowState)); } + @NonNull + TaskInfo getTopTaskInfo() { + return activity().top().getTask().getTaskInfo(); + } + + void checkTaskInfoEligibleForUserAspectRatioButton(boolean eligible) { + Assert.assertEquals(eligible, getTopTaskInfo().appCompatTaskInfo + .eligibleForUserAspectRatioButton()); + } + + void checkTaskInfoFreeformCameraCompatMode(@FreeformCameraCompatMode int mode) { + Assert.assertEquals(mode, getTopTaskInfo().appCompatTaskInfo + .cameraCompatTaskInfo.freeformCameraCompatMode); + } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java index a48813d775d1..dbcef10a6be2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java @@ -35,7 +35,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; -import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM; +import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -60,6 +60,7 @@ import android.content.res.Configuration.Orientation; import android.graphics.Rect; import android.hardware.camera2.CameraManager; import android.os.Handler; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.view.DisplayInfo; import android.view.Surface; @@ -135,7 +136,6 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { }); mActivityRefresher = new ActivityRefresher(mDisplayContent.mWmService, mMockHandler); - mSetFlagsRule.enableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM); CameraStateMonitor cameraStateMonitor = new CameraStateMonitor(mDisplayContent, mMockHandler); mCameraCompatFreeformPolicy = @@ -147,6 +147,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { cameraStateMonitor.startListeningToCameraState(); } + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test public void testFullscreen_doesNotActivateCameraCompatMode() { configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); @@ -157,6 +158,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { assertNotInCameraCompatMode(); } + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test public void testOrientationUnspecified_doesNotActivateCameraCompatMode() { configureActivity(SCREEN_ORIENTATION_UNSPECIFIED); @@ -164,12 +166,14 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { assertNotInCameraCompatMode(); } + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test public void testNoCameraConnection_doesNotActivateCameraCompatMode() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); assertNotInCameraCompatMode(); } + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -210,6 +214,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { assertActivityRefreshRequested(/* refreshRequested */ false); } + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test public void testCameraReconnected_cameraCompatModeAndRefresh() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -235,6 +240,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { } @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT}) public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -246,6 +252,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { } @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -254,6 +261,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { } @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -268,6 +276,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { } @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception { when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) .thenReturn(false); @@ -281,6 +290,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { } @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 85cb1bcc01fb..5c0d424f4f42 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -83,7 +83,7 @@ import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_TOKEN_TRANSFO import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS; import static com.android.server.wm.WindowContainer.POSITION_TOP; import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL; -import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM; +import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE; import static com.google.common.truth.Truth.assertThat; @@ -2820,7 +2820,7 @@ public class DisplayContentTests extends WindowTestsBase { verify(mWm.mUmInternal, never()).isUserVisible(userId2, displayId); } - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test public void cameraCompatFreeformFlagEnabled_cameraCompatFreeformPolicyNotNull() { doReturn(true).when(() -> @@ -2829,7 +2829,7 @@ public class DisplayContentTests extends WindowTestsBase { assertTrue(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy()); } - @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test public void cameraCompatFreeformFlagNotEnabled_cameraCompatFreeformPolicyIsNull() { doReturn(true).when(() -> @@ -2838,7 +2838,7 @@ public class DisplayContentTests extends WindowTestsBase { assertFalse(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy()); } - @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM) + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) @Test public void desktopWindowingFlagNotEnabled_cameraCompatFreeformPolicyIsNull() { diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java index e0344d73f540..df17cd1d24b7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java @@ -432,6 +432,29 @@ public class RecentTasksTest extends WindowTestsBase { } @Test + public void testAddTaskCompatibleWindowingMode_withFreeformAndFullscreen_expectRemove() { + Task task1 = createTaskBuilder(".Task1") + .setFlags(FLAG_ACTIVITY_NEW_TASK) + .build(); + doReturn(WINDOWING_MODE_FREEFORM).when(task1).getWindowingMode(); + mRecentTasks.add(task1); + mCallbacksRecorder.clear(); + + Task task2 = createTaskBuilder(".Task1") + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setFlags(FLAG_ACTIVITY_NEW_TASK) + .build(); + assertEquals(WINDOWING_MODE_FULLSCREEN, task2.getWindowingMode()); + mRecentTasks.add(task2); + + assertThat(mCallbacksRecorder.mAdded).hasSize(1); + assertThat(mCallbacksRecorder.mAdded).contains(task2); + assertThat(mCallbacksRecorder.mTrimmed).isEmpty(); + assertThat(mCallbacksRecorder.mRemoved).hasSize(1); + assertThat(mCallbacksRecorder.mRemoved).contains(task1); + } + + @Test public void testAddTaskIncompatibleWindowingMode_expectNoRemove() { Task task1 = createTaskBuilder(".Task1") .setWindowingMode(WINDOWING_MODE_FULLSCREEN) diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 8fa4667c3b24..adc969c40e35 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -4849,6 +4849,39 @@ public class SizeCompatTests extends WindowTestsBase { } @Test + public void testUniversalResizeable() { + mWm.mConstants.mIgnoreActivityOrientationRequest = true; + setUpApp(mDisplayContent); + final float maxAspect = 1.8f; + final float minAspect = 1.5f; + prepareLimitedBounds(mActivity, maxAspect, minAspect, + ActivityInfo.SCREEN_ORIENTATION_LOCKED, true /* isUnresizable */); + + assertTrue(mActivity.isUniversalResizeable()); + assertTrue(mActivity.isResizeable()); + assertFalse(mActivity.shouldCreateAppCompatDisplayInsets()); + assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, mActivity.getOverrideOrientation()); + assertEquals(mActivity.getTask().getBounds(), mActivity.getBounds()); + final AppCompatAspectRatioPolicy aspectRatioPolicy = mActivity.mAppCompatController + .getAppCompatAspectRatioPolicy(); + assertEquals(0, aspectRatioPolicy.getMaxAspectRatio(), 0 /* delta */); + assertEquals(0, aspectRatioPolicy.getMinAspectRatio(), 0 /* delta */); + + // Compat override can still take effect. + final AppCompatAspectRatioOverrides aspectRatioOverrides = + mActivity.mAppCompatController.getAppCompatAspectRatioOverrides(); + spyOn(aspectRatioOverrides); + doReturn(true).when(aspectRatioOverrides).shouldOverrideMinAspectRatio(); + assertEquals(minAspect, aspectRatioPolicy.getMinAspectRatio(), 0 /* delta */); + + // User override can still take effect. + doReturn(true).when(aspectRatioOverrides).shouldApplyUserMinAspectRatioOverride(); + assertFalse(mActivity.isResizeable()); + assertEquals(maxAspect, aspectRatioPolicy.getMaxAspectRatio(), 0 /* delta */); + assertNotEquals(SCREEN_ORIENTATION_UNSPECIFIED, mActivity.getOverrideOrientation()); + } + + @Test public void testClearSizeCompat_resetOverrideConfig() { final int origDensity = 480; final int newDensity = 520; diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index 4b03483d43b9..e4512c31069a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -18,7 +18,6 @@ package com.android.server.wm; import static android.app.ActivityTaskManager.INVALID_TASK_ID; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; @@ -705,50 +704,6 @@ public class TaskTests extends WindowTestsBase { } @Test - public void testTopActivityEligibleForUserAspectRatioButton() { - DisplayContent display = mAtm.mRootWindowContainer.getDefaultDisplay(); - final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true) - .setWindowingMode(WINDOWING_MODE_FULLSCREEN).setDisplay(display).build(); - final Task task = rootTask.getBottomMostTask(); - final ActivityRecord root = task.getTopNonFinishingActivity(); - spyOn(mWm.mAppCompatConfiguration); - spyOn(root); - spyOn(root.mAppCompatController.getAppCompatAspectRatioOverrides()); - - doReturn(true).when(root).fillsParent(); - doReturn(true).when( - root.mAppCompatController.getAppCompatAspectRatioOverrides()) - .shouldEnableUserAspectRatioSettings(); - doReturn(false).when(root).inSizeCompatMode(); - doReturn(task).when(root).getOrganizedTask(); - - // The button should be eligible to be displayed - assertTrue(task.getTaskInfo() - .appCompatTaskInfo.eligibleForUserAspectRatioButton()); - - // When shouldApplyUserMinAspectRatioOverride is disable the button is not enabled - doReturn(false).when( - root.mAppCompatController.getAppCompatAspectRatioOverrides()) - .shouldEnableUserAspectRatioSettings(); - assertFalse(task.getTaskInfo() - .appCompatTaskInfo.eligibleForUserAspectRatioButton()); - doReturn(true).when(root.mAppCompatController - .getAppCompatAspectRatioOverrides()).shouldEnableUserAspectRatioSettings(); - - // When in size compat mode the button is not enabled - doReturn(true).when(root).inSizeCompatMode(); - assertFalse(task.getTaskInfo() - .appCompatTaskInfo.eligibleForUserAspectRatioButton()); - doReturn(false).when(root).inSizeCompatMode(); - - // When the top activity is transparent, the button is not enabled - doReturn(false).when(root).fillsParent(); - assertFalse(task.getTaskInfo() - .appCompatTaskInfo.eligibleForUserAspectRatioButton()); - doReturn(true).when(root).fillsParent(); - } - - @Test public void testIsTopActivityTranslucent() { DisplayContent display = mAtm.mRootWindowContainer.getDefaultDisplay(); final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true) @@ -2112,17 +2067,6 @@ public class TaskTests extends WindowTestsBase { } @Test - public void getTaskInfoPropagatesCameraCompatMode() { - final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build(); - final ActivityRecord activity = task.getTopMostActivity(); - activity.mAppCompatController.getAppCompatCameraOverrides().setFreeformCameraCompatMode( - CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE); - - assertEquals(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE, - task.getTaskInfo().appCompatTaskInfo.cameraCompatTaskInfo.freeformCameraCompatMode); - } - - @Test public void testUpdateTaskDescriptionOnReparent() { final Task rootTask1 = createTask(mDisplayContent); final Task rootTask2 = createTask(mDisplayContent); diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java index 6c1e1a428fb8..129494517cd6 100644 --- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java +++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java @@ -1027,28 +1027,36 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser boolean enabled = (mCurrentFunctions & UsbManager.FUNCTION_MIDI) != 0; if (enabled != mMidiEnabled) { if (enabled) { + boolean midiDeviceFound = false; if (android.hardware.usb.flags.Flags.enableUsbSysfsMidiIdentification()) { try { getMidiCardDevice(); + midiDeviceFound = true; } catch (FileNotFoundException e) { - Slog.e(TAG, "could not identify MIDI device", e); - enabled = false; + Slog.w(TAG, "could not identify MIDI device", e); } - } else { + } + // For backward compatibility with older kernels without + // https://lore.kernel.org/r/20240307030922.3573161-1-royluo@google.com + if (!midiDeviceFound) { Scanner scanner = null; try { scanner = new Scanner(new File(MIDI_ALSA_PATH)); mMidiCard = scanner.nextInt(); mMidiDevice = scanner.nextInt(); + midiDeviceFound = true; } catch (FileNotFoundException e) { Slog.e(TAG, "could not open MIDI file", e); - enabled = false; } finally { if (scanner != null) { scanner.close(); } } } + if (!midiDeviceFound) { + Slog.e(TAG, "Failed to enable MIDI function"); + enabled = false; + } } mMidiEnabled = enabled; } diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 3e226ccf2737..92effe05882a 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -19426,4 +19426,48 @@ public class TelephonyManager { return "UNKNOWN(" + state + ")"; } } + + /** + * This API can be used by only CTS to override the Euicc UI component. + * + * @param componentName ui component to be launched for testing. {@code null} to reset. + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + public void setTestEuiccUiComponent(@Nullable ComponentName componentName) { + try { + ITelephony telephony = getITelephony(); + if (telephony == null) { + Rlog.e(TAG, "setTestEuiccUiComponent(): ITelephony instance is NULL"); + throw new IllegalStateException("Telephony service not available."); + } + telephony.setTestEuiccUiComponent(componentName); + } catch (RemoteException ex) { + Rlog.e(TAG, "setTestEuiccUiComponent() RemoteException : " + ex); + throw ex.rethrowAsRuntimeException(); + } + } + + /** + * This API can be used by only CTS to retrieve the Euicc UI component. + * + * @return The Euicc UI component for testing. {@code null} if not available. + * @hide + */ + @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @Nullable + public ComponentName getTestEuiccUiComponent() { + try { + ITelephony telephony = getITelephony(); + if (telephony == null) { + Rlog.e(TAG, "getTestEuiccUiComponent(): ITelephony instance is NULL"); + throw new IllegalStateException("Telephony service not available."); + } + return telephony.getTestEuiccUiComponent(); + } catch (RemoteException ex) { + Rlog.e(TAG, "getTestEuiccUiComponent() RemoteException : " + ex); + throw ex.rethrowAsRuntimeException(); + } + } } diff --git a/telephony/java/android/telephony/data/ApnSetting.java b/telephony/java/android/telephony/data/ApnSetting.java index 44d3fca6aec6..567314beadd3 100644 --- a/telephony/java/android/telephony/data/ApnSetting.java +++ b/telephony/java/android/telephony/data/ApnSetting.java @@ -128,6 +128,12 @@ public class ApnSetting implements Parcelable { /** APN type for RCS (Rich Communication Services). */ @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) public static final int TYPE_RCS = ApnTypes.RCS; + /** APN type for OEM_PAID networks (Automotive PANS) */ + @FlaggedApi(Flags.FLAG_OEM_PAID_PRIVATE) + public static final int TYPE_OEM_PAID = 1 << 16; // TODO(b/366194627): ApnTypes.OEM_PAID; + /** APN type for OEM_PRIVATE networks (Automotive PANS) */ + @FlaggedApi(Flags.FLAG_OEM_PAID_PRIVATE) + public static final int TYPE_OEM_PRIVATE = 1 << 17; // TODO(b/366194627): ApnTypes.OEM_PRIVATE; /** @hide */ @IntDef(flag = true, prefix = {"TYPE_"}, value = { @@ -146,7 +152,9 @@ public class ApnSetting implements Parcelable { TYPE_BIP, TYPE_VSIM, TYPE_ENTERPRISE, - TYPE_RCS + TYPE_RCS, + TYPE_OEM_PAID, + TYPE_OEM_PRIVATE, }) @Retention(RetentionPolicy.SOURCE) public @interface ApnType { @@ -375,6 +383,27 @@ public class ApnSetting implements Parcelable { @SystemApi public static final String TYPE_RCS_STRING = "rcs"; + /** + * APN type for OEM_PAID networks (Automotive PANS) + * + * Note: String representations of APN types are intended for system apps to communicate with + * modem components or carriers. Non-system apps should use the integer variants instead. + * @hide + */ + @FlaggedApi(Flags.FLAG_OEM_PAID_PRIVATE) + @SystemApi + public static final String TYPE_OEM_PAID_STRING = "oem_paid"; + + /** + * APN type for OEM_PRIVATE networks (Automotive PANS) + * + * Note: String representations of APN types are intended for system apps to communicate with + * modem components or carriers. Non-system apps should use the integer variants instead. + * @hide + */ + @FlaggedApi(Flags.FLAG_OEM_PAID_PRIVATE) + @SystemApi + public static final String TYPE_OEM_PRIVATE_STRING = "oem_private"; /** @hide */ @IntDef(prefix = { "AUTH_TYPE_" }, value = { @@ -489,6 +518,8 @@ public class ApnSetting implements Parcelable { APN_TYPE_STRING_MAP.put(TYPE_VSIM_STRING, TYPE_VSIM); APN_TYPE_STRING_MAP.put(TYPE_BIP_STRING, TYPE_BIP); APN_TYPE_STRING_MAP.put(TYPE_RCS_STRING, TYPE_RCS); + APN_TYPE_STRING_MAP.put(TYPE_OEM_PAID_STRING, TYPE_OEM_PAID); + APN_TYPE_STRING_MAP.put(TYPE_OEM_PRIVATE_STRING, TYPE_OEM_PRIVATE); APN_TYPE_INT_MAP = new ArrayMap<>(); APN_TYPE_INT_MAP.put(TYPE_DEFAULT, TYPE_DEFAULT_STRING); @@ -507,6 +538,8 @@ public class ApnSetting implements Parcelable { APN_TYPE_INT_MAP.put(TYPE_VSIM, TYPE_VSIM_STRING); APN_TYPE_INT_MAP.put(TYPE_BIP, TYPE_BIP_STRING); APN_TYPE_INT_MAP.put(TYPE_RCS, TYPE_RCS_STRING); + APN_TYPE_INT_MAP.put(TYPE_OEM_PAID, TYPE_OEM_PAID_STRING); + APN_TYPE_INT_MAP.put(TYPE_OEM_PRIVATE, TYPE_OEM_PRIVATE_STRING); PROTOCOL_STRING_MAP = new ArrayMap<>(); PROTOCOL_STRING_MAP.put("IP", PROTOCOL_IP); @@ -2383,7 +2416,8 @@ public class ApnSetting implements Parcelable { public ApnSetting build() { if ((mApnTypeBitmask & (TYPE_DEFAULT | TYPE_MMS | TYPE_SUPL | TYPE_DUN | TYPE_HIPRI | TYPE_FOTA | TYPE_IMS | TYPE_CBS | TYPE_IA | TYPE_EMERGENCY | TYPE_MCX - | TYPE_XCAP | TYPE_VSIM | TYPE_BIP | TYPE_ENTERPRISE | TYPE_RCS)) == 0 + | TYPE_XCAP | TYPE_VSIM | TYPE_BIP | TYPE_ENTERPRISE | TYPE_RCS | TYPE_OEM_PAID + | TYPE_OEM_PRIVATE)) == 0 || TextUtils.isEmpty(mApnName) || TextUtils.isEmpty(mEntryName)) { return null; } diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 4eefaaca71f4..bd5c7597ba14 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -1113,6 +1113,12 @@ public final class SatelliteManager { * @hide */ public static final int DATAGRAM_TYPE_SMS = 6; + /** + * Datagram type indicating that the message to be sent is an SMS checking + * for pending incoming SMS. + * @hide + */ + public static final int DATAGRAM_TYPE_CHECK_PENDING_INCOMING_SMS = 7; /** @hide */ @IntDef(prefix = "DATAGRAM_TYPE_", value = { @@ -1122,7 +1128,8 @@ public final class SatelliteManager { DATAGRAM_TYPE_KEEP_ALIVE, DATAGRAM_TYPE_LAST_SOS_MESSAGE_STILL_NEED_HELP, DATAGRAM_TYPE_LAST_SOS_MESSAGE_NO_HELP_NEEDED, - DATAGRAM_TYPE_SMS + DATAGRAM_TYPE_SMS, + DATAGRAM_TYPE_CHECK_PENDING_INCOMING_SMS }) @Retention(RetentionPolicy.SOURCE) public @interface DatagramType {} diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index e57c207a0b3e..7f25ef25c9ac 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -3017,6 +3017,14 @@ interface ITelephony { boolean setSatelliteListeningTimeoutDuration(in long timeoutMillis); /** + * This API can be used by only CTS to control ingoring cellular service state event. + * + * @param enabled Whether to enable boolean config. + * @return {@code true} if the value is set successfully, {@code false} otherwise. + */ + boolean setSatelliteIgnoreCellularServiceState(in boolean enabled); + + /** * This API can be used by only CTS to update satellite pointing UI app package and class names. * * @param packageName The package name of the satellite pointing UI app. @@ -3409,4 +3417,20 @@ interface ITelephony { * @hide */ boolean setSatelliteSubscriberIdListChangedIntentComponent(in String name); + + /** + * This API can be used by only CTS to override the Euicc UI component. + * + * @param componentName ui component to be launched for testing + * @hide + */ + void setTestEuiccUiComponent(in ComponentName componentName); + + /** + * This API can be used by only CTS to retrieve the Euicc UI component. + * + * @return The Euicc UI component for testing. + * @hide + */ + ComponentName getTestEuiccUiComponent(); } diff --git a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml index 82de070921f0..8b65efdfb5f9 100644 --- a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml +++ b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml index 4ffb11ab92ae..3382c1e227b3 100644 --- a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml +++ b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml index 0fa4d07b2eca..e941e79faea3 100644 --- a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml +++ b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml index 4d9fefbc7d88..4e06dca17fe2 100644 --- a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml +++ b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/IME/AndroidTestTemplate.xml b/tests/FlickerTests/IME/AndroidTestTemplate.xml index b879c54dcab3..0cadd68597b6 100644 --- a/tests/FlickerTests/IME/AndroidTestTemplate.xml +++ b/tests/FlickerTests/IME/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- enable AOD --> <option name="set-secure-setting" key="doze_always_on" value="1" /> <!-- prevents the phone from restarting --> diff --git a/tests/FlickerTests/Notification/AndroidTestTemplate.xml b/tests/FlickerTests/Notification/AndroidTestTemplate.xml index 04b312a896b9..f32e8bed85ef 100644 --- a/tests/FlickerTests/Notification/AndroidTestTemplate.xml +++ b/tests/FlickerTests/Notification/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml index 8acdabc2337d..68ae4f1f7f4f 100644 --- a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml +++ b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml index 91ece214aad5..ec186723b4a4 100644 --- a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml +++ b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/testables/Android.bp b/tests/testables/Android.bp index 7596ee722d01..f2111856c666 100644 --- a/tests/testables/Android.bp +++ b/tests/testables/Android.bp @@ -25,7 +25,10 @@ package { java_library { name: "testables", - srcs: ["src/**/*.java"], + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], libs: [ "android.test.runner.stubs.system", "android.test.mock.stubs.system", diff --git a/tests/testables/src/android/testing/TestWithLooperRule.java b/tests/testables/src/android/testing/TestWithLooperRule.java index 37b39c314e53..10df17f991d3 100644 --- a/tests/testables/src/android/testing/TestWithLooperRule.java +++ b/tests/testables/src/android/testing/TestWithLooperRule.java @@ -34,13 +34,13 @@ import java.util.List; * Looper for the Statement. */ public class TestWithLooperRule implements MethodRule { - /* * This rule requires to be the inner most Rule, so the next statement is RunAfters * instead of another rule. You can set it by '@Rule(order = Integer.MAX_VALUE)' */ @Override public Statement apply(Statement base, FrameworkMethod method, Object target) { + // getting testRunner check, if AndroidTestingRunning then we skip this rule RunWith runWithAnnotation = target.getClass().getAnnotation(RunWith.class); if (runWithAnnotation != null) { @@ -97,6 +97,9 @@ public class TestWithLooperRule implements MethodRule { case "InvokeParameterizedMethod": this.wrapFieldMethodFor(next, "frameworkMethod", method, target); return; + case "ExpectException": + next = this.getNextStatement(next, "next"); + break; default: throw new Exception( String.format("Unexpected Statement received: [%s]", diff --git a/tests/testables/tests/Android.bp b/tests/testables/tests/Android.bp index 1eb36fa5f908..c23f41a6c3d4 100644 --- a/tests/testables/tests/Android.bp +++ b/tests/testables/tests/Android.bp @@ -34,6 +34,7 @@ android_test { "androidx.core_core-animation", "androidx.core_core-ktx", "androidx.test.rules", + "androidx.test.ext.junit", "hamcrest-library", "mockito-target-inline-minus-junit4", "testables", diff --git a/tests/testables/tests/src/android/testing/TestableLooperJUnit4Test.java b/tests/testables/tests/src/android/testing/TestableLooperJUnit4Test.java new file mode 100644 index 000000000000..b7d5e0e12942 --- /dev/null +++ b/tests/testables/tests/src/android/testing/TestableLooperJUnit4Test.java @@ -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 android.testing; + +import android.testing.TestableLooper.RunWithLooper; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test that TestableLooper now handles expected exceptions in tests + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +@RunWithLooper +public class TestableLooperJUnit4Test { + @Rule + public final TestWithLooperRule mTestWithLooperRule = new TestWithLooperRule(); + + @Test(expected = Exception.class) + public void testException() throws Exception { + throw new Exception("this exception is expected"); + } +} + diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp index df1d51e37660..064b4617b0a2 100644 --- a/tools/aapt2/Debug.cpp +++ b/tools/aapt2/Debug.cpp @@ -346,6 +346,21 @@ void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions& value->value->Accept(&body_printer); printer->Undent(); } + printer->Println("Flag disabled values:"); + for (const auto& value : entry.flag_disabled_values) { + printer->Print("("); + printer->Print(value->config.to_string()); + printer->Print(") "); + value->value->Accept(&headline_printer); + if (options.show_sources && !value->value->GetSource().path.empty()) { + printer->Print(" src="); + printer->Print(value->value->GetSource().to_string()); + } + printer->Println(); + printer->Indent(); + value->value->Accept(&body_printer); + printer->Undent(); + } printer->Undent(); } } diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h index a274f047586c..0d261abd728d 100644 --- a/tools/aapt2/Resource.h +++ b/tools/aapt2/Resource.h @@ -71,6 +71,17 @@ enum class ResourceType { enum class FlagStatus { NoFlag = 0, Disabled = 1, Enabled = 2 }; +struct FeatureFlagAttribute { + std::string name; + bool negated = false; + + std::string ToString() { + return (negated ? "!" : "") + name; + } + + bool operator==(const FeatureFlagAttribute& o) const = default; +}; + android::StringPiece to_string(ResourceType type); /** @@ -232,6 +243,12 @@ struct ResourceFile { // Exported symbols std::vector<SourcedResourceName> exported_symbols; + + // Flag status + FlagStatus flag_status = FlagStatus::NoFlag; + + // Flag + std::optional<FeatureFlagAttribute> flag; }; /** diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp index a5aecc855707..fce6aa7c80d9 100644 --- a/tools/aapt2/ResourceParser.cpp +++ b/tools/aapt2/ResourceParser.cpp @@ -107,9 +107,10 @@ struct ParsedResource { Visibility::Level visibility_level = Visibility::Level::kUndefined; bool staged_api = false; bool allow_new = false; - FlagStatus flag_status = FlagStatus::NoFlag; std::optional<OverlayableItem> overlayable_item; std::optional<StagedId> staged_alias; + std::optional<FeatureFlagAttribute> flag; + FlagStatus flag_status; std::string comment; std::unique_ptr<Value> value; @@ -151,6 +152,7 @@ static bool AddResourcesToTable(ResourceTable* table, android::IDiagnostics* dia } if (res->value != nullptr) { + res->value->SetFlag(res->flag); res->value->SetFlagStatus(res->flag_status); // Attach the comment, source and config to the value. res->value->SetComment(std::move(res->comment)); @@ -162,8 +164,6 @@ static bool AddResourcesToTable(ResourceTable* table, android::IDiagnostics* dia res_builder.SetStagedId(res->staged_alias.value()); } - res_builder.SetFlagStatus(res->flag_status); - bool error = false; if (!res->name.entry.empty()) { if (!table->AddResource(res_builder.Build(), diag)) { @@ -546,12 +546,26 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, {"symbol", std::mem_fn(&ResourceParser::ParseSymbol)}, }); - std::string resource_type = parser->element_name(); - auto flag_status = GetFlagStatus(parser); - if (!flag_status) { - return false; + std::string_view resource_type = parser->element_name(); + if (auto flag = ParseFlag(xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag"))) { + if (options_.flag) { + diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) + << "Resource flag are not allowed both in the path and in the file"); + return false; + } + out_resource->flag = std::move(flag); + std::string error; + auto flag_status = GetFlagStatus(out_resource->flag, options_.feature_flag_values, &error); + if (flag_status) { + out_resource->flag_status = flag_status.value(); + } else { + diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) << error); + return false; + } + } else if (options_.flag) { + out_resource->flag = options_.flag; + out_resource->flag_status = options_.flag_status; } - out_resource->flag_status = flag_status.value(); // The value format accepted for this resource. uint32_t resource_format = 0u; @@ -567,7 +581,7 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, // Items have their type encoded in the type attribute. if (std::optional<StringPiece> maybe_type = xml::FindNonEmptyAttribute(parser, "type")) { - resource_type = std::string(maybe_type.value()); + resource_type = maybe_type.value(); } else { diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) << "<item> must have a 'type' attribute"); @@ -590,7 +604,7 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, // Bags have their type encoded in the type attribute. if (std::optional<StringPiece> maybe_type = xml::FindNonEmptyAttribute(parser, "type")) { - resource_type = std::string(maybe_type.value()); + resource_type = maybe_type.value(); } else { diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) << "<bag> must have a 'type' attribute"); @@ -733,33 +747,6 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, return false; } -std::optional<FlagStatus> ResourceParser::GetFlagStatus(xml::XmlPullParser* parser) { - auto flag_status = FlagStatus::NoFlag; - - std::optional<StringPiece> flag = xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag"); - if (flag) { - auto flag_it = options_.feature_flag_values.find(flag.value()); - if (flag_it == options_.feature_flag_values.end()) { - diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) - << "Resource flag value undefined"); - return {}; - } - const auto& flag_properties = flag_it->second; - if (!flag_properties.read_only) { - diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) - << "Only read only flags may be used with resources"); - return {}; - } - if (!flag_properties.enabled.has_value()) { - diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) - << "Only flags with a value may be used with resources"); - return {}; - } - flag_status = flag_properties.enabled.value() ? FlagStatus::Enabled : FlagStatus::Disabled; - } - return flag_status; -} - bool ResourceParser::ParseItem(xml::XmlPullParser* parser, ParsedResource* out_resource, const uint32_t format) { @@ -1666,21 +1653,25 @@ bool ResourceParser::ParseArrayImpl(xml::XmlPullParser* parser, const std::string& element_namespace = parser->element_namespace(); const std::string& element_name = parser->element_name(); if (element_namespace.empty() && element_name == "item") { - auto flag_status = GetFlagStatus(parser); - if (!flag_status) { - error = true; - continue; - } + auto flag = ParseFlag(xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag")); std::unique_ptr<Item> item = ParseXml(parser, typeMask, kNoRawString); if (!item) { diag_->Error(android::DiagMessage(item_source) << "could not parse array item"); error = true; continue; } - item->SetFlagStatus(flag_status.value()); + item->SetFlag(flag); + std::string err; + auto status = GetFlagStatus(flag, options_.feature_flag_values, &err); + if (status) { + item->SetFlagStatus(status.value()); + } else { + diag_->Error(android::DiagMessage(item_source) << err); + error = true; + continue; + } item->SetSource(item_source); array->elements.emplace_back(std::move(item)); - } else if (!ShouldIgnoreElement(element_namespace, element_name)) { diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) << "unknown tag <" << element_namespace << ":" << element_name << ">"); diff --git a/tools/aapt2/ResourceParser.h b/tools/aapt2/ResourceParser.h index 442dea89ef40..90690d522ef2 100644 --- a/tools/aapt2/ResourceParser.h +++ b/tools/aapt2/ResourceParser.h @@ -57,6 +57,11 @@ struct ResourceParserOptions { std::optional<Visibility::Level> visibility; FeatureFlagValues feature_flag_values; + + // The flag that should be applied to all resources parsed + std::optional<FeatureFlagAttribute> flag; + + FlagStatus flag_status = FlagStatus::NoFlag; }; struct FlattenedXmlSubTree { @@ -85,8 +90,6 @@ class ResourceParser { private: DISALLOW_COPY_AND_ASSIGN(ResourceParser); - std::optional<FlagStatus> GetFlagStatus(xml::XmlPullParser* parser); - std::optional<FlattenedXmlSubTree> CreateFlattenSubTree(xml::XmlPullParser* parser); // Parses the XML subtree as a StyleString (flattened XML representation for strings with diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp index 97514599c0b1..5435cba290fc 100644 --- a/tools/aapt2/ResourceTable.cpp +++ b/tools/aapt2/ResourceTable.cpp @@ -101,6 +101,21 @@ struct lt_config_key_ref { } }; +struct ConfigFlagKey { + const ConfigDescription* config; + StringPiece product; + const FeatureFlagAttribute& flag; +}; + +struct lt_config_flag_key_ref { + template <typename T> + bool operator()(const T& lhs, const ConfigFlagKey& rhs) const noexcept { + return std::tie(lhs->config, lhs->product, lhs->value->GetFlag()->name, + lhs->value->GetFlag()->negated) < + std::tie(*rhs.config, rhs.product, rhs.flag.name, rhs.flag.negated); + } +}; + } // namespace ResourceTable::ResourceTable(ResourceTable::Validation validation) : validation_(validation) { @@ -213,6 +228,25 @@ std::vector<ResourceConfigValue*> ResourceEntry::FindAllValues(const ConfigDescr return results; } +ResourceConfigValue* ResourceEntry::FindOrCreateFlagDisabledValue( + const FeatureFlagAttribute& flag, const android::ConfigDescription& config, + android::StringPiece product) { + auto iter = std::lower_bound(flag_disabled_values.begin(), flag_disabled_values.end(), + ConfigFlagKey{&config, product, flag}, lt_config_flag_key_ref()); + if (iter != flag_disabled_values.end()) { + ResourceConfigValue* value = iter->get(); + const auto value_flag = value->value->GetFlag().value(); + if (value_flag.name == flag.name && value_flag.negated == flag.negated && + value->config == config && value->product == product) { + return value; + } + } + ResourceConfigValue* newValue = + flag_disabled_values.insert(iter, util::make_unique<ResourceConfigValue>(config, product)) + ->get(); + return newValue; +} + bool ResourceEntry::HasDefaultValue() const { // The default config should be at the top of the list, since the list is sorted. return !values.empty() && values.front()->config == ConfigDescription::DefaultConfig(); @@ -375,13 +409,14 @@ struct EntryViewComparer { } }; -void InsertEntryIntoTableView(ResourceTableView& table, const ResourceTablePackage* package, - const ResourceTableType* type, const std::string& entry_name, - const std::optional<ResourceId>& id, const Visibility& visibility, - const std::optional<AllowNew>& allow_new, - const std::optional<OverlayableItem>& overlayable_item, - const std::optional<StagedId>& staged_id, - const std::vector<std::unique_ptr<ResourceConfigValue>>& values) { +void InsertEntryIntoTableView( + ResourceTableView& table, const ResourceTablePackage* package, const ResourceTableType* type, + const std::string& entry_name, const std::optional<ResourceId>& id, + const Visibility& visibility, const std::optional<AllowNew>& allow_new, + const std::optional<OverlayableItem>& overlayable_item, + const std::optional<StagedId>& staged_id, + const std::vector<std::unique_ptr<ResourceConfigValue>>& values, + const std::vector<std::unique_ptr<ResourceConfigValue>>& flag_disabled_values) { SortedVectorInserter<ResourceTablePackageView, PackageViewComparer> package_inserter; SortedVectorInserter<ResourceTableTypeView, TypeViewComparer> type_inserter; SortedVectorInserter<ResourceTableEntryView, EntryViewComparer> entry_inserter; @@ -408,6 +443,9 @@ void InsertEntryIntoTableView(ResourceTableView& table, const ResourceTablePacka for (auto& value : values) { new_entry.values.emplace_back(value.get()); } + for (auto& value : flag_disabled_values) { + new_entry.flag_disabled_values.emplace_back(value.get()); + } entry_inserter.Insert(view_type->entries, std::move(new_entry)); } @@ -426,6 +464,21 @@ const ResourceConfigValue* ResourceTableEntryView::FindValue(const ConfigDescrip return nullptr; } +const ResourceConfigValue* ResourceTableEntryView::FindFlagDisabledValue( + const FeatureFlagAttribute& flag, const ConfigDescription& config, + android::StringPiece product) const { + auto iter = std::lower_bound(flag_disabled_values.begin(), flag_disabled_values.end(), + ConfigFlagKey{&config, product, flag}, lt_config_flag_key_ref()); + if (iter != values.end()) { + const ResourceConfigValue* value = *iter; + if (value->value->GetFlag() == flag && value->config == config && + StringPiece(value->product) == product) { + return value; + } + } + return nullptr; +} + ResourceTableView ResourceTable::GetPartitionedView(const ResourceTableViewOptions& options) const { ResourceTableView view; for (const auto& package : packages) { @@ -433,13 +486,13 @@ ResourceTableView ResourceTable::GetPartitionedView(const ResourceTableViewOptio for (const auto& entry : type->entries) { InsertEntryIntoTableView(view, package.get(), type.get(), entry->name, entry->id, entry->visibility, entry->allow_new, entry->overlayable_item, - entry->staged_id, entry->values); + entry->staged_id, entry->values, entry->flag_disabled_values); if (options.create_alias_entries && entry->staged_id) { auto alias_id = entry->staged_id.value().id; InsertEntryIntoTableView(view, package.get(), type.get(), entry->name, alias_id, entry->visibility, entry->allow_new, entry->overlayable_item, {}, - entry->values); + entry->values, entry->flag_disabled_values); } } } @@ -587,6 +640,25 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag) entry->staged_id = res.staged_id.value(); } + if (res.value != nullptr && res.value->GetFlagStatus() == FlagStatus::Disabled) { + auto disabled_config_value = + entry->FindOrCreateFlagDisabledValue(res.value->GetFlag().value(), res.config, res.product); + if (!disabled_config_value->value) { + // Resource does not exist, add it now. + // Must clone the value since it might be in the values vector as well + CloningValueTransformer cloner(&string_pool); + disabled_config_value->value = res.value->Transform(cloner); + } else { + diag->Error(android::DiagMessage(source) + << "duplicate value for resource '" << res.name << "' " << "with config '" + << res.config << "' and flag '" + << (res.value->GetFlag().value().negated ? "!" : "") + << res.value->GetFlag().value().name << "'"); + diag->Error(android::DiagMessage(source) << "resource previously defined here"); + return false; + } + } + if (res.value != nullptr) { auto config_value = entry->FindOrCreateValue(res.config, res.product); if (!config_value->value) { @@ -595,9 +667,9 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag) } else { // When validation is enabled, ensure that a resource cannot have multiple values defined for // the same configuration unless protected by flags. - auto result = - validate ? ResolveFlagCollision(config_value->value->GetFlagStatus(), res.flag_status) - : CollisionResult::kKeepBoth; + auto result = validate ? ResolveFlagCollision(config_value->value->GetFlagStatus(), + res.value->GetFlagStatus()) + : CollisionResult::kKeepBoth; if (result == CollisionResult::kConflict) { result = ResolveValueCollision(config_value->value.get(), res.value.get()); } @@ -771,11 +843,6 @@ NewResourceBuilder& NewResourceBuilder::SetAllowMangled(bool allow_mangled) { return *this; } -NewResourceBuilder& NewResourceBuilder::SetFlagStatus(FlagStatus flag_status) { - res_.flag_status = flag_status; - return *this; -} - NewResource NewResourceBuilder::Build() { return std::move(res_); } diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h index cba6b70cfbd6..b0e185536d16 100644 --- a/tools/aapt2/ResourceTable.h +++ b/tools/aapt2/ResourceTable.h @@ -136,6 +136,9 @@ class ResourceEntry { // The resource's values for each configuration. std::vector<std::unique_ptr<ResourceConfigValue>> values; + // The resource's values that are behind disabled flags. + std::vector<std::unique_ptr<ResourceConfigValue>> flag_disabled_values; + explicit ResourceEntry(android::StringPiece name) : name(name) { } @@ -148,6 +151,13 @@ class ResourceEntry { android::StringPiece product); std::vector<ResourceConfigValue*> FindAllValues(const android::ConfigDescription& config); + // Either returns the existing ResourceConfigValue in the disabled list with the given flag, + // config, and product or creates a new one and returns that. In either case the returned value + // does not have the flag set on the value so it must be set by the caller. + ResourceConfigValue* FindOrCreateFlagDisabledValue(const FeatureFlagAttribute& flag, + const android::ConfigDescription& config, + android::StringPiece product = {}); + template <typename Func> std::vector<ResourceConfigValue*> FindValuesIf(Func f) { std::vector<ResourceConfigValue*> results; @@ -215,9 +225,14 @@ struct ResourceTableEntryView { std::optional<OverlayableItem> overlayable_item; std::optional<StagedId> staged_id; std::vector<const ResourceConfigValue*> values; + std::vector<const ResourceConfigValue*> flag_disabled_values; const ResourceConfigValue* FindValue(const android::ConfigDescription& config, android::StringPiece product = {}) const; + + const ResourceConfigValue* FindFlagDisabledValue(const FeatureFlagAttribute& flag, + const android::ConfigDescription& config, + android::StringPiece product = {}) const; }; struct ResourceTableTypeView { @@ -269,7 +284,6 @@ struct NewResource { std::optional<AllowNew> allow_new; std::optional<StagedId> staged_id; bool allow_mangled = false; - FlagStatus flag_status = FlagStatus::NoFlag; }; struct NewResourceBuilder { @@ -283,7 +297,6 @@ struct NewResourceBuilder { NewResourceBuilder& SetAllowNew(AllowNew allow_new); NewResourceBuilder& SetStagedId(StagedId id); NewResourceBuilder& SetAllowMangled(bool allow_mangled); - NewResourceBuilder& SetFlagStatus(FlagStatus flag_status); NewResource Build(); private: diff --git a/tools/aapt2/ResourceValues.cpp b/tools/aapt2/ResourceValues.cpp index b75e87c90128..723cfc0e035b 100644 --- a/tools/aapt2/ResourceValues.cpp +++ b/tools/aapt2/ResourceValues.cpp @@ -1102,6 +1102,7 @@ template <typename T> std::unique_ptr<T> CopyValueFields(std::unique_ptr<T> new_value, const T* value) { new_value->SetSource(value->GetSource()); new_value->SetComment(value->GetComment()); + new_value->SetFlag(value->GetFlag()); new_value->SetFlagStatus(value->GetFlagStatus()); return new_value; } diff --git a/tools/aapt2/ResourceValues.h b/tools/aapt2/ResourceValues.h index a1b1839b19ef..e000c653b87a 100644 --- a/tools/aapt2/ResourceValues.h +++ b/tools/aapt2/ResourceValues.h @@ -65,10 +65,21 @@ class Value { return translatable_; } + void SetFlag(std::optional<FeatureFlagAttribute> val) { + flag_ = val; + } + + std::optional<FeatureFlagAttribute> GetFlag() const { + return flag_; + } + void SetFlagStatus(FlagStatus val) { flag_status_ = val; } + // If the value is behind a flag this returns whether that flag was enabled when the value was + // parsed by comparing it to the flags passed on the command line to aapt2 (taking into account + // negation if necessary). If there was no flag, FlagStatus::NoFlag is returned instead. FlagStatus GetFlagStatus() const { return flag_status_; } @@ -128,6 +139,7 @@ class Value { std::string comment_; bool weak_ = false; bool translatable_ = true; + std::optional<FeatureFlagAttribute> flag_; FlagStatus flag_status_ = FlagStatus::NoFlag; private: diff --git a/tools/aapt2/Resources.proto b/tools/aapt2/Resources.proto index 5c6408940b34..a0f60b62db3a 100644 --- a/tools/aapt2/Resources.proto +++ b/tools/aapt2/Resources.proto @@ -240,6 +240,9 @@ message Entry { // The staged resource ID of this finalized resource. StagedId staged_id = 7; + + // The set of values defined for this entry which are behind disabled flags + repeated ConfigValue flag_disabled_config_value = 8; } // A Configuration/Value pair. @@ -283,6 +286,8 @@ message Item { // The status of the flag the value is behind if any uint32 flag_status = 8; + bool flag_negated = 9; + string flag_name = 10; } // A CompoundValue is an abstract type. It represents a value that is a made of other values. diff --git a/tools/aapt2/ResourcesInternal.proto b/tools/aapt2/ResourcesInternal.proto index b0ed3da33368..f4735a2f6ce7 100644 --- a/tools/aapt2/ResourcesInternal.proto +++ b/tools/aapt2/ResourcesInternal.proto @@ -49,4 +49,9 @@ message CompiledFile { // Any symbols this file auto-generates/exports (eg. @+id/foo in an XML file). repeated Symbol exported_symbol = 5; + + // The status of the flag the file is behind if any + uint32 flag_status = 6; + bool flag_negated = 7; + string flag_name = 8; } diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp index 2a978a5153ca..52372fa38525 100644 --- a/tools/aapt2/cmd/Compile.cpp +++ b/tools/aapt2/cmd/Compile.cpp @@ -67,6 +67,7 @@ struct ResourcePathData { std::string resource_dir; std::string name; std::string extension; + std::string flag_name; // Original config str. We keep this because when we parse the config, we may add on // version qualifiers. We want to preserve the original input so the output is easily @@ -81,6 +82,22 @@ static std::optional<ResourcePathData> ExtractResourcePathData(const std::string std::string* out_error, const CompileOptions& options) { std::vector<std::string> parts = util::Split(path, dir_sep); + + std::string flag_name; + // Check for a flag + for (auto iter = parts.begin(); iter != parts.end();) { + if (iter->starts_with("flag(") && iter->ends_with(")")) { + if (!flag_name.empty()) { + if (out_error) *out_error = "resource path cannot contain more than one flag directory"; + return {}; + } + flag_name = iter->substr(5, iter->size() - 6); + iter = parts.erase(iter); + } else { + ++iter; + } + } + if (parts.size() < 2) { if (out_error) *out_error = "bad resource path"; return {}; @@ -131,6 +148,7 @@ static std::optional<ResourcePathData> ExtractResourcePathData(const std::string std::string(dir_str), std::string(name), std::string(extension), + std::move(flag_name), std::string(config_str), config}; } @@ -142,6 +160,9 @@ static std::string BuildIntermediateContainerFilename(const ResourcePathData& da name << "-" << data.config_str; } name << "_" << data.name; + if (!data.flag_name.empty()) { + name << ".(" << data.flag_name << ")"; + } if (!data.extension.empty()) { name << "." << data.extension; } @@ -163,7 +184,6 @@ static bool CompileTable(IAaptContext* context, const CompileOptions& options, << "failed to open file: " << fin->GetError()); return false; } - // Parse the values file from XML. xml::XmlPullParser xml_parser(fin.get()); @@ -176,6 +196,18 @@ static bool CompileTable(IAaptContext* context, const CompileOptions& options, // If visibility was forced, we need to use it when creating a new resource and also error if // we try to parse the <public>, <public-group>, <java-symbol> or <symbol> tags. parser_options.visibility = options.visibility; + parser_options.flag = ParseFlag(path_data.flag_name); + + if (parser_options.flag) { + std::string error; + auto flag_status = GetFlagStatus(parser_options.flag, options.feature_flag_values, &error); + if (flag_status) { + parser_options.flag_status = std::move(flag_status.value()); + } else { + context->GetDiagnostics()->Error(android::DiagMessage(path_data.source) << error); + return false; + } + } ResourceParser res_parser(context->GetDiagnostics(), &table, path_data.source, path_data.config, parser_options); @@ -402,6 +434,18 @@ static bool CompileXml(IAaptContext* context, const CompileOptions& options, xmlres->file.config = path_data.config; xmlres->file.source = path_data.source; xmlres->file.type = ResourceFile::Type::kProtoXml; + xmlres->file.flag = ParseFlag(path_data.flag_name); + + if (xmlres->file.flag) { + std::string error; + auto flag_status = GetFlagStatus(xmlres->file.flag, options.feature_flag_values, &error); + if (flag_status) { + xmlres->file.flag_status = flag_status.value(); + } else { + context->GetDiagnostics()->Error(android::DiagMessage(path_data.source) << error); + return false; + } + } // Collect IDs that are defined here. XmlIdCollector collector; @@ -491,6 +535,27 @@ static bool CompilePng(IAaptContext* context, const CompileOptions& options, res_file.source = path_data.source; res_file.type = ResourceFile::Type::kPng; + if (!path_data.flag_name.empty()) { + FeatureFlagAttribute flag; + auto name = path_data.flag_name; + if (name.starts_with('!')) { + flag.negated = true; + flag.name = name.substr(1); + } else { + flag.name = name; + } + res_file.flag = flag; + + std::string error; + auto flag_status = GetFlagStatus(flag, options.feature_flag_values, &error); + if (flag_status) { + res_file.flag_status = flag_status.value(); + } else { + context->GetDiagnostics()->Error(android::DiagMessage(path_data.source) << error); + return false; + } + } + { auto data = file->OpenAsData(); if (!data) { diff --git a/tools/aapt2/cmd/Diff.cpp b/tools/aapt2/cmd/Diff.cpp index 6da3176b2bee..d3750a6100d3 100644 --- a/tools/aapt2/cmd/Diff.cpp +++ b/tools/aapt2/cmd/Diff.cpp @@ -138,6 +138,22 @@ static bool EmitResourceEntryDiff(IAaptContext* context, LoadedApk* apk_a, } } + for (const ResourceConfigValue* config_value_a : entry_a.flag_disabled_values) { + auto config_value_b = entry_b.FindFlagDisabledValue(config_value_a->value->GetFlag().value(), + config_value_a->config); + if (!config_value_b) { + std::stringstream str_stream; + str_stream << "missing disabled value " << pkg_a.name << ":" << type_a.named_type << "/" + << entry_a.name << " config=" << config_value_a->config + << " flag=" << config_value_a->value->GetFlag()->ToString(); + EmitDiffLine(apk_b->GetSource(), str_stream.str()); + diff = true; + } else { + diff |= EmitResourceConfigValueDiff(context, apk_a, pkg_a, type_a, entry_a, config_value_a, + apk_b, pkg_b, type_b, entry_b, config_value_b); + } + } + // Check for any newly added config values. for (const ResourceConfigValue* config_value_b : entry_b.values) { auto config_value_a = entry_a.FindValue(config_value_b->config); @@ -149,6 +165,18 @@ static bool EmitResourceEntryDiff(IAaptContext* context, LoadedApk* apk_a, diff = true; } } + for (const ResourceConfigValue* config_value_b : entry_b.flag_disabled_values) { + auto config_value_a = entry_a.FindFlagDisabledValue(config_value_b->value->GetFlag().value(), + config_value_b->config); + if (!config_value_a) { + std::stringstream str_stream; + str_stream << "new disabled config " << pkg_b.name << ":" << type_b.named_type << "/" + << entry_b.name << " config=" << config_value_b->config + << " flag=" << config_value_b->value->GetFlag()->ToString(); + EmitDiffLine(apk_b->GetSource(), str_stream.str()); + diff = true; + } + } return diff; } diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp index 7739171b347f..08f8f0d85807 100644 --- a/tools/aapt2/cmd/Util.cpp +++ b/tools/aapt2/cmd/Util.cpp @@ -34,6 +34,44 @@ using ::android::base::StringPrintf; namespace aapt { +std::optional<FeatureFlagAttribute> ParseFlag(std::optional<std::string_view> flag_text) { + if (!flag_text || flag_text->empty()) { + return {}; + } + FeatureFlagAttribute flag; + if (flag_text->starts_with('!')) { + flag.negated = true; + flag.name = flag_text->substr(1); + } else { + flag.name = flag_text.value(); + } + return flag; +} + +std::optional<FlagStatus> GetFlagStatus(const std::optional<FeatureFlagAttribute>& flag, + const FeatureFlagValues& feature_flag_values, + std::string* out_err) { + if (!flag) { + return FlagStatus::NoFlag; + } + auto flag_it = feature_flag_values.find(flag->name); + if (flag_it == feature_flag_values.end()) { + *out_err = "Resource flag value undefined: " + flag->name; + return {}; + } + const auto& flag_properties = flag_it->second; + if (!flag_properties.read_only) { + *out_err = "Only read only flags may be used with resources: " + flag->name; + return {}; + } + if (!flag_properties.enabled.has_value()) { + *out_err = "Only flags with a value may be used with resources: " + flag->name; + return {}; + } + return (flag_properties.enabled.value() != flag->negated) ? FlagStatus::Enabled + : FlagStatus::Disabled; +} + std::optional<uint16_t> ParseTargetDensityParameter(StringPiece arg, android::IDiagnostics* diag) { ConfigDescription preferred_density_config; if (!ConfigDescription::Parse(arg, &preferred_density_config)) { diff --git a/tools/aapt2/cmd/Util.h b/tools/aapt2/cmd/Util.h index 6b8813b34082..d32e532b86a8 100644 --- a/tools/aapt2/cmd/Util.h +++ b/tools/aapt2/cmd/Util.h @@ -49,6 +49,12 @@ struct FeatureFlagProperties { using FeatureFlagValues = std::map<std::string, FeatureFlagProperties, std::less<>>; +std::optional<FeatureFlagAttribute> ParseFlag(std::optional<std::string_view> flag_text); + +std::optional<FlagStatus> GetFlagStatus(const std::optional<FeatureFlagAttribute>& flag, + const FeatureFlagValues& feature_flag_values, + std::string* out_err); + // Parses a configuration density (ex. hdpi, xxhdpi, 234dpi, anydpi, etc). // Returns Nothing and logs a human friendly error message if the string was not legal. std::optional<uint16_t> ParseTargetDensityParameter(android::StringPiece arg, diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp index 55f5e5668a16..8583cadff6d2 100644 --- a/tools/aapt2/format/proto/ProtoDeserialize.cpp +++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp @@ -536,6 +536,34 @@ static bool DeserializePackageFromPb(const pb::Package& pb_package, const ResStr config_value->value = DeserializeValueFromPb(pb_config_value.value(), src_pool, config, &out_table->string_pool, files, out_error); + + if (config_value->value == nullptr) { + return false; + } + } + + // flag disabled + for (const pb::ConfigValue& pb_config_value : pb_entry.flag_disabled_config_value()) { + const pb::Configuration& pb_config = pb_config_value.config(); + + ConfigDescription config; + if (!DeserializeConfigFromPb(pb_config, &config, out_error)) { + return false; + } + + FeatureFlagAttribute flag; + flag.name = pb_config_value.value().item().flag_name(); + flag.negated = pb_config_value.value().item().flag_negated(); + ResourceConfigValue* config_value = + entry->FindOrCreateFlagDisabledValue(std::move(flag), config, pb_config.product()); + if (config_value->value != nullptr) { + *out_error = "duplicate configuration in resource table"; + return false; + } + + config_value->value = DeserializeValueFromPb(pb_config_value.value(), src_pool, config, + &out_table->string_pool, files, out_error); + if (config_value->value == nullptr) { return false; } @@ -615,6 +643,12 @@ bool DeserializeCompiledFileFromPb(const pb::internal::CompiledFile& pb_file, out_file->source.path = pb_file.source_path(); out_file->type = DeserializeFileReferenceTypeFromPb(pb_file.type()); + out_file->flag_status = (FlagStatus)pb_file.flag_status(); + if (!pb_file.flag_name().empty()) { + out_file->flag = + FeatureFlagAttribute{.name = pb_file.flag_name(), .negated = pb_file.flag_negated()}; + } + std::string config_error; if (!DeserializeConfigFromPb(pb_file.config(), &out_file->config, &config_error)) { std::ostringstream error; @@ -748,7 +782,6 @@ std::unique_ptr<Value> DeserializeValueFromPb(const pb::Value& pb_value, if (value == nullptr) { return {}; } - } else if (pb_value.has_compound_value()) { const pb::CompoundValue& pb_compound_value = pb_value.compound_value(); switch (pb_compound_value.value_case()) { @@ -1018,6 +1051,12 @@ std::unique_ptr<Item> DeserializeItemFromPb(const pb::Item& pb_item, DeserializeItemFromPbInternal(pb_item, src_pool, config, value_pool, files, out_error); if (item) { item->SetFlagStatus((FlagStatus)pb_item.flag_status()); + if (!pb_item.flag_name().empty()) { + FeatureFlagAttribute flag; + flag.name = pb_item.flag_name(); + flag.negated = pb_item.flag_negated(); + item->SetFlag(std::move(flag)); + } } return item; } diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp index 5772b3b0b3e6..d83fe916ee95 100644 --- a/tools/aapt2/format/proto/ProtoSerialize.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize.cpp @@ -427,6 +427,14 @@ void SerializeTableToPb(const ResourceTable& table, pb::ResourceTable* out_table SerializeValueToPb(*config_value->value, pb_config_value->mutable_value(), source_pool.get()); } + + for (const ResourceConfigValue* config_value : entry.flag_disabled_values) { + pb::ConfigValue* pb_config_value = pb_entry->add_flag_disabled_config_value(); + SerializeConfig(config_value->config, pb_config_value->mutable_config()); + pb_config_value->mutable_config()->set_product(config_value->product); + SerializeValueToPb(*config_value->value, pb_config_value->mutable_value(), + source_pool.get()); + } } } } @@ -721,6 +729,11 @@ void SerializeValueToPb(const Value& value, pb::Value* out_value, android::Strin } if (out_value->has_item()) { out_value->mutable_item()->set_flag_status((uint32_t)value.GetFlagStatus()); + if (value.GetFlag()) { + const auto& flag = value.GetFlag(); + out_value->mutable_item()->set_flag_negated(flag->negated); + out_value->mutable_item()->set_flag_name(flag->name); + } } } @@ -730,6 +743,11 @@ void SerializeItemToPb(const Item& item, pb::Item* out_item) { item.Accept(&serializer); out_item->MergeFrom(value.item()); out_item->set_flag_status((uint32_t)item.GetFlagStatus()); + if (item.GetFlag()) { + const auto& flag = item.GetFlag(); + out_item->set_flag_negated(flag->negated); + out_item->set_flag_name(flag->name); + } } void SerializeCompiledFileToPb(const ResourceFile& file, pb::internal::CompiledFile* out_file) { @@ -737,6 +755,11 @@ void SerializeCompiledFileToPb(const ResourceFile& file, pb::internal::CompiledF out_file->set_source_path(file.source.path); out_file->set_type(SerializeFileReferenceTypeToPb(file.type)); SerializeConfig(file.config, out_file->mutable_config()); + out_file->set_flag_status((uint32_t)file.flag_status); + if (file.flag) { + out_file->set_flag_negated(file.flag->negated); + out_file->set_flag_name(file.flag->name); + } for (const SourcedResourceName& exported : file.exported_symbols) { pb::internal::CompiledFile_Symbol* pb_symbol = out_file->add_exported_symbol(); diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp index c456e5c296d2..7160b35033da 100644 --- a/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp @@ -31,13 +31,29 @@ genrule { "res/values/ints.xml", "res/values/strings.xml", "res/layout/layout1.xml", + "res/layout/layout3.xml", + "res/flag(test.package.falseFlag)/values/bools.xml", + "res/flag(test.package.falseFlag)/layout/layout2.xml", + "res/flag(test.package.falseFlag)/drawable/removedpng.png", + "res/flag(test.package.trueFlag)/layout/layout3.xml", + "res/values/flag(test.package.trueFlag)/bools.xml", + "res/values/flag(!test.package.trueFlag)/bools.xml", + "res/values/flag(!test.package.falseFlag)/bools.xml", ], out: [ + "drawable_removedpng.(test.package.falseFlag).png.flat", "values_bools.arsc.flat", + "values_bools.(test.package.falseFlag).arsc.flat", + "values_bools.(test.package.trueFlag).arsc.flat", + "values_bools.(!test.package.falseFlag).arsc.flat", + "values_bools.(!test.package.trueFlag).arsc.flat", "values_bools2.arsc.flat", "values_ints.arsc.flat", "values_strings.arsc.flat", "layout_layout1.xml.flat", + "layout_layout2.(test.package.falseFlag).xml.flat", + "layout_layout3.xml.flat", + "layout_layout3.(test.package.trueFlag).xml.flat", ], cmd: "$(location aapt2) compile $(in) -o $(genDir) " + "--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true", diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/drawable/removedpng.png b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/drawable/removedpng.png Binary files differnew file mode 100644 index 000000000000..8a9e6984be96 --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/drawable/removedpng.png diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/layout/layout2.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/layout/layout2.xml new file mode 100644 index 000000000000..dec5de72925a --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/layout/layout2.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > +</LinearLayout>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/values/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/values/bools.xml new file mode 100644 index 000000000000..c46c4d4d8546 --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/values/bools.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <bool name="bool7">false</bool> +</resources>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.trueFlag)/layout/layout3.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.trueFlag)/layout/layout3.xml new file mode 100644 index 000000000000..5aeee0ee1e28 --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.trueFlag)/layout/layout3.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > + <TextView android:id="@+id/text1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="foobar" /> +</LinearLayout>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout3.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout3.xml new file mode 100644 index 000000000000..dec5de72925a --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout3.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > +</LinearLayout>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml index 1ed0c8a5f1e6..35975ed1274a 100644 --- a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml @@ -9,4 +9,15 @@ <bool name="bool3">false</bool> <bool name="bool4" android:featureFlag="test.package.falseFlag">true</bool> + + <bool name="bool5">false</bool> + <bool name="bool5" android:featureFlag="!test.package.falseFlag">true</bool> + + <bool name="bool6">true</bool> + <bool name="bool6" android:featureFlag="!test.package.trueFlag">false</bool> + + <bool name="bool7">true</bool> + <bool name="bool8">false</bool> + <bool name="bool9">true</bool> + <bool name="bool10">false</bool> </resources>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(!test.package.falseFlag)/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(!test.package.falseFlag)/bools.xml new file mode 100644 index 000000000000..a63749c6ed7e --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(!test.package.falseFlag)/bools.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <bool name="bool10">true</bool> +</resources>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(!test.package.trueFlag)/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(!test.package.trueFlag)/bools.xml new file mode 100644 index 000000000000..bb5526e69f97 --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(!test.package.trueFlag)/bools.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <bool name="bool9">false</bool> +</resources>
\ No newline at end of file diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(test.package.trueFlag)/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(test.package.trueFlag)/bools.xml new file mode 100644 index 000000000000..eba780e88c9a --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(test.package.trueFlag)/bools.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <bool name="bool8">true</bool> +</resources>
\ No newline at end of file diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp index 3db37c2fa6f8..629300838bbe 100644 --- a/tools/aapt2/link/FlaggedResources_test.cpp +++ b/tools/aapt2/link/FlaggedResources_test.cpp @@ -17,6 +17,7 @@ #include "LoadedApk.h" #include "cmd/Dump.h" #include "io/StringStream.h" +#include "test/Common.h" #include "test/Test.h" #include "text/Printer.h" @@ -75,6 +76,10 @@ TEST_F(FlaggedResourcesTest, DisabledResourcesRemovedFromTable) { std::string output; DumpResourceTableToString(loaded_apk.get(), &output); + ASSERT_EQ(output.find("bool4"), std::string::npos); + ASSERT_EQ(output.find("str1"), std::string::npos); + ASSERT_EQ(output.find("layout2"), std::string::npos); + ASSERT_EQ(output.find("removedpng"), std::string::npos); } TEST_F(FlaggedResourcesTest, DisabledResourcesRemovedFromTableChunks) { @@ -86,6 +91,8 @@ TEST_F(FlaggedResourcesTest, DisabledResourcesRemovedFromTableChunks) { ASSERT_EQ(output.find("bool4"), std::string::npos); ASSERT_EQ(output.find("str1"), std::string::npos); + ASSERT_EQ(output.find("layout2"), std::string::npos); + ASSERT_EQ(output.find("removedpng"), std::string::npos); } TEST_F(FlaggedResourcesTest, DisabledResourcesInRJava) { @@ -98,4 +105,47 @@ TEST_F(FlaggedResourcesTest, DisabledResourcesInRJava) { ASSERT_NE(r_contents.find("public static final int str1"), std::string::npos); } +TEST_F(FlaggedResourcesTest, TwoValuesSameDisabledFlag) { + test::TestDiagnosticsImpl diag; + const std::string compiled_files_dir = GetTestPath("compiled"); + ASSERT_FALSE(CompileFile( + GetTestPath("res/values/values.xml"), + R"(<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <bool name="bool1" android:featureFlag="test.package.falseFlag">false</bool> + <bool name="bool1" android:featureFlag="test.package.falseFlag">true</bool> + </resources>)", + compiled_files_dir, &diag, + {"--feature-flags", "test.package.falseFlag:ro=false,test.package.trueFlag:ro=true"})); + ASSERT_TRUE(diag.GetLog().contains("duplicate value for resource 'bool/bool1'")); +} + +TEST_F(FlaggedResourcesTest, TwoValuesSameDisabledFlagDifferentFiles) { + test::TestDiagnosticsImpl diag; + const std::string compiled_files_dir = GetTestPath("compiled"); + ASSERT_TRUE(CompileFile( + GetTestPath("res/values/values1.xml"), + R"(<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <bool name="bool1" android:featureFlag="test.package.falseFlag">false</bool> + </resources>)", + compiled_files_dir, &diag, + {"--feature-flags", "test.package.falseFlag:ro=false,test.package.trueFlag:ro=true"})); + ASSERT_TRUE(CompileFile( + GetTestPath("res/values/values2.xml"), + R"(<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <bool name="bool1" android:featureFlag="test.package.falseFlag">true</bool> + </resources>)", + compiled_files_dir, &diag, + {"--feature-flags", "test.package.falseFlag:ro=false,test.package.trueFlag:ro=true"})); + const std::string out_apk = GetTestPath("out.apk"); + std::vector<std::string> link_args = { + "--manifest", + GetDefaultManifest(), + "-o", + out_apk, + }; + + ASSERT_FALSE(Link(link_args, compiled_files_dir, &diag)); + ASSERT_TRUE(diag.GetLog().contains("duplicate value for resource 'bool1'")); +} + } // namespace aapt diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp index 37a039e9528f..1bef5f8b17f6 100644 --- a/tools/aapt2/link/TableMerger.cpp +++ b/tools/aapt2/link/TableMerger.cpp @@ -321,6 +321,30 @@ bool TableMerger::DoMerge(const android::Source& src, ResourceTablePackage* src_ } } } + + // disabled values + for (auto& src_config_value : src_entry->flag_disabled_values) { + auto dst_config_value = dst_entry->FindOrCreateFlagDisabledValue( + src_config_value->value->GetFlag().value(), src_config_value->config, + src_config_value->product); + if (!dst_config_value->value) { + // Resource does not exist, add it now. + // Must clone the value since it might be in the values vector as well + CloningValueTransformer cloner(&main_table_->string_pool); + dst_config_value->value = src_config_value->value->Transform(cloner); + } else { + error = true; + context_->GetDiagnostics()->Error( + android::DiagMessage(src_config_value->value->GetSource()) + << "duplicate value for resource '" << src_entry->name << "' " << "with config '" + << src_config_value->config << "' and flag '" + << (src_config_value->value->GetFlag()->negated ? "!" : "") + << src_config_value->value->GetFlag()->name << "'"); + context_->GetDiagnostics()->Note( + android::DiagMessage(dst_config_value->value->GetSource()) + << "resource previously defined here"); + } + } } } return !error; @@ -353,6 +377,8 @@ bool TableMerger::MergeFile(const ResourceFile& file_desc, bool overlay, io::IFi file_ref->SetSource(file_desc.source); file_ref->type = file_desc.type; file_ref->file = file; + file_ref->SetFlagStatus(file_desc.flag_status); + file_ref->SetFlag(file_desc.flag); ResourceTablePackage* pkg = table.FindOrCreatePackage(file_desc.name.package); pkg->FindOrCreateType(file_desc.name.type) diff --git a/tools/aapt2/test/Common.cpp b/tools/aapt2/test/Common.cpp index cdf245341844..c7dd4c90e67f 100644 --- a/tools/aapt2/test/Common.cpp +++ b/tools/aapt2/test/Common.cpp @@ -21,23 +21,6 @@ using android::ConfigDescription; namespace aapt { namespace test { -struct TestDiagnosticsImpl : public android::IDiagnostics { - void Log(Level level, android::DiagMessageActual& actual_msg) override { - switch (level) { - case Level::Note: - return; - - case Level::Warn: - std::cerr << actual_msg.source << ": warn: " << actual_msg.message << "." << std::endl; - break; - - case Level::Error: - std::cerr << actual_msg.source << ": error: " << actual_msg.message << "." << std::endl; - break; - } - } -}; - android::IDiagnostics* GetDiagnostics() { static TestDiagnosticsImpl diag; return &diag; diff --git a/tools/aapt2/test/Common.h b/tools/aapt2/test/Common.h index 04379804d8ee..b06c4329488e 100644 --- a/tools/aapt2/test/Common.h +++ b/tools/aapt2/test/Common.h @@ -37,6 +37,32 @@ namespace aapt { namespace test { +struct TestDiagnosticsImpl : public android::IDiagnostics { + void Log(Level level, android::DiagMessageActual& actual_msg) override { + switch (level) { + case Level::Note: + return; + + case Level::Warn: + std::cerr << actual_msg.source << ": warn: " << actual_msg.message << "." << std::endl; + log << actual_msg.source << ": warn: " << actual_msg.message << "." << std::endl; + break; + + case Level::Error: + std::cerr << actual_msg.source << ": error: " << actual_msg.message << "." << std::endl; + log << actual_msg.source << ": error: " << actual_msg.message << "." << std::endl; + break; + } + } + + std::string GetLog() { + return log.str(); + } + + private: + std::ostringstream log; +}; + android::IDiagnostics* GetDiagnostics(); inline ResourceName ParseNameOrDie(android::StringPiece str) { diff --git a/tools/aapt2/test/Fixture.cpp b/tools/aapt2/test/Fixture.cpp index b91abe572306..570bcf16c92c 100644 --- a/tools/aapt2/test/Fixture.cpp +++ b/tools/aapt2/test/Fixture.cpp @@ -91,10 +91,13 @@ void TestDirectoryFixture::WriteFile(const std::string& path, const std::string& } bool CommandTestFixture::CompileFile(const std::string& path, const std::string& contents, - android::StringPiece out_dir, android::IDiagnostics* diag) { + android::StringPiece out_dir, android::IDiagnostics* diag, + const std::vector<android::StringPiece>& additional_args) { WriteFile(path, contents); CHECK(file::mkdirs(out_dir.data())); - return CompileCommand(diag).Execute({path, "-o", out_dir, "-v"}, &std::cerr) == 0; + std::vector<android::StringPiece> args = {path, "-o", out_dir, "-v"}; + args.insert(args.end(), additional_args.begin(), additional_args.end()); + return CompileCommand(diag).Execute(args, &std::cerr) == 0; } bool CommandTestFixture::Link(const std::vector<std::string>& args, android::IDiagnostics* diag) { diff --git a/tools/aapt2/test/Fixture.h b/tools/aapt2/test/Fixture.h index 14298d1678f0..178d01156f32 100644 --- a/tools/aapt2/test/Fixture.h +++ b/tools/aapt2/test/Fixture.h @@ -73,7 +73,8 @@ class CommandTestFixture : public TestDirectoryFixture { // Wries the contents of the file to the specified path. The file is compiled and the flattened // file is written to the out directory. bool CompileFile(const std::string& path, const std::string& contents, - android::StringPiece flat_out_dir, android::IDiagnostics* diag); + android::StringPiece flat_out_dir, android::IDiagnostics* diag, + const std::vector<android::StringPiece>& additional_args = {}); // Executes the link command with the specified arguments. bool Link(const std::vector<std::string>& args, android::IDiagnostics* diag); |