diff options
821 files changed, 23595 insertions, 8194 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 4e05cbc23dc3..2dd16de3e188 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -73,6 +73,7 @@ aconfig_declarations_group { "android.service.dreams.flags-aconfig-java", "android.service.notification.flags-aconfig-java", "android.service.quickaccesswallet.flags-aconfig-java", + "android.service.selinux.flags-aconfig-java", "android.service.voice.flags-aconfig-java", "android.speech.flags-aconfig-java", "android.systemserver.flags-aconfig-java", @@ -1943,3 +1944,19 @@ java_aconfig_library { aconfig_declarations: "android.service.quickaccesswallet.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], } + +// SELinux log collector +aconfig_declarations { + name: "android.service.selinux.flags-aconfig", + package: "com.android.server.selinux.flags", + container: "system", + srcs: [ + "services/core/java/com/android/server/selinux/*.aconfig", + ], +} + +java_aconfig_library { + name: "android.service.selinux.flags-aconfig-java", + aconfig_declarations: "android.service.selinux.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} diff --git a/Android.bp b/Android.bp index 444725eb2c79..127556f8e075 100644 --- a/Android.bp +++ b/Android.bp @@ -415,6 +415,7 @@ java_defaults { "mimemap", "av-types-aidl-java", "tv_tuner_resource_manager_aidl_interface-java", + "media_quality_aidl_interface-java", "soundtrigger_middleware-aidl-java", "modules-utils-binary-xml", "modules-utils-build", diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index cc2d104813e4..d48af2cccd59 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -810,7 +810,7 @@ public class JobInfo implements Parcelable { /** * <p class="caution"><strong>Note:</strong> Beginning with - * {@link android.os.Build.VERSION_CODES#B}, this flag will be ignored and no longer + * {@link android.os.Build.VERSION_CODES#BAKLAVA}, this flag will be ignored and no longer * function effectively, regardless of the calling app's target SDK version. * Calling this method will always return {@code false}. * @@ -2137,9 +2137,9 @@ public class JobInfo implements Parcelable { * Jobs marked as important-while-foreground are given {@link #PRIORITY_HIGH} by default. * * <p class="caution"><strong>Note:</strong> Beginning with - * {@link android.os.Build.VERSION_CODES#B}, this flag will be ignored and no longer + * {@link android.os.Build.VERSION_CODES#BAKLAVA}, this flag will be ignored and no longer * function effectively, regardless of the calling app's target SDK version. - * {link #isImportantWhileForeground()} will always return {@code false}. + * {@link #isImportantWhileForeground()} will always return {@code false}. * Apps should use {link #setExpedited(boolean)} with {@code true} to indicate * that this job is important and needs to run as soon as possible. * diff --git a/boot/boot-image-profile-extra.txt b/boot/boot-image-profile-extra.txt index ce99bfed1ce3..cc02c8ae36e6 100644 --- a/boot/boot-image-profile-extra.txt +++ b/boot/boot-image-profile-extra.txt @@ -70,3 +70,10 @@ HSPLandroid/os/PerfettoTrackEventExtra$FieldString;->* HSPLandroid/os/PerfettoTrackEventExtra$FieldNested;->* HSPLandroid/os/PerfettoTrackEventExtra$Pool;->* HSPLandroid/os/PerfettoTrackEventExtra$RingBuffer;->* + +# While the SystemFeaturesMetadata static cache isn't heavyweight, ensure it's +# pre-initialized in the boot image to avoid redundant per-process overhead. +# TODO(b/326623529): Consider removing this after the feature has fully ramped +# and is captured with the boot image profiling pipeline. +HSPLcom/android/internal/pm/SystemFeaturesMetadata;->* +Lcom/android/internal/pm/SystemFeaturesMetadata; diff --git a/boot/preloaded-classes b/boot/preloaded-classes index f87828ec8d9a..7f4b3244c164 100644 --- a/boot/preloaded-classes +++ b/boot/preloaded-classes @@ -11885,6 +11885,7 @@ com.android.internal.os.ZygoteServer$UsapPoolRefillAction com.android.internal.os.ZygoteServer com.android.internal.os.logging.MetricsLoggerWrapper com.android.internal.pm.RoSystemFeatures +com.android.internal.pm.SystemFeaturesMetadata com.android.internal.pm.parsing.PackageParser2$Callback com.android.internal.pm.parsing.PackageParserException com.android.internal.pm.pkg.component.flags.FeatureFlags diff --git a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java index 696bc82a9ffc..ae150aece432 100644 --- a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java +++ b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java @@ -1309,6 +1309,8 @@ public class Bmgr { return "AGENT_FAILURE_DURING_RESTORE"; case BackupManagerMonitor.LOG_EVENT_ID_FAILED_TO_READ_DATA_FROM_TRANSPORT: return "FAILED_TO_READ_DATA_FROM_TRANSPORT"; + case BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN: + return "LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN"; default: return "UNKNOWN_ID"; } diff --git a/cmds/svc/src/com/android/commands/svc/UsbCommand.java b/cmds/svc/src/com/android/commands/svc/UsbCommand.java index 26e20f601c7a..6542d08d6384 100644 --- a/cmds/svc/src/com/android/commands/svc/UsbCommand.java +++ b/cmds/svc/src/com/android/commands/svc/UsbCommand.java @@ -89,6 +89,11 @@ public class UsbCommand extends Svc.Command { IUsbManager usbMgr = IUsbManager.Stub.asInterface(ServiceManager.getService( Context.USB_SERVICE)); + if (usbMgr == null) { + System.err.println("Could not obtain USB service. Try again later."); + return; + } + Executor executor = context.getMainExecutor(); Consumer<Integer> consumer = new Consumer<Integer>(){ public void accept(Integer status){ diff --git a/config/preloaded-classes b/config/preloaded-classes index 4147fd7c4ae6..707acb00b102 100644 --- a/config/preloaded-classes +++ b/config/preloaded-classes @@ -11921,6 +11921,7 @@ com.android.internal.os.ZygoteServer$UsapPoolRefillAction com.android.internal.os.ZygoteServer com.android.internal.os.logging.MetricsLoggerWrapper com.android.internal.pm.RoSystemFeatures +com.android.internal.pm.SystemFeaturesMetadata com.android.internal.pm.parsing.PackageParser2$Callback com.android.internal.pm.parsing.PackageParserException com.android.internal.pm.pkg.component.flags.FeatureFlags diff --git a/core/api/current.txt b/core/api/current.txt index 7bc0fb220e1a..1630d80346ce 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -58287,7 +58287,9 @@ package android.view.inspector { } public final class WindowInspector { + method @FlaggedApi("android.view.flags.root_view_changed_listener") public static void addGlobalWindowViewsListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.view.View>>); method @NonNull public static java.util.List<android.view.View> getGlobalWindowViews(); + method @FlaggedApi("android.view.flags.root_view_changed_listener") public static void removeGlobalWindowViewsListener(@NonNull java.util.function.Consumer<java.util.List<android.view.View>>); } } diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 526a213a6003..132c65cc26ee 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -410,6 +410,7 @@ package android.os { method public void invalidateCache(); method public static void invalidateCache(@NonNull String, @NonNull String); method @Nullable public Result query(@NonNull Query); + method @FlaggedApi("android.os.ipc_data_cache_test_apis") public static void setTestMode(boolean); field public static final String MODULE_BLUETOOTH = "bluetooth"; } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 984bc680c685..32b170a6286b 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -11725,6 +11725,7 @@ package android.os { field public static final String USER_TYPE_FULL_GUEST = "android.os.usertype.full.GUEST"; field public static final String USER_TYPE_FULL_SECONDARY = "android.os.usertype.full.SECONDARY"; field public static final String USER_TYPE_FULL_SYSTEM = "android.os.usertype.full.SYSTEM"; + field @FlaggedApi("android.multiuser.allow_supervising_profile") public static final String USER_TYPE_PROFILE_SUPERVISING = "android.os.usertype.profile.SUPERVISING"; field public static final String USER_TYPE_SYSTEM_HEADLESS = "android.os.usertype.system.HEADLESS"; } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index d651010b641a..bb023f25094e 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1199,6 +1199,7 @@ package android.content.pm { method public boolean isProfile(); method public boolean isQuietModeEnabled(); method public boolean isRestricted(); + method @FlaggedApi("android.multiuser.allow_supervising_profile") public boolean isSupervisingProfile(); method public boolean supportsSwitchTo(); method @Deprecated public boolean supportsSwitchToByUser(); method public void writeToParcel(android.os.Parcel, int); @@ -2459,7 +2460,7 @@ package android.os { method public static void invalidateCache(@NonNull String, @NonNull String); method public final boolean isDisabled(); method @Nullable public Result query(@NonNull Query); - method public static void setTestMode(boolean); + method @FlaggedApi("android.os.ipc_data_cache_test_apis") public static void setTestMode(boolean); field public static final String MODULE_BLUETOOTH = "bluetooth"; field public static final String MODULE_SYSTEM = "system_server"; field public static final String MODULE_TEST = "test"; diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java index d84a4c12a2cd..d5b2f980e1a6 100644 --- a/core/java/android/animation/AnimationHandler.java +++ b/core/java/android/animation/AnimationHandler.java @@ -384,6 +384,12 @@ public class AnimationHandler { }); } + void removePendingEndAnimationCallback(Runnable notifyEndAnimation) { + if (mPendingEndAnimationListeners != null) { + mPendingEndAnimationListeners.remove(notifyEndAnimation); + } + } + private void doAnimationFrame(long frameTime) { long currentTime = SystemClock.uptimeMillis(); final int size = mAnimationCallbacks.size(); diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java index 4bf87f91cb2f..e62cd556af9a 100644 --- a/core/java/android/animation/Animator.java +++ b/core/java/android/animation/Animator.java @@ -82,6 +82,12 @@ public abstract class Animator implements Cloneable { static boolean sPostNotifyEndListenerEnabled; /** + * If {@link #sPostNotifyEndListenerEnabled} is enabled, it will be set when the end callback + * is scheduled. It is cleared when it runs or finishes immediately, e.g. cancel. + */ + private Runnable mPendingEndCallback; + + /** * A cache of the values in a list. Used so that when calling the list, we have a copy * of it in case the list is modified while iterating. The array can be reused to avoid * allocation on every notification. @@ -660,10 +666,33 @@ public abstract class Animator implements Cloneable { } } + /** + * This is called when the animator needs to finish immediately. This is usually no-op unless + * {@link #sPostNotifyEndListenerEnabled} is enabled and a finish request calls around the last + * animation frame. + * + * @param notifyListeners Whether to invoke {@link AnimatorListener#onAnimationEnd}. + * @return {@code true} if the pending listeners are removed. + */ + boolean consumePendingEndListeners(boolean notifyListeners) { + if (mPendingEndCallback == null) { + return false; + } + AnimationHandler.getInstance().removePendingEndAnimationCallback(mPendingEndCallback); + mPendingEndCallback = null; + if (notifyListeners) { + notifyEndListeners(false /* isReversing */); + } + return true; + } + void notifyEndListenersFromEndAnimation(boolean isReversing, boolean postNotifyEndListener) { if (postNotifyEndListener) { - AnimationHandler.getInstance().postEndAnimationCallback( - () -> completeEndAnimation(isReversing, "postNotifyAnimEnd")); + mPendingEndCallback = () -> { + completeEndAnimation(isReversing, "postNotifyAnimEnd"); + mPendingEndCallback = null; + }; + AnimationHandler.getInstance().postEndAnimationCallback(mPendingEndCallback); } else { completeEndAnimation(isReversing, "notifyAnimEnd"); } diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java index 78566d2fe98d..4a07de0410ae 100644 --- a/core/java/android/animation/AnimatorSet.java +++ b/core/java/android/animation/AnimatorSet.java @@ -423,6 +423,13 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim notifyListeners(AnimatorCaller.ON_CANCEL, false); callOnPlayingSet(Animator::cancel); mPlayingSet.clear(); + // If the end callback is pending, invoke the end callbacks of the animator nodes before + // ending this set. Pass notifyListeners=false because this endAnimation will do that. + if (consumePendingEndListeners(false /* notifyListeners */)) { + for (int i = mNodeMap.size() - 1; i >= 0; i--) { + mNodeMap.keyAt(i).consumePendingEndListeners(true /* notifyListeners */); + } + } endAnimation(); } } diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index 492c2ffc561f..fbcc73ea59e7 100644 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -1182,6 +1182,7 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio // If end has already been requested, through a previous end() or cancel() call, no-op // until animation starts again. if (mAnimationEndRequested) { + consumePendingEndListeners(true /* notifyListeners */); return; } diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index b38f5da6b638..54ab3b8f185b 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -55,9 +55,7 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Point; -import android.graphics.Rect; import android.graphics.drawable.Icon; -import android.hardware.HardwareBuffer; import android.os.BatteryStats; import android.os.Binder; import android.os.Build; @@ -86,7 +84,6 @@ import android.util.Log; import android.util.Singleton; import android.util.Size; import android.view.WindowInsetsController.Appearance; -import android.window.TaskSnapshot; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.LocalePicker; @@ -5420,10 +5417,11 @@ public class ActivityManager { * * @hide */ + @Nullable @RequiresPermission(Manifest.permission.MANAGE_USERS) - public @Nullable String getSwitchingFromUserMessage() { + public String getSwitchingFromUserMessage(@UserIdInt int userId) { try { - return getService().getSwitchingFromUserMessage(); + return getService().getSwitchingFromUserMessage(userId); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } @@ -5434,10 +5432,11 @@ public class ActivityManager { * * @hide */ + @Nullable @RequiresPermission(Manifest.permission.MANAGE_USERS) - public @Nullable String getSwitchingToUserMessage() { + public String getSwitchingToUserMessage(@UserIdInt int userId) { try { - return getService().getSwitchingToUserMessage(); + return getService().getSwitchingToUserMessage(userId); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index a12c0674998e..e5f7889859c1 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -292,14 +292,14 @@ public abstract class ActivityManagerInternal { public abstract boolean canStartMoreUsers(); /** - * Sets the user switcher message for switching from {@link android.os.UserHandle#SYSTEM}. + * Sets the user switcher message for switching from a user. */ - public abstract void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage); + public abstract void setSwitchingFromUserMessage(@UserIdInt int user, @Nullable String message); /** - * Sets the user switcher message for switching to {@link android.os.UserHandle#SYSTEM}. + * Sets the user switcher message for switching to a user. */ - public abstract void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage); + public abstract void setSwitchingToUserMessage(@UserIdInt int user, @Nullable String message); /** * Returns maximum number of users that can run simultaneously. diff --git a/core/java/android/app/AppCompatTaskInfo.java b/core/java/android/app/AppCompatTaskInfo.java index 599f1a8be233..ea4646aa9eb9 100644 --- a/core/java/android/app/AppCompatTaskInfo.java +++ b/core/java/android/app/AppCompatTaskInfo.java @@ -102,6 +102,8 @@ public class AppCompatTaskInfo implements Parcelable { private static final int FLAG_FULLSCREEN_OVERRIDE_USER = FLAG_BASE << 8; /** Top activity flag for whether min aspect ratio of the activity has been overridden.*/ public static final int FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE = FLAG_BASE << 9; + /** Top activity flag for whether restart menu is shown due to display move. */ + private static final int FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE = FLAG_BASE << 10; @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = { @@ -115,7 +117,8 @@ public class AppCompatTaskInfo implements Parcelable { FLAG_ELIGIBLE_FOR_USER_ASPECT_RATIO_BUTTON, FLAG_FULLSCREEN_OVERRIDE_SYSTEM, FLAG_FULLSCREEN_OVERRIDE_USER, - FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE + FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE, + FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE }) public @interface TopActivityFlag {} @@ -133,7 +136,8 @@ public class AppCompatTaskInfo implements Parcelable { @TopActivityFlag private static final int FLAGS_COMPAT_UI_INTERESTED = FLAGS_ORGANIZER_INTERESTED - | FLAG_IN_SIZE_COMPAT | FLAG_ELIGIBLE_FOR_LETTERBOX_EDU | FLAG_LETTERBOX_EDU_ENABLED; + | FLAG_IN_SIZE_COMPAT | FLAG_ELIGIBLE_FOR_LETTERBOX_EDU | FLAG_LETTERBOX_EDU_ENABLED + | FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE; private AppCompatTaskInfo() { // Do nothing @@ -300,6 +304,21 @@ public class AppCompatTaskInfo implements Parcelable { } /** + * @return {@code true} if the restart menu is enabled for the top activity due to display move. + */ + public boolean isRestartMenuEnabledForDisplayMove() { + return isTopActivityFlagEnabled(FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE); + } + + /** + * Sets the top activity flag for whether the restart menu is enabled for the top activity due + * to display move. + */ + public void setRestartMenuEnabledForDisplayMove(boolean enable) { + setTopActivityFlag(FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE, enable); + } + + /** * @return {@code true} if the top activity bounds are letterboxed. */ public boolean isTopActivityLetterboxed() { diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 248f191cb8b8..1864d4a55f2e 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -18,7 +18,6 @@ package android.app; import static android.location.flags.Flags.FLAG_LOCATION_BYPASS; -import static android.media.audio.Flags.roForegroundAudioControl; import static android.permission.flags.Flags.FLAG_OP_ENABLE_MOBILE_DATA_BY_USER; import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS; import static android.view.contentprotection.flags.Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED; @@ -3481,6 +3480,16 @@ public class AppOpsManager { } /** + * Whether an app op is backed by a runtime permission or not. + * @hide + */ + public static boolean opIsRuntimePermission(int op) { + if (op == OP_NONE) return false; + + return ArrayUtils.contains(RUNTIME_PERMISSION_OPS, op); + } + + /** * Retrieve the user restriction associated with an operation, or null if there is not one. * @hide */ diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index ad01ad57b2d8..6cdfb97520ae 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -403,8 +403,8 @@ interface IActivityManager { void setPackageScreenCompatMode(in String packageName, int mode); @UnsupportedAppUsage boolean switchUser(int userid); - String getSwitchingFromUserMessage(); - String getSwitchingToUserMessage(); + String getSwitchingFromUserMessage(int userId); + String getSwitchingToUserMessage(int userId); @UnsupportedAppUsage void setStopUserOnSwitch(int value); boolean removeTask(int taskId); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 8d4925d8182d..127a08b04e87 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -6143,6 +6143,20 @@ public class Notification implements Parcelable result.mTitleMarginSet.applyToView(contentView, p.mTextViewId); contentView.setInt(p.mTextViewId, "setNumIndentLines", p.hasTitle() ? 0 : 1); } + // The expand button uses paddings rather than margins, so we'll adjust it + // separately. + adjustExpandButtonPadding(contentView, result.mRightIconVisible); + } + + private void adjustExpandButtonPadding(RemoteViews contentView, boolean rightIconVisible) { + if (notificationsRedesignTemplates()) { + final Resources res = mContext.getResources(); + int normalPadding = res.getDimensionPixelSize(R.dimen.notification_2025_margin); + int iconSpacing = res.getDimensionPixelSize( + R.dimen.notification_2025_expand_button_right_icon_spacing); + contentView.setInt(R.id.expand_button, "setStartPadding", + rightIconVisible ? iconSpacing : normalPadding); + } } // This code is executed on behalf of other apps' notifications, sometimes even by 3p apps, @@ -6154,12 +6168,21 @@ public class Notification implements Parcelable @NonNull TemplateBindResult result) { final Resources resources = mContext.getResources(); final float density = resources.getDisplayMetrics().density; - final float iconMarginDp = resources.getDimension( - R.dimen.notification_right_icon_content_margin) / density; + int iconMarginId = notificationsRedesignTemplates() + ? R.dimen.notification_2025_right_icon_content_margin + : R.dimen.notification_right_icon_content_margin; + final float iconMarginDp = resources.getDimension(iconMarginId) / density; final float contentMarginDp = resources.getDimension( R.dimen.notification_content_margin_end) / density; - final float expanderSizeDp = resources.getDimension( - R.dimen.notification_header_expand_icon_size) / density - contentMarginDp; + float spaceForExpanderDp; + if (notificationsRedesignTemplates()) { + spaceForExpanderDp = resources.getDimension( + R.dimen.notification_2025_right_icon_expanded_margin_end) / density + - contentMarginDp; + } else { + spaceForExpanderDp = resources.getDimension( + R.dimen.notification_header_expand_icon_size) / density - contentMarginDp; + } final float viewHeightDp = resources.getDimension( R.dimen.notification_right_icon_size) / density; float viewWidthDp = viewHeightDp; // icons are 1:1 by default @@ -6176,9 +6199,10 @@ public class Notification implements Parcelable } } } + // Margin needed for the header to accommodate the icon when shown final float extraMarginEndDpIfVisible = viewWidthDp + iconMarginDp; result.setRightIconState(rightIcon != null /* visible */, viewWidthDp, - viewHeightDp, extraMarginEndDpIfVisible, expanderSizeDp); + viewHeightDp, extraMarginEndDpIfVisible, spaceForExpanderDp); } /** @@ -14658,13 +14682,19 @@ public class Notification implements Parcelable public final MarginSet mTitleMarginSet = new MarginSet(); public void setRightIconState(boolean visible, float widthDp, float heightDp, - float marginEndDpIfVisible, float expanderSizeDp) { + float marginEndDpIfVisible, float spaceForExpanderDp) { mRightIconVisible = visible; mRightIconWidthDp = widthDp; mRightIconHeightDp = heightDp; - mHeadingExtraMarginSet.setValues(0, marginEndDpIfVisible); - mHeadingFullMarginSet.setValues(expanderSizeDp, marginEndDpIfVisible + expanderSizeDp); - mTitleMarginSet.setValues(0, marginEndDpIfVisible + expanderSizeDp); + mHeadingExtraMarginSet.setValues( + /* valueIfGone = */ 0, + /* valueIfVisible = */ marginEndDpIfVisible); + mHeadingFullMarginSet.setValues( + /* valueIfGone = */ spaceForExpanderDp, + /* valueIfVisible = */ marginEndDpIfVisible + spaceForExpanderDp); + mTitleMarginSet.setValues( + /* valueIfGone = */ 0, + /* valueIfVisible = */ marginEndDpIfVisible + spaceForExpanderDp); } /** diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index 38141cf20ce3..6e495768bfd4 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -1417,7 +1417,36 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * Enable or disable testing. The protocol requires that the mode toggle: for instance, it is + * Throw if the current process is not allowed to use test APIs. + */ + @android.ravenwood.annotation.RavenwoodReplace + private static void throwIfNotTest() { + final ActivityThread activityThread = ActivityThread.currentActivityThread(); + if (activityThread == null) { + // Only tests can reach here. + return; + } + final Instrumentation instrumentation = activityThread.getInstrumentation(); + if (instrumentation == null) { + // Only tests can reach here. + return; + } + if (instrumentation.isInstrumenting()) { + return; + } + if (Flags.enforcePicTestmodeProtocol()) { + throw new IllegalStateException("Test-only API called not from a test."); + } + } + + /** + * Do not throw if running under ravenwood. + */ + private static void throwIfNotTest$ravenwood() { + } + + /** + * Enable or disable test mode. The protocol requires that the mode toggle: for instance, it is * illegal to clear the test mode if the test mode is already off. Enabling test mode puts * all caches in the process into test mode; all nonces are initialized to UNSET and * subsequent reads and writes are to process memory. This has the effect of disabling all @@ -1425,10 +1454,12 @@ public class PropertyInvalidatedCache<Query, Result> { * operation. * @param mode The desired test mode. * @throws IllegalStateException if the supplied mode is already set. + * @throws IllegalStateException if the process is not running an instrumentation test. * @hide */ @VisibleForTesting public static void setTestMode(boolean mode) { + throwIfNotTest(); synchronized (sGlobalLock) { if (sTestMode == mode) { final String msg = "cannot set test mode redundantly: mode=" + mode; @@ -1464,9 +1495,11 @@ public class PropertyInvalidatedCache<Query, Result> { * for which it would not otherwise have permission. Caches in test mode do NOT write their * values to the system properties. The effect is local to the current process. Test mode * must be true when this method is called. + * @throws IllegalStateException if the process is not running an instrumentation test. * @hide */ public void testPropertyName() { + throwIfNotTest(); synchronized (sGlobalLock) { if (sTestMode == false) { throw new IllegalStateException("cannot test property name with test mode off"); @@ -1777,10 +1810,12 @@ public class PropertyInvalidatedCache<Query, Result> { * When multiple caches share a single property value, using an instance method on one of * the cache objects to invalidate all of the cache objects becomes confusing and you should * just use the static version of this function. + * @throws IllegalStateException if the process is not running an instrumentation test. * @hide */ @VisibleForTesting public void disableSystemWide() { + throwIfNotTest(); disableSystemWide(mPropertyName); } diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java index 6936ddc5eeac..59247934ba46 100644 --- a/core/java/android/app/TaskInfo.java +++ b/core/java/android/app/TaskInfo.java @@ -655,8 +655,10 @@ public class TaskInfo { + " effectiveUid=" + effectiveUid + " displayId=" + displayId + " isRunning=" + isRunning - + " baseIntent=" + baseIntent + " baseActivity=" + baseActivity - + " topActivity=" + topActivity + " origActivity=" + origActivity + + " baseIntent=" + baseIntent + + " baseActivity=" + baseActivity + + " topActivity=" + topActivity + + " origActivity=" + origActivity + " realActivity=" + realActivity + " numActivities=" + numActivities + " lastActiveTime=" + lastActiveTime diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 5359ba44a3d2..73de1b67dc66 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -8269,6 +8269,7 @@ public class DevicePolicyManager { * * @throws SecurityException if the caller is not a device owner, a profile owner or * delegated certificate chooser. + * @throws IllegalArgumentException if {@code alias} does not correspond to an existing key * @see #grantKeyPairToWifiAuth */ public boolean isKeyPairGrantedToWifiAuth(@NonNull String alias) { diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 0ecd2754b1f0..572bffe6c6a4 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -402,3 +402,13 @@ flag { description: "Add new API for secondary lockscreen" bug: "336297680" } + +flag { + name: "remove_managed_esim_on_work_profile_deletion" + namespace: "enterprise" + description: "Remove managed eSIM when work profile is deleted" + bug: "347925470" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/app/backup/BackupManagerMonitor.java b/core/java/android/app/backup/BackupManagerMonitor.java index e741bc2bf608..19c24cd8a14a 100644 --- a/core/java/android/app/backup/BackupManagerMonitor.java +++ b/core/java/android/app/backup/BackupManagerMonitor.java @@ -297,6 +297,9 @@ public class BackupManagerMonitor { @hide */ public static final int LOG_EVENT_ID_FAILED_TO_READ_DATA_FROM_TRANSPORT = 81; + /** The pipe between the BackupAgent and the framework was broken during full backup. @hide */ + public static final int LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN = 82; + /** * This method will be called each time something important happens on BackupManager. * diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index 5c267c9f6475..4afe75f7814c 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -265,6 +265,16 @@ flag { } flag { + name: "expanding_public_view" + namespace: "systemui" + description: "enables user expanding the public view of a notification" + bug: "398853084" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "api_rich_ongoing" is_exported: true namespace: "systemui" diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 55d78f9b8c48..cc288b1f5601 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -744,15 +744,22 @@ public abstract class Context { */ public static final long BIND_MATCH_QUARANTINED_COMPONENTS = 0x2_0000_0000L; + /** + * Flag for {@link #bindService} that allows the bound app to be frozen if it is eligible. + * + * @hide + */ + public static final long BIND_ALLOW_FREEZE = 0x4_0000_0000L; /** * These bind flags reduce the strength of the binding such that we shouldn't * consider it as pulling the process up to the level of the one that is bound to it. * @hide */ - public static final int BIND_REDUCTION_FLAGS = + public static final long BIND_REDUCTION_FLAGS = Context.BIND_ALLOW_OOM_MANAGEMENT | Context.BIND_WAIVE_PRIORITY - | Context.BIND_NOT_PERCEPTIBLE | Context.BIND_NOT_VISIBLE; + | Context.BIND_NOT_PERCEPTIBLE | Context.BIND_NOT_VISIBLE + | Context.BIND_ALLOW_FREEZE; /** @hide */ @IntDef(flag = true, prefix = { "RECEIVER_VISIBLE" }, value = { diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java index 74da62c85ed2..10d3051cff6f 100644 --- a/core/java/android/content/pm/RegisteredServicesCache.java +++ b/core/java/android/content/pm/RegisteredServicesCache.java @@ -166,7 +166,15 @@ public abstract class RegisteredServicesCache<V> { @UnsupportedAppUsage public RegisteredServicesCache(Context context, String interfaceName, String metaDataName, String attributeName, XmlSerializerAndParser<V> serializerAndParser) { - mContext = context; + this(new Injector<V>(context), interfaceName, metaDataName, attributeName, + serializerAndParser); + } + + /** Provides the basic functionality for unit tests. */ + @VisibleForTesting + public RegisteredServicesCache(Injector<V> injector, String interfaceName, String metaDataName, + String attributeName, XmlSerializerAndParser<V> serializerAndParser) { + mContext = injector.getContext(); mInterfaceName = interfaceName; mMetaDataName = metaDataName; mAttributesName = attributeName; @@ -184,7 +192,7 @@ public abstract class RegisteredServicesCache<V> { if (isCore) { intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); } - mBackgroundHandler = BackgroundThread.getHandler(); + mBackgroundHandler = injector.getBackgroundHandler(); mContext.registerReceiverAsUser( mPackageReceiver, UserHandle.ALL, intentFilter, null, mBackgroundHandler); @@ -918,4 +926,25 @@ public abstract class RegisteredServicesCache<V> { return null; } } + + /** + * Point of injection for test dependencies. + * @param <V> The type of the value. + */ + @VisibleForTesting + public static class Injector<V> { + private final Context mContext; + + public Injector(Context context) { + mContext = context; + } + + public Context getContext() { + return mContext; + } + + public Handler getBackgroundHandler() { + return BackgroundThread.getHandler(); + } + } } diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java index 582a1a9442ce..53203eba4020 100644 --- a/core/java/android/content/pm/UserInfo.java +++ b/core/java/android/content/pm/UserInfo.java @@ -410,6 +410,11 @@ public class UserInfo implements Parcelable { return UserManager.isUserTypePrivateProfile(userType); } + @FlaggedApi(android.multiuser.Flags.FLAG_ALLOW_SUPERVISING_PROFILE) + public boolean isSupervisingProfile() { + return UserManager.isUserTypeSupervisingProfile(userType); + } + /** See {@link #FLAG_DISABLED}*/ @UnsupportedAppUsage public boolean isEnabled() { diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 5c904c15e706..7f57f5dbf0ab 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -617,8 +617,8 @@ flag { } flag { - namespace: "multi_user" name: "logout_user_api" + namespace: "multiuser" description: "Add API to logout user" bug: "350045389" } @@ -629,3 +629,10 @@ flag { description: "Enable moving content into the Private Space" bug: "360066001" } + +flag { + name: "allow_supervising_profile" + namespace: "supervision" + description: "Enables support for new supervising user type" + bug: "389712089" +} diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java index e43a5fce6cb7..040dcfdeb998 100644 --- a/core/java/android/database/sqlite/SQLiteConnection.java +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -183,11 +183,6 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen private static native long nativeChanges(long connectionPtr); private static native long nativeTotalChanges(long connectionPtr); - // This method is deprecated and should be removed when it is no longer needed by the - // robolectric tests. It should not be called from any frameworks java code. - @Deprecated - private static native void nativeClose(long connectionPtr); - private SQLiteConnection(SQLiteConnectionPool pool, SQLiteDatabaseConfiguration configuration, int connectionId, boolean primaryConnection) { diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index bcb7ebfb286f..6e9dcf5a83a1 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -1714,7 +1714,7 @@ public final class CameraManager { final TaskInfo taskInfo = appTask.getTaskInfo(); final int freeformCameraCompatMode = taskInfo.appCompatTaskInfo .cameraCompatTaskInfo.freeformCameraCompatMode; - if (freeformCameraCompatMode != 0 + if (isInCameraCompatMode(freeformCameraCompatMode) && taskInfo.topActivity != null && taskInfo.topActivity.getPackageName().equals(packageName)) { // WindowManager has requested rotation override. @@ -1741,6 +1741,12 @@ public final class CameraManager { : ICameraService.ROTATION_OVERRIDE_NONE; } + private static boolean isInCameraCompatMode(@CameraCompatTaskInfo.FreeformCameraCompatMode int + freeformCameraCompatMode) { + return (freeformCameraCompatMode != CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_UNSPECIFIED) + && (freeformCameraCompatMode != CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE); + } + private static int getRotationOverrideForCompatFreeform( @CameraCompatTaskInfo.FreeformCameraCompatMode int freeformCameraCompatMode) { // Only rotate-and-crop if the app and device orientations do not match. diff --git a/core/java/android/hardware/input/IKeyGestureHandler.aidl b/core/java/android/hardware/input/IKeyGestureHandler.aidl index 509b9482154e..4da991ee85b1 100644 --- a/core/java/android/hardware/input/IKeyGestureHandler.aidl +++ b/core/java/android/hardware/input/IKeyGestureHandler.aidl @@ -28,15 +28,4 @@ interface IKeyGestureHandler { * to that gesture. */ boolean handleKeyGesture(in AidlKeyGestureEvent event, in IBinder focusedToken); - - /** - * Called to know if a particular gesture type is supported by the handler. - * - * TODO(b/358569822): Remove this call to reduce the binder calls to single call for - * handleKeyGesture. For this we need to remove dependency of multi-key gestures to identify if - * a key gesture is supported on first relevant key down. - * Also, for now we prioritize handlers in the system server process above external handlers to - * reduce IPC binder calls. - */ - boolean isKeyGestureSupported(int gestureType); } diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 49db54d81e65..d6419afb2a5a 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -1758,13 +1758,6 @@ public final class InputManager { */ boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event, @Nullable IBinder focusedToken); - - /** - * Called to identify if a particular gesture is of interest to a handler. - * - * NOTE: If no active handler supports certain gestures, the gestures will not be captured. - */ - boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType); } /** @hide */ diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java index a9a45ae45ec3..c4b4831ba76e 100644 --- a/core/java/android/hardware/input/InputManagerGlobal.java +++ b/core/java/android/hardware/input/InputManagerGlobal.java @@ -1193,23 +1193,6 @@ public final class InputManagerGlobal { } return false; } - - @Override - public boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) { - synchronized (mKeyGestureEventHandlerLock) { - if (mKeyGestureEventHandlers == null) { - return false; - } - final int numHandlers = mKeyGestureEventHandlers.size(); - for (int i = 0; i < numHandlers; i++) { - KeyGestureEventHandler handler = mKeyGestureEventHandlers.get(i); - if (handler.isKeyGestureSupported(gestureType)) { - return true; - } - } - } - return false; - } } /** diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java index 1a712d2b3f31..9dd1fed4a85a 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -108,7 +108,8 @@ public final class KeyGestureEvent { public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD = 55; public static final int KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD = 56; public static final int KEY_GESTURE_TYPE_GLOBAL_ACTIONS = 57; - public static final int KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD = 58; + @Deprecated + public static final int DEPRECATED_KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD = 58; public static final int KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT = 59; public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT = 60; public static final int KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS = 61; @@ -200,7 +201,6 @@ public final class KeyGestureEvent { KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD, KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD, KEY_GESTURE_TYPE_GLOBAL_ACTIONS, - KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD, KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT, KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT, KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS, @@ -777,8 +777,6 @@ public final class KeyGestureEvent { return "KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD"; case KEY_GESTURE_TYPE_GLOBAL_ACTIONS: return "KEY_GESTURE_TYPE_GLOBAL_ACTIONS"; - case KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD: - return "KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD"; case KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT: return "KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT"; case KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT: diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java index 0964cde5a1f4..c3ec96d17437 100644 --- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java +++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java @@ -145,8 +145,17 @@ public final class MessageQueue { } if (Flags.forceConcurrentMessageQueue()) { - sIsProcessAllowedToUseConcurrent = true; - return; + // b/379472827: Robolectric tests use reflection to access MessageQueue.mMessages. + // This is a hack to allow Robolectric tests to use the legacy implementation. + try { + Class.forName("org.robolectric.Robolectric"); + } catch (ClassNotFoundException e) { + // This is not a Robolectric test. + sIsProcessAllowedToUseConcurrent = true; + return; + } + // This is a Robolectric test. + // Continue to the following checks. } final String processName = Process.myProcessName(); diff --git a/core/java/android/os/IpcDataCache.java b/core/java/android/os/IpcDataCache.java index 2e7c3be53d90..e888f520b842 100644 --- a/core/java/android/os/IpcDataCache.java +++ b/core/java/android/os/IpcDataCache.java @@ -718,7 +718,7 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, } /** - * Enable or disable testing. The protocol requires that the mode toggle: for instance, it is + * Enable or disable test mode. The protocol requires that the mode toggle: for instance, it is * illegal to clear the test mode if the test mode is already off. Enabling test mode puts * all caches in the process into test mode; all nonces are initialized to UNSET and * subsequent reads and writes are to process memory. This has the effect of disabling all @@ -726,8 +726,11 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, * operation. * @param mode The desired test mode. * @throws IllegalStateException if the supplied mode is already set. + * @throws IllegalStateException if the process is not running an instrumentation test. * @hide */ + @FlaggedApi(android.os.Flags.FLAG_IPC_DATA_CACHE_TEST_APIS) + @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) @TestApi public static void setTestMode(boolean mode) { PropertyInvalidatedCache.setTestMode(mode); diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 767019d97758..c01c3cdc7ace 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -209,6 +209,23 @@ public class UserManager { public static final String USER_TYPE_PROFILE_COMMUNAL = "android.os.usertype.profile.COMMUNAL"; /** + * User type representing a user who manages supervision on the device. + * When any full user on the device is supervised, the credentials for this profile will be + * required in order to perform certain actions for that user (i.e. those controlled by + * {@link android.app.supervision.SupervisionManager} or the + * {@link android.app.role.RoleManager#ROLE_SYSTEM_SUPERVISION supervision role holder}). + * There can only be one supervising profile per device, and the credentials set for that + * profile will be used to authorize actions for any supervised user on the device. This is + * distinct from a managed profile in that it functions only to authorize certain supervised + * actions; it does not represent the user to which restriction or management is applied. + * @hide + */ + @FlaggedApi(android.multiuser.Flags.FLAG_ALLOW_SUPERVISING_PROFILE) + @SystemApi + public static final String USER_TYPE_PROFILE_SUPERVISING = + "android.os.usertype.profile.SUPERVISING"; + + /** * User type representing a {@link UserHandle#USER_SYSTEM system} user that is <b>not</b> a * human user. * This type of user cannot be created; it can only pre-exist on first boot. @@ -3226,6 +3243,18 @@ public class UserManager { } /** + * Returns whether the user type is a + * {@link UserManager#USER_TYPE_PROFILE_SUPERVISING supervising profile}. + * + * @hide + */ + @FlaggedApi(android.multiuser.Flags.FLAG_ALLOW_SUPERVISING_PROFILE) + @android.ravenwood.annotation.RavenwoodKeep + public static boolean isUserTypeSupervisingProfile(@Nullable String userType) { + return USER_TYPE_PROFILE_SUPERVISING.equals(userType); + } + + /** * @hide * @deprecated Use {@link #isRestrictedProfile()} */ diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index 86acb2b21cfa..5d80119410e1 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -227,6 +227,14 @@ flag { } flag { + name: "ipc_data_cache_test_apis" + namespace: "system_performance" + description: "Expose IpcDataCache test apis to mainline modules." + bug: "396173886" + is_exported: true +} + +flag { name: "mainline_vcn_platform_api" namespace: "vcn" description: "Expose platform APIs to mainline VCN" diff --git a/core/java/android/os/health/SystemHealthManager.java b/core/java/android/os/health/SystemHealthManager.java index a8a22f675e08..b82f278ef7d5 100644 --- a/core/java/android/os/health/SystemHealthManager.java +++ b/core/java/android/os/health/SystemHealthManager.java @@ -216,7 +216,7 @@ public class SystemHealthManager { /** * Gets the maximum number of TIDs this device supports for getting CPU headroom. * <p> - * See {@link CpuHeadroomParams#setTids(int...)}. + * See {@link CpuHeadroomParams.Builder#setTids(int...)}. * * @return the maximum size of TIDs supported * @throws UnsupportedOperationException if the CPU headroom API is unsupported. @@ -288,9 +288,7 @@ public class SystemHealthManager { /** * Gets the range of the calculation window size for CPU headroom. * <p> - * In API version 36, the range will be a superset of [50, 10000]. - * <p> - * See {@link CpuHeadroomParams#setCalculationWindowMillis(int)}. + * See {@link CpuHeadroomParams.Builder#setCalculationWindowMillis(int)}. * * @return the range of the calculation window size supported in milliseconds. * @throws UnsupportedOperationException if the CPU headroom API is unsupported. @@ -310,9 +308,7 @@ public class SystemHealthManager { /** * Gets the range of the calculation window size for GPU headroom. * <p> - * In API version 36, the range will be a superset of [50, 10000]. - * <p> - * See {@link GpuHeadroomParams#setCalculationWindowMillis(int)}. + * See {@link GpuHeadroomParams.Builder#setCalculationWindowMillis(int)}. * * @return the range of the calculation window size supported in milliseconds. * @throws UnsupportedOperationException if the GPU headroom API is unsupported. diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig index 414f27498414..176c0c8ab966 100644 --- a/core/java/android/os/vibrator/flags.aconfig +++ b/core/java/android/os/vibrator/flags.aconfig @@ -165,3 +165,14 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "haptics" + name: "remove_hidl_support" + description: "Remove framework code to support HIDL vibrator HALs." + bug: "308452413" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_FEATURE + } +} diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 0476f62ec263..34272b17cf54 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -71,6 +71,19 @@ flag { } flag { + name: "unknown_call_setting_blocked_logging_enabled" + is_exported: true + is_fixed_read_only: true + namespace: "permissions" + description: "enable the metrics when blocking certain app installs during an unknown call" + bug: "364535720" + + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "op_enable_mobile_data_by_user" is_exported: true namespace: "permissions" @@ -372,6 +385,15 @@ flag { } flag { + name: "record_all_runtime_appops_sqlite" + is_fixed_read_only: true + is_exported: true + namespace: "permissions" + description: "Enables recording of all runtime app ops into SQlite" + bug: "377584611" +} + +flag { name: "ranging_permission_enabled" is_fixed_read_only: true is_exported: true diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index b97c9b5e83f2..da4709b4b8b1 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -8651,6 +8651,34 @@ public final class Settings { public static final String DOCKED_CLOCK_FACE = "docked_clock_face"; /** + * Setting to indicate that content filters should be enabled on web browsers. + * + * <ul> + * <li>0 = Allow all sites + * <li>1 = Try to block explicit sites + * </ul> + * + * @hide + */ + @Readable + public static final String BROWSER_CONTENT_FILTERS_ENABLED = + "browser_content_filters_enabled"; + + /** + * Setting to indicate that content filters should be enabled in web search engines. + * + * <ul> + * <li>0 = Off + * <li>1 = Filter + * </ul> + * + * @hide + */ + @Readable + public static final String SEARCH_CONTENT_FILTERS_ENABLED = + "search_content_filters_enabled"; + + /** * Set by the system to track if the user needs to see the call to action for * the lockscreen notification policy. * @hide @@ -10589,6 +10617,57 @@ public final class Settings { public static final String GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled"; /** + * Indicates that glanceable hub should never be started automatically. + * + * @hide + */ + public static final int GLANCEABLE_HUB_START_NEVER = 0; + + /** + * Indicates that glanceable hub should be started when charging. + * + * @hide + */ + public static final int GLANCEABLE_HUB_START_CHARGING = 1; + + /** + * Indicates that glanceable hub should be started when charging and upright. + * + * @hide + */ + public static final int GLANCEABLE_HUB_START_CHARGING_UPRIGHT = 2; + + /** + * Indicates that glanceable hub should be started when docked. + * + * @hide + */ + public static final int GLANCEABLE_HUB_START_DOCKED = 3; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + GLANCEABLE_HUB_START_NEVER, + GLANCEABLE_HUB_START_CHARGING, + GLANCEABLE_HUB_START_CHARGING_UPRIGHT, + GLANCEABLE_HUB_START_DOCKED, + }) + public @interface WhenToStartGlanceableHub { + } + + /** + * Indicates when to start glanceable hub. Possible values are: + * 0: Never + * 1: While charging always + * 2: While upright and charging + * 3: While docked + * + * @hide + */ + public static final String WHEN_TO_START_GLANCEABLE_HUB = + "when_to_start_glanceable_hub"; + + /** * Whether home controls are enabled to be shown over the screensaver by the user. * * @hide @@ -11132,6 +11211,12 @@ public final class Settings { public static final String DOUBLE_TAP_TO_WAKE = "double_tap_to_wake"; /** + * Controls whether double tap to sleep is enabled. + * @hide + */ + public static final String DOUBLE_TAP_TO_SLEEP = "double_tap_to_sleep"; + + /** * The current assistant component. It could be a voice interaction service, * or an activity that handles ACTION_ASSIST, or empty which means using the default * handling. @@ -12499,6 +12584,48 @@ public final class Settings { "accessibility_magnification_always_on_enabled"; /** + * Controls how the magnification follows the cursor. + * + * @hide + */ + public static final String ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE = + "accessibility_magnification_cursor_following_mode"; + + /** + * Magnification cursor following mode value for the continuous mode. + * + * @hide + */ + public static final int ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS = 0; + + /** + * Magnification cursor following mode value for the center mode. + * + * @hide + */ + public static final int ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CENTER = 1; + + /** + * Magnification cursor following mode value for the edge mode. + * + * @hide + */ + public static final int ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE = 2; + + /** + * Different cursor following settings that can be used as values with + * {@link #ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE}. + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_" }, + value = { + ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS, + ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CENTER, + ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE}) + public @interface AccessibilityMagnificationCursorFollowingMode {} + + /** * Whether the following typing focus feature for magnification is enabled. * @hide */ diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig index ae0b56e6f009..45a21beabd89 100644 --- a/core/java/android/service/chooser/flags.aconfig +++ b/core/java/android/service/chooser/flags.aconfig @@ -44,6 +44,16 @@ flag { } flag { + name: "notify_single_item_change_on_icon_load" + namespace: "intentresolver" + description: "ChooserGridAdapter to notify specific items change when the target icon is loaded (instead of all-item change)." + bug: "298193161" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "fix_resolver_memory_leak" is_exported: true namespace: "intentresolver" diff --git a/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java b/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java index e1dc6f6d642b..e9ddfc3559bf 100644 --- a/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java +++ b/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java @@ -96,6 +96,10 @@ class QuickAccessWalletServiceInfo { defaultAppPackageName = defaultPaymentApp.getPackageName(); } + if (defaultAppPackageName == null || defaultAppUser < 0) { + return null; + } + ServiceInfo serviceInfo = getWalletServiceInfo(context, defaultAppPackageName, defaultAppUser); if (serviceInfo == null) { diff --git a/core/java/android/service/voice/HotwordDetectionService.java b/core/java/android/service/voice/HotwordDetectionService.java index 937aecc8d718..0ace80875325 100644 --- a/core/java/android/service/voice/HotwordDetectionService.java +++ b/core/java/android/service/voice/HotwordDetectionService.java @@ -230,8 +230,8 @@ public abstract class HotwordDetectionService extends Service } @Override - public void ping(IRemoteCallback callback) throws RemoteException { - callback.sendResult(null); + public void ping(IPingMe callback) throws RemoteException { + callback.onPing(); } @Override diff --git a/core/java/android/service/voice/ISandboxedDetectionService.aidl b/core/java/android/service/voice/ISandboxedDetectionService.aidl index c76ac28eb36c..5c4503cb359c 100644 --- a/core/java/android/service/voice/ISandboxedDetectionService.aidl +++ b/core/java/android/service/voice/ISandboxedDetectionService.aidl @@ -65,11 +65,15 @@ oneway interface ISandboxedDetectionService { void updateRecognitionServiceManager( in IRecognitionServiceManager recognitionServiceManager); + interface IPingMe { + void onPing(); + } + /** * Simply requests the service to trigger the callback, so that the system can check its * identity. */ - void ping(in IRemoteCallback callback); + void ping(in IPingMe callback); void stopDetection(); diff --git a/core/java/android/service/voice/VisualQueryDetectionService.java b/core/java/android/service/voice/VisualQueryDetectionService.java index 8c9731d12df3..5dcaa3e976eb 100644 --- a/core/java/android/service/voice/VisualQueryDetectionService.java +++ b/core/java/android/service/voice/VisualQueryDetectionService.java @@ -122,8 +122,8 @@ public abstract class VisualQueryDetectionService extends Service } @Override - public void ping(IRemoteCallback callback) throws RemoteException { - callback.sendResult(null); + public void ping(IPingMe callback) throws RemoteException { + callback.onPing(); } @Override diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 44c3f9a8244e..0152c52a6753 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -75,9 +75,12 @@ public abstract class Layout { // These should match the constants in framework/base/libs/hwui/hwui/DrawTextFunctor.h private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX = 0f; private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR = 0f; - private static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_DP = 5f; // since we're not using soft light yet, this needs to be much lower than the spec'd 0.8 private static final float HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE = 0.7f; + @VisibleForTesting + static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP = 5f; + @VisibleForTesting + static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR = 0.5f; /** @hide */ @IntDef(prefix = { "BREAK_STRATEGY_" }, value = { @@ -1030,7 +1033,9 @@ public abstract class Layout { var padding = Math.max(HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX, mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR); - var cornerRadius = mPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_DP; + var cornerRadius = Math.max( + mPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP, + mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR); // We set the alpha on the color itself instead of Paint.setAlpha(), because that function // actually mutates the color in... *ehem* very strange ways. Also the color might get reset diff --git a/core/java/android/util/MapCollections.java b/core/java/android/util/MapCollections.java index cce3a0e3eaa9..e7ceaae964ef 100644 --- a/core/java/android/util/MapCollections.java +++ b/core/java/android/util/MapCollections.java @@ -355,7 +355,12 @@ abstract class MapCollections<K, V> { } return result; } - }; + + @Override + public String toString() { + return toStringHelper(0, this, "KeySet"); + } + } final class ValuesCollection implements Collection<V> { @@ -456,7 +461,12 @@ abstract class MapCollections<K, V> { public <T> T[] toArray(T[] array) { return toArrayHelper(array, 1); } - }; + + @Override + public String toString() { + return toStringHelper(1, this, "ValuesCollection"); + } + } public static <K, V> boolean containsAllHelper(Map<K, V> map, Collection<?> collection) { Iterator<?> it = collection.iterator(); @@ -513,6 +523,29 @@ abstract class MapCollections<K, V> { return array; } + private String toStringHelper(int offset, Object thing, String thingName) { + int size = colGetSize(); + if (size == 0) { + return "[]"; + } + + StringBuilder buffer = new StringBuilder(size * 14); + buffer.append('['); + for (int i = 0; i < size; i++) { + if (i > 0) { + buffer.append(", "); + } + Object entry = colGetEntry(i, offset); + if (entry != thing) { + buffer.append(entry); + } else { + buffer.append("(this ").append(thingName).append(")"); + } + } + buffer.append(']'); + return buffer.toString(); + } + public static <T> boolean equalsSetHelper(Set<T> set, Object object) { if (set == object) { return true; diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java index 7c1e4976b9d3..3a2ec91b3b20 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -967,8 +967,11 @@ public final class Choreographer { DisplayEventReceiver.VsyncEventData vsyncEventData) { final long startNanos; final long frameIntervalNanos = vsyncEventData.frameInterval; - boolean resynced = false; + // Original intended vsync time that is not adjusted by jitter + // or buffer stuffing recovery. Reported for jank tracking. + final long intendedFrameTimeNanos = frameTimeNanos; long offsetFrameTimeNanos = frameTimeNanos; + boolean resynced = false; // Evaluate if buffer stuffing recovery needs to start or end, and // what actions need to be taken for recovery. @@ -1012,7 +1015,6 @@ public final class Choreographer { + ((offsetFrameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms"); } - long intendedFrameTimeNanos = offsetFrameTimeNanos; startNanos = System.nanoTime(); // Calculating jitter involves using the original frame time without // adjustments from buffer stuffing diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java index aad3bf2679d9..901fc02f0040 100644 --- a/core/java/android/view/LayoutInflater.java +++ b/core/java/android/view/LayoutInflater.java @@ -516,8 +516,9 @@ public abstract class LayoutInflater { mConstructorArgs[0] = inflaterContext; View result = root; - if (root != null && root.getViewRootImpl() != null) { - root.getViewRootImpl().notifyRendererOfExpensiveFrame(); + ViewRootImpl viewRootImpl = root != null ? root.getViewRootImpl() : null; + if (viewRootImpl != null) { + viewRootImpl.notifyRendererOfExpensiveFrame(); } try { diff --git a/core/java/android/view/ListenerWrapper.java b/core/java/android/view/ListenerWrapper.java new file mode 100644 index 000000000000..fcf3fdb68112 --- /dev/null +++ b/core/java/android/view/ListenerWrapper.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.annotation.NonNull; + +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * A utilty class to bundle a {@link Consumer} and an {@link Executor} + * @param <T> the type of value to be reported. + * @hide + */ +public class ListenerWrapper<T> { + + @NonNull + private final Consumer<T> mConsumer; + @NonNull + private final Executor mExecutor; + + public ListenerWrapper(@NonNull Executor executor, @NonNull Consumer<T> consumer) { + mExecutor = Objects.requireNonNull(executor); + mConsumer = Objects.requireNonNull(consumer); + } + + /** + * Relays the new value to the {@link Consumer} using the {@link Executor} + */ + public void accept(@NonNull T value) { + mExecutor.execute(() -> mConsumer.accept(value)); + } + + /** + * Returns {@code true} if the consumer matches the one provided in the constructor, + * {@code false} otherwise. + */ + public boolean isConsumerSame(@NonNull Consumer<T> consumer) { + return mConsumer.equals(consumer); + } +} diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index a5da0c3ce5b1..624216776f42 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -44,6 +44,7 @@ import android.util.Log; import android.util.Pair; import android.util.SparseArray; import android.view.inputmethod.InputMethodManager; +import android.view.translation.ListenerGroup; import android.window.ITrustedPresentationListener; import android.window.InputTransferToken; import android.window.TrustedPresentationThresholds; @@ -58,6 +59,7 @@ import java.io.FileOutputStream; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.List; import java.util.WeakHashMap; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -147,6 +149,12 @@ public final class WindowManagerGlobal { @UnsupportedAppUsage private final ArrayList<View> mViews = new ArrayList<View>(); + /** + * The {@link ListenerGroup} that is associated to {@link #mViews}. + * @hide + */ + @GuardedBy("mLock") + private final ListenerGroup<List<View>> mWindowViewsListenerGroup = new ListenerGroup<>(); @UnsupportedAppUsage private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>(); @UnsupportedAppUsage @@ -319,6 +327,29 @@ public final class WindowManagerGlobal { } } + /** + * Adds a listener that will be notified whenever {@link #getWindowViews()} changes. The + * current value is provided immediately. If it was registered previously then this is ano op. + */ + public void addWindowViewsListener(@NonNull Executor executor, + @NonNull Consumer<List<View>> consumer) { + synchronized (mLock) { + mWindowViewsListenerGroup.addListener(executor, consumer); + mWindowViewsListenerGroup.accept(getWindowViews()); + } + } + + /** + * Removes a listener that was registered in + * {@link #addWindowViewsListener(Executor, Consumer)}. If it was not registered previously, + * then this is a no op. + */ + public void removeWindowViewsListener(@NonNull Consumer<List<View>> consumer) { + synchronized (mLock) { + mWindowViewsListenerGroup.removeListener(consumer); + } + } + public View getWindowView(IBinder windowToken) { synchronized (mLock) { final int numViews = mViews.size(); @@ -454,6 +485,7 @@ public final class WindowManagerGlobal { // do this last because it fires off messages to start doing things try { root.setView(view, wparams, panelParentView, userId); + mWindowViewsListenerGroup.accept(getWindowViews()); } catch (RuntimeException e) { Log.e(TAG, "Couldn't add view: " + view, e); final int viewIndex = (index >= 0) ? index : (mViews.size() - 1); @@ -575,6 +607,7 @@ public final class WindowManagerGlobal { mDyingViews.remove(view); } allViewsRemoved = mRoots.isEmpty(); + mWindowViewsListenerGroup.accept(getWindowViews()); } // If we don't have any views anymore in our process, we no longer need the diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig index d06f885638b6..d97310494d34 100644 --- a/core/java/android/view/flags/view_flags.aconfig +++ b/core/java/android/view/flags/view_flags.aconfig @@ -159,3 +159,11 @@ flag { bug: "364653005" is_fixed_read_only: true } + +flag { + name: "root_view_changed_listener" + namespace: "windowing_sdk" + description: "Implement listener pattern for WindowInspector#getGlobalWindowViews." + bug: "394397033" + is_fixed_read_only: false +} diff --git a/core/java/android/view/inspector/WindowInspector.java b/core/java/android/view/inspector/WindowInspector.java index 69d004e860fd..3ebca3c9d9b6 100644 --- a/core/java/android/view/inspector/WindowInspector.java +++ b/core/java/android/view/inspector/WindowInspector.java @@ -16,11 +16,14 @@ package android.view.inspector; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.view.View; import android.view.WindowManagerGlobal; import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * Provides access to window inspection information. @@ -37,4 +40,25 @@ public final class WindowInspector { public static List<View> getGlobalWindowViews() { return WindowManagerGlobal.getInstance().getWindowViews(); } + + /** + * Adds a listener that is notified whenever the list of global window views changes. If a + * {@link Consumer} is already registered this method is a no op. + * @see #getGlobalWindowViews() + */ + @FlaggedApi(android.view.flags.Flags.FLAG_ROOT_VIEW_CHANGED_LISTENER) + public static void addGlobalWindowViewsListener(@NonNull Executor executor, + @NonNull Consumer<List<View>> consumer) { + WindowManagerGlobal.getInstance().addWindowViewsListener(executor, consumer); + } + + /** + * Removes a listener from getting notifications of global window views changes. If the + * {@link Consumer} is not registered this method is a no op. + * @see #addGlobalWindowViewsListener(Executor, Consumer) + */ + @FlaggedApi(android.view.flags.Flags.FLAG_ROOT_VIEW_CHANGED_LISTENER) + public static void removeGlobalWindowViewsListener(@NonNull Consumer<List<View>> consumer) { + WindowManagerGlobal.getInstance().removeWindowViewsListener(consumer); + } } diff --git a/core/java/android/view/translation/ListenerGroup.java b/core/java/android/view/translation/ListenerGroup.java new file mode 100644 index 000000000000..bf506815f841 --- /dev/null +++ b/core/java/android/view/translation/ListenerGroup.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.translation; + +import android.annotation.NonNull; +import android.view.ListenerWrapper; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * A utility class to manage a list of {@link ListenerWrapper}. This class is not thread safe. + * @param <T> the type of the value to be reported. + * @hide + */ +public class ListenerGroup<T> { + private final List<ListenerWrapper<T>> mListeners = new ArrayList<>(); + + /** + * Relays the value to all the registered {@link java.util.function.Consumer} + */ + public void accept(@NonNull T value) { + Objects.requireNonNull(value); + for (int i = 0; i < mListeners.size(); i++) { + mListeners.get(i).accept(value); + } + } + + /** + * Adds a {@link Consumer} to the group. If the {@link Consumer} is already present then this + * is a no op. + */ + public void addListener(@NonNull Executor executor, @NonNull Consumer<T> consumer) { + if (isContained(consumer)) { + return; + } + mListeners.add(new ListenerWrapper<>(executor, consumer)); + } + + /** + * Removes a {@link Consumer} from the group. If the {@link Consumer} was not present then this + * is a no op. + */ + public void removeListener(@NonNull Consumer<T> consumer) { + final int index = computeIndex(consumer); + if (index > -1) { + mListeners.remove(index); + } + } + + /** + * Returns {@code true} if the {@link Consumer} is present in the list, {@code false} + * otherwise. + */ + private boolean isContained(Consumer<T> consumer) { + return computeIndex(consumer) > -1; + } + + /** + * Returns the index of the matching {@link ListenerWrapper} if present, {@code -1} otherwise. + */ + private int computeIndex(Consumer<T> consumer) { + for (int i = 0; i < mListeners.size(); i++) { + if (mListeners.get(i).isConsumerSame(consumer)) { + return i; + } + } + return -1; + } +} diff --git a/core/java/android/window/DesktopExperienceFlags.java b/core/java/android/window/DesktopExperienceFlags.java index b4ff8e70ec36..866c16cb566d 100644 --- a/core/java/android/window/DesktopExperienceFlags.java +++ b/core/java/android/window/DesktopExperienceFlags.java @@ -47,17 +47,19 @@ public enum DesktopExperienceFlags { com.android.server.display.feature.flags.Flags::baseDensityForExternalDisplays, true), CONNECTED_DISPLAYS_CURSOR(com.android.input.flags.Flags::connectedDisplaysCursor, true), DISPLAY_TOPOLOGY(com.android.server.display.feature.flags.Flags::displayTopology, true), - ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY(Flags::enableBugFixesForSecondaryDisplay, false), + ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY(Flags::enableBugFixesForSecondaryDisplay, true), ENABLE_CONNECTED_DISPLAYS_DND(Flags::enableConnectedDisplaysDnd, false), ENABLE_CONNECTED_DISPLAYS_PIP(Flags::enableConnectedDisplaysPip, false), - ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG(Flags::enableConnectedDisplaysWindowDrag, false), + ENABLE_CONNECTED_DISPLAYS_WALLPAPER( + android.app.Flags::enableConnectedDisplaysWallpaper, false), + ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG(Flags::enableConnectedDisplaysWindowDrag, true), ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT( com.android.server.display.feature.flags.Flags::enableDisplayContentModeManagement, true), - ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS(Flags::enableDisplayFocusInShellTransitions, false), - ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING(Flags::enableDisplayWindowingModeSwitching, false), + ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS(Flags::enableDisplayFocusInShellTransitions, true), + ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING(Flags::enableDisplayWindowingModeSwitching, true), ENABLE_DRAG_TO_MAXIMIZE(Flags::enableDragToMaximize, true), - ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT(Flags::enableMoveToNextDisplayShortcut, false), + ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT(Flags::enableMoveToNextDisplayShortcut, true), ENABLE_MULTIPLE_DESKTOPS_BACKEND(Flags::enableMultipleDesktopsBackend, false), ENABLE_MULTIPLE_DESKTOPS_FRONTEND(Flags::enableMultipleDesktopsFrontend, false), ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS( @@ -67,9 +69,10 @@ public enum DesktopExperienceFlags { ENABLE_PER_DISPLAY_PACKAGE_CONTEXT_CACHE_IN_STATUSBAR_NOTIF( Flags::enablePerDisplayPackageContextCacheInStatusbarNotif, false), ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE(Flags::enableProjectedDisplayDesktopMode, false), - ENABLE_TASKBAR_CONNECTED_DISPLAYS(Flags::enableTaskbarConnectedDisplays, false), + ENABLE_TASKBAR_CONNECTED_DISPLAYS(Flags::enableTaskbarConnectedDisplays, true), ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS(Flags::enterDesktopByDefaultOnFreeformDisplays, - false), + true), + FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH(Flags::formFactorBasedDesktopFirstSwitch, false), REPARENT_WINDOW_TOKEN_API(Flags::reparentWindowTokenApi, true) // go/keep-sorted end ; diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index 78769248c013..17165cdcf7b1 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -138,10 +138,14 @@ public enum DesktopModeFlags { ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS( Flags::enableWindowingTransitionHandlersObservers, false), EXCLUDE_CAPTION_FROM_APP_BOUNDS(Flags::excludeCaptionFromAppBounds, false), + FORCE_CLOSE_TOP_TRANSPARENT_FULLSCREEN_TASK( + Flags::forceCloseTopTransparentFullscreenTask, false), IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES( Flags::ignoreAspectRatioRestrictionsForResizeableFreeformActivities, true), INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC( - Flags::includeTopTransparentFullscreenTaskInDesktopHeuristic, true) + Flags::includeTopTransparentFullscreenTaskInDesktopHeuristic, true), + INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES( + Flags::inheritTaskBoundsForTrampolineTaskLaunches, false), // go/keep-sorted end ; diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index 1156503cf8e8..ea345a5ef46a 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -688,12 +688,6 @@ public final class WindowContainerTransaction implements Parcelable { @NonNull public WindowContainerTransaction setAdjacentRoots( @NonNull WindowContainerToken root1, @NonNull WindowContainerToken root2) { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - mHierarchyOps.add(HierarchyOp.createForAdjacentRoots( - root1.asBinder(), - root2.asBinder())); - return this; - } return setAdjacentRootSet(root1, root2); } @@ -714,10 +708,6 @@ public final class WindowContainerTransaction implements Parcelable { */ @NonNull public WindowContainerTransaction setAdjacentRootSet(@NonNull WindowContainerToken... roots) { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - throw new IllegalArgumentException("allowMultipleAdjacentTaskFragments is not enabled." - + " Use #setAdjacentRoots instead."); - } if (roots.length < 2) { throw new IllegalArgumentException("setAdjacentRootSet must have size >= 2"); } @@ -1973,13 +1963,6 @@ public final class WindowContainerTransaction implements Parcelable { return mContainers; } - /** @deprecated b/373709676 replace with {@link #getContainers()}. */ - @Deprecated - @NonNull - public IBinder getAdjacentRoot() { - return mReparent; - } - public boolean getToTop() { return mToTop; } @@ -2127,17 +2110,12 @@ public final class WindowContainerTransaction implements Parcelable { sb.append(mContainer).append(" to ").append(mToTop ? "top" : "bottom"); break; case HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS: - if (Flags.allowMultipleAdjacentTaskFragments()) { - for (IBinder container : mContainers) { - if (container == mContainers[0]) { - sb.append("adjacentRoots=").append(container); - } else { - sb.append(", ").append(container); - } + for (IBinder container : mContainers) { + if (container == mContainers[0]) { + sb.append("adjacentRoots=").append(container); + } else { + sb.append(", ").append(container); } - } else { - sb.append("container=").append(mContainer) - .append(" adjacentRoot=").append(mReparent); } break; case HIERARCHY_OP_TYPE_LAUNCH_TASK: diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 1f710c1cc8c0..e706af999117 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -27,6 +27,17 @@ flag { } flag { + name: "inherit_task_bounds_for_trampoline_task_launches" + namespace: "lse_desktop_experience" + description: "Forces trampoline task launches to inherit the bounds of the previous instance /n" + "before is closes to prevent each task from cascading." + bug: "392815318" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "include_top_transparent_fullscreen_task_in_desktop_heuristic" namespace: "lse_desktop_experience" description: "Whether to include any top transparent fullscreen task launched in desktop /n" @@ -50,6 +61,17 @@ flag { } flag { + name: "force_close_top_transparent_fullscreen_task" + namespace: "lse_desktop_experience" + description: "If a top transparent fullscreen task is on top of desktop mode, force it to /n" + "close if another task is opened or brought to front." + bug: "395041610" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_windowing_dynamic_initial_bounds" namespace: "lse_desktop_experience" description: "Enables new initial bounds for desktop windowing which adjust depending on app constraints" @@ -916,3 +938,10 @@ flag { description: "Enables the desktop-first mode switching logic based on its form factor." bug: "394736817" } + +flag { + name: "enable_restart_menu_for_connected_displays" + namespace: "lse_desktop_experience" + description: "Enable restart menu UI, which is shown when an app moves between displays." + bug: "397804287" +} diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index b4fec416bd5f..3927c713e500 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -86,6 +86,14 @@ flag { } flag { + name: "action_mode_edge_to_edge" + namespace: "windowing_frontend" + description: "Make contextual action bar edge-to-edge" + bug: "379783298" + is_fixed_read_only: true +} + +flag { name: "keyguard_going_away_timeout" namespace: "windowing_frontend" description: "Allow a maximum of 10 seconds with keyguardGoingAway=true before force-resetting" @@ -501,6 +509,17 @@ flag { description: "Sets Launch powermode for activity launches earlier" bug: "399380676" is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "scramble_snapshot_file_name" + namespace: "windowing_frontend" + description: "Scramble the file name of task snapshot." + bug: "293139053" + is_fixed_read_only: true metadata { purpose: PURPOSE_BUGFIX } diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index c009fc3b7e63..9bc6671bbc31 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -23,6 +23,7 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE; import static android.content.ContentProvider.getUriWithoutUserId; import static android.content.ContentProvider.getUserIdFromUri; +import static android.service.chooser.Flags.notifySingleItemChangeOnIconLoad; import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL; import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK; @@ -163,9 +164,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -3212,6 +3215,8 @@ public class ChooserActivity extends ResolverActivity implements private static final int NUM_EXPANSIONS_TO_HIDE_AZ_LABEL = 20; + private final Set<ViewHolderBase> mBoundViewHolders = new HashSet<>(); + ChooserGridAdapter(ChooserListAdapter wrappedAdapter) { super(); mChooserListAdapter = wrappedAdapter; @@ -3232,6 +3237,31 @@ public class ChooserActivity extends ResolverActivity implements notifyDataSetChanged(); } }); + if (notifySingleItemChangeOnIconLoad()) { + wrappedAdapter.setOnIconLoadedListener(this::onTargetIconLoaded); + } + } + + private void onTargetIconLoaded(DisplayResolveInfo info) { + for (ViewHolderBase holder : mBoundViewHolders) { + switch (holder.getViewType()) { + case VIEW_TYPE_NORMAL: + TargetInfo itemInfo = + mChooserListAdapter.getItem( + ((ItemViewHolder) holder).mListPosition); + if (info == itemInfo) { + notifyItemChanged(holder.getAdapterPosition()); + } + break; + case VIEW_TYPE_CALLER_AND_RANK: + ItemGroupViewHolder groupHolder = (ItemGroupViewHolder) holder; + if (suggestedAppsGroupContainsTarget(groupHolder, info)) { + notifyItemChanged(holder.getAdapterPosition()); + } + break; + } + + } } public void setFooterHeight(int height) { @@ -3382,6 +3412,9 @@ public class ChooserActivity extends ResolverActivity implements @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + if (notifySingleItemChangeOnIconLoad()) { + mBoundViewHolders.add((ViewHolderBase) holder); + } int viewType = ((ViewHolderBase) holder).getViewType(); switch (viewType) { case VIEW_TYPE_DIRECT_SHARE: @@ -3396,6 +3429,22 @@ public class ChooserActivity extends ResolverActivity implements } @Override + public void onViewRecycled(RecyclerView.ViewHolder holder) { + if (notifySingleItemChangeOnIconLoad()) { + mBoundViewHolders.remove((ViewHolderBase) holder); + } + super.onViewRecycled(holder); + } + + @Override + public boolean onFailedToRecycleView(RecyclerView.ViewHolder holder) { + if (notifySingleItemChangeOnIconLoad()) { + mBoundViewHolders.remove((ViewHolderBase) holder); + } + return super.onFailedToRecycleView(holder); + } + + @Override public int getItemViewType(int position) { int count; @@ -3604,6 +3653,33 @@ public class ChooserActivity extends ResolverActivity implements } } + /** + * Checks whether the suggested apps group, {@code holder}, contains the target, + * {@code info}. + */ + private boolean suggestedAppsGroupContainsTarget( + ItemGroupViewHolder holder, DisplayResolveInfo info) { + + int position = holder.getAdapterPosition(); + int start = getListPosition(position); + int startType = getRowType(start); + + int columnCount = holder.getColumnCount(); + int end = start + columnCount - 1; + while (getRowType(end) != startType && end >= start) { + end--; + } + + for (int i = 0; i < columnCount; i++) { + if (start + i <= end) { + if (mChooserListAdapter.getItem(holder.getItemIndex(i)) == info) { + return true; + } + } + } + return false; + } + int getListPosition(int position) { position -= getSystemRowCount() + getProfileRowCount(); diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java index d38689c7505b..1b8c36db3908 100644 --- a/core/java/com/android/internal/app/ChooserListAdapter.java +++ b/core/java/com/android/internal/app/ChooserListAdapter.java @@ -16,9 +16,12 @@ package com.android.internal.app; +import static android.service.chooser.Flags.notifySingleItemChangeOnIconLoad; + import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE; import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER; +import android.annotation.Nullable; import android.app.prediction.AppPredictor; import android.content.ComponentName; import android.content.Context; @@ -56,6 +59,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; public class ChooserListAdapter extends ResolverListAdapter { private static final String TAG = "ChooserListAdapter"; @@ -108,6 +112,9 @@ public class ChooserListAdapter extends ResolverListAdapter { // Represents the UserSpace in which the Initial Intents should be resolved. private final UserHandle mInitialIntentsUserSpace; + @Nullable + private Consumer<DisplayResolveInfo> mOnIconLoadedListener; + // For pinned direct share labels, if the text spans multiple lines, the TextView will consume // the full width, even if the characters actually take up less than that. Measure the actual // line widths and constrain the View's width based upon that so that the pin doesn't end up @@ -218,6 +225,10 @@ public class ChooserListAdapter extends ResolverListAdapter { true); } + public void setOnIconLoadedListener(Consumer<DisplayResolveInfo> onIconLoadedListener) { + mOnIconLoadedListener = onIconLoadedListener; + } + AppPredictor getAppPredictor() { return mAppPredictor; } @@ -329,6 +340,15 @@ public class ChooserListAdapter extends ResolverListAdapter { } } + @Override + protected void onIconLoaded(DisplayResolveInfo info) { + if (notifySingleItemChangeOnIconLoad() && mOnIconLoadedListener != null) { + mOnIconLoadedListener.accept(info); + } else { + notifyDataSetChanged(); + } + } + private void loadDirectShareIcon(SelectableTargetInfo info) { LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info); if (task == null) { diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java index 54c0e61fd5cd..4d9ce86096c7 100644 --- a/core/java/com/android/internal/app/ResolverListAdapter.java +++ b/core/java/com/android/internal/app/ResolverListAdapter.java @@ -680,6 +680,10 @@ public class ResolverListAdapter extends BaseAdapter { } } + protected void onIconLoaded(DisplayResolveInfo info) { + notifyDataSetChanged(); + } + private void loadLabel(DisplayResolveInfo info) { LoadLabelTask task = mLabelLoaders.get(info); if (task == null) { @@ -1004,7 +1008,7 @@ public class ResolverListAdapter extends BaseAdapter { mResolverListCommunicator.updateProfileViewButton(); } else if (!mDisplayResolveInfo.hasDisplayIcon()) { mDisplayResolveInfo.setDisplayIcon(d); - notifyDataSetChanged(); + onIconLoaded(mDisplayResolveInfo); } } } diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java index 81ca23173457..c6207f9451c2 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -45,11 +45,13 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.concurrent.locks.ReentrantLock; /** @@ -203,15 +205,6 @@ public class BatteryStatsHistory { BatteryHistoryFragment getLatestFragment(); /** - * Given a fragment, returns the earliest fragment that follows it whose monotonic - * start time falls within the specified range. `startTimeMs` is inclusive, `endTimeMs` - * is exclusive. - */ - @Nullable - BatteryHistoryFragment getNextFragment(BatteryHistoryFragment current, long startTimeMs, - long endTimeMs); - - /** * Acquires a lock on the entire store. */ void lock(); @@ -268,6 +261,60 @@ public class BatteryStatsHistory { void reset(); } + class BatteryHistoryParcelContainer { + private boolean mParcelReadyForReading; + private Parcel mParcel; + private BatteryStatsHistory.BatteryHistoryFragment mFragment; + private long mMonotonicStartTime; + + BatteryHistoryParcelContainer(@NonNull Parcel parcel, long monotonicStartTime) { + mParcel = parcel; + mMonotonicStartTime = monotonicStartTime; + mParcelReadyForReading = true; + } + + BatteryHistoryParcelContainer(@NonNull BatteryHistoryFragment fragment) { + mFragment = fragment; + mMonotonicStartTime = fragment.monotonicTimeMs; + mParcelReadyForReading = false; + } + + @Nullable + Parcel getParcel() { + if (mParcelReadyForReading) { + return mParcel; + } + + Parcel parcel = Parcel.obtain(); + if (readFragmentToParcel(parcel, mFragment)) { + parcel.readInt(); // skip buffer size + mParcel = parcel; + } else { + parcel.recycle(); + } + mParcelReadyForReading = true; + return mParcel; + } + + long getMonotonicStartTime() { + return mMonotonicStartTime; + } + + /** + * Recycles the parcel (if appropriate). Should be called after the parcel has been + * processed by the iterator. + */ + void close() { + if (mParcel != null && mFragment != null) { + mParcel.recycle(); + } + // ParcelContainers are not meant to be reusable. To prevent any unintentional + // access to the parcel after it has been recycled, clear the references to it. + mParcel = null; + mFragment = null; + } + } + private final Parcel mHistoryBuffer; private final HistoryStepDetailsCalculator mStepDetailsCalculator; private final Clock mClock; @@ -447,6 +494,7 @@ public class BatteryStatsHistory { mWritableHistory = writableHistory; if (mWritableHistory != null) { mMutable = false; + mHistoryBufferStartTime = mWritableHistory.mHistoryBufferStartTime; mHistoryMonotonicEndTime = mWritableHistory.mHistoryMonotonicEndTime; } @@ -689,95 +737,66 @@ public class BatteryStatsHistory { } /** - * When iterating history files and history buffer, always start from the lowest numbered - * history file, when reached the mActiveFile (highest numbered history file), do not read from - * mActiveFile, read from history buffer instead because the buffer has more updated data. - * - * @return The parcel that has next record. null if finished all history files and history - * buffer + * Returns all chunks of accumulated history that contain items within the range between + * `startTimeMs` (inclusive) and `endTimeMs` (exclusive) */ - @Nullable - public Parcel getNextParcel(long startTimeMs, long endTimeMs) { - checkImmutable(); + Queue<BatteryHistoryParcelContainer> getParcelContainers(long startTimeMs, long endTimeMs) { + if (mMutable) { + throw new IllegalStateException("Iterating over a mutable battery history"); + } - // First iterate through all records in current parcel. - if (mCurrentParcel != null) { - if (mCurrentParcel.dataPosition() < mCurrentParcelEnd) { - // There are more records in current parcel. - return mCurrentParcel; - } else if (mHistoryBuffer == mCurrentParcel) { - // finished iterate through all history files and history buffer. - return null; - } else if (mHistoryParcels == null - || !mHistoryParcels.contains(mCurrentParcel)) { - // current parcel is from history file. - mCurrentParcel.recycle(); - } + if (endTimeMs == MonotonicClock.UNDEFINED || endTimeMs == 0) { + endTimeMs = Long.MAX_VALUE; } + Queue<BatteryHistoryParcelContainer> containers = new ArrayDeque<>(); + if (mStore != null) { - BatteryHistoryFragment next = mStore.getNextFragment(mCurrentFragment, startTimeMs, - endTimeMs); - while (next != null) { - mCurrentParcel = null; - mCurrentParcelEnd = 0; - final Parcel p = Parcel.obtain(); - if (readFragmentToParcel(p, next)) { - int bufSize = p.readInt(); - int curPos = p.dataPosition(); - mCurrentParcelEnd = curPos + bufSize; - mCurrentParcel = p; - if (curPos < mCurrentParcelEnd) { - mCurrentFragment = next; - return mCurrentParcel; - } - } else { - p.recycle(); + List<BatteryHistoryFragment> fragments = mStore.getFragments(); + for (int i = 0; i < fragments.size(); i++) { + BatteryHistoryFragment fragment = fragments.get(i); + if (fragment.monotonicTimeMs >= endTimeMs) { + break; + } + + if (fragment.monotonicTimeMs >= startTimeMs && fragment != mActiveFragment) { + containers.add(new BatteryHistoryParcelContainer(fragment)); } - next = mStore.getNextFragment(next, startTimeMs, endTimeMs); } } - // mHistoryParcels is created when BatteryStatsImpl object is created from deserialization - // of a parcel, such as Settings app or checkin file. if (mHistoryParcels != null) { - while (mParcelIndex < mHistoryParcels.size()) { - final Parcel p = mHistoryParcels.get(mParcelIndex++); + for (int i = 0; i < mHistoryParcels.size(); i++) { + final Parcel p = mHistoryParcels.get(i); if (!verifyVersion(p)) { continue; } - // skip monotonic time field. - p.readLong(); - // skip monotonic end time field - p.readLong(); + + long monotonicStartTime = p.readLong(); + if (monotonicStartTime >= endTimeMs) { + continue; + } + + long monotonicEndTime = p.readLong(); + if (monotonicEndTime < startTimeMs) { + continue; + } + // skip monotonic size field p.readLong(); + // skip buffer size field + p.readInt(); - final int bufSize = p.readInt(); - final int curPos = p.dataPosition(); - mCurrentParcelEnd = curPos + bufSize; - mCurrentParcel = p; - if (curPos < mCurrentParcelEnd) { - return mCurrentParcel; - } + containers.add(new BatteryHistoryParcelContainer(p, monotonicStartTime)); } } - // finished iterator through history files (except the last one), now history buffer. - if (mHistoryBuffer.dataSize() <= 0) { - // buffer is empty. - return null; - } - mHistoryBuffer.setDataPosition(0); - mCurrentParcel = mHistoryBuffer; - mCurrentParcelEnd = mCurrentParcel.dataSize(); - return mCurrentParcel; - } - - private void checkImmutable() { - if (mMutable) { - throw new IllegalStateException("Iterating over a mutable battery history"); + if (mHistoryBufferStartTime < endTimeMs) { + mHistoryBuffer.setDataPosition(0); + containers.add( + new BatteryHistoryParcelContainer(mHistoryBuffer, mHistoryBufferStartTime)); } + return containers; } /** @@ -818,21 +837,6 @@ public class BatteryStatsHistory { } /** - * Extracts the monotonic time, as per {@link MonotonicClock}, from the supplied battery history - * buffer. - */ - public long getHistoryBufferStartTime(Parcel p) { - int pos = p.dataPosition(); - p.setDataPosition(0); - p.readInt(); // Skip the version field - long monotonicTime = p.readLong(); - p.readLong(); // Skip monotonic end time field - p.readLong(); // Skip monotonic size field - p.setDataPosition(pos); - return monotonicTime; - } - - /** * Writes the battery history contents for persistence. */ public void writeSummaryToParcel(Parcel out, boolean inclHistory) { diff --git a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java index 38398b4bf6f0..09f9b0bf8c7e 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java +++ b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java @@ -24,6 +24,7 @@ import android.util.Slog; import android.util.SparseArray; import java.util.Iterator; +import java.util.Queue; /** * An iterator for {@link BatteryStats.HistoryItem}'s. @@ -48,7 +49,10 @@ public class BatteryStatsHistoryIterator implements Iterator<BatteryStats.Histor private long mBaseMonotonicTime; private long mBaseTimeUtc; private int mItemIndex = 0; - private int mMaxHistoryItems; + private final int mMaxHistoryItems; + private int mParcelDataPosition; + + private Queue<BatteryStatsHistory.BatteryHistoryParcelContainer> mParcelContainers; public BatteryStatsHistoryIterator(@NonNull BatteryStatsHistory history, long startTimeMs, long endTimeMs) { @@ -62,7 +66,11 @@ public class BatteryStatsHistoryIterator implements Iterator<BatteryStats.Histor @Override public boolean hasNext() { if (!mNextItemReady) { - advance(); + if (!advance()) { + mHistoryItem = null; + close(); + } + mNextItemReady = true; } return mHistoryItem != null; @@ -75,35 +83,48 @@ public class BatteryStatsHistoryIterator implements Iterator<BatteryStats.Histor @Override public BatteryStats.HistoryItem next() { if (!mNextItemReady) { - advance(); + if (!advance()) { + mHistoryItem = null; + close(); + } } mNextItemReady = false; return mHistoryItem; } - private void advance() { - while (true) { - if (mItemIndex > mMaxHistoryItems) { - Slog.wtfStack(TAG, "Number of battery history items is too large: " + mItemIndex); - break; - } + private boolean advance() { + if (mParcelContainers == null) { + mParcelContainers = mBatteryStatsHistory.getParcelContainers(mStartTimeMs, mEndTimeMs); + } - Parcel p = mBatteryStatsHistory.getNextParcel(mStartTimeMs, mEndTimeMs); - if (p == null) { - break; + BatteryStatsHistory.BatteryHistoryParcelContainer container; + while ((container = mParcelContainers.peek()) != null) { + Parcel p = container.getParcel(); + if (p == null || p.dataPosition() >= p.dataSize()) { + container.close(); + mParcelContainers.remove(); + mParcelDataPosition = 0; + continue; } if (!mTimeInitialized) { - mBaseMonotonicTime = mBatteryStatsHistory.getHistoryBufferStartTime(p); + mBaseMonotonicTime = container.getMonotonicStartTime(); mHistoryItem.time = mBaseMonotonicTime; mTimeInitialized = true; } try { readHistoryDelta(p, mHistoryItem); + int dataPosition = p.dataPosition(); + if (dataPosition <= mParcelDataPosition) { + Slog.wtf(TAG, "Corrupted battery history, parcel is not progressing: " + + dataPosition + " of " + p.dataSize()); + return false; + } + mParcelDataPosition = dataPosition; } catch (Throwable t) { Slog.wtf(TAG, "Corrupted battery history", t); - break; + return false; } if (mHistoryItem.cmd == BatteryStats.HistoryItem.CMD_CURRENT_TIME @@ -111,21 +132,24 @@ public class BatteryStatsHistoryIterator implements Iterator<BatteryStats.Histor mBaseTimeUtc = mHistoryItem.currentTime - (mHistoryItem.time - mBaseMonotonicTime); } - mHistoryItem.currentTime = mBaseTimeUtc + (mHistoryItem.time - mBaseMonotonicTime); + if (mHistoryItem.time < mStartTimeMs) { + continue; + } - if (mEndTimeMs != 0 && mHistoryItem.time >= mEndTimeMs) { - break; + if (mEndTimeMs != 0 && mEndTimeMs != MonotonicClock.UNDEFINED + && mHistoryItem.time >= mEndTimeMs) { + return false; } - if (mHistoryItem.time >= mStartTimeMs) { - mItemIndex++; - mNextItemReady = true; - return; + + if (mItemIndex++ > mMaxHistoryItems) { + Slog.wtfStack(TAG, "Number of battery history items is too large: " + mItemIndex); + return false; } - } - mHistoryItem = null; - mNextItemReady = true; - close(); + mHistoryItem.currentTime = mBaseTimeUtc + (mHistoryItem.time - mBaseMonotonicTime); + return true; + } + return false; } private void readHistoryDelta(Parcel src, BatteryStats.HistoryItem cur) { diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index e20a52b24485..3d81e4fc7acd 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -120,6 +120,7 @@ import com.android.internal.view.menu.MenuHelper; import com.android.internal.widget.ActionBarContextView; import com.android.internal.widget.BackgroundFallback; import com.android.internal.widget.floatingtoolbar.FloatingToolbar; +import com.android.window.flags.Flags; import java.util.List; import java.util.concurrent.Executor; @@ -1003,7 +1004,8 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind public void onWindowSystemUiVisibilityChanged(int visible) { updateColorViews(null /* insets */, true /* animate */); - if (mStatusGuard != null && mStatusGuard.getVisibility() == VISIBLE) { + if (!Flags.actionModeEdgeToEdge() + && mStatusGuard != null && mStatusGuard.getVisibility() == VISIBLE) { updateStatusGuardColor(); } } @@ -1040,7 +1042,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind } mFrameOffsets.set(insets.getSystemWindowInsetsAsRect()); insets = updateColorViews(insets, true /* animate */); - insets = updateStatusGuard(insets); + insets = updateActionModeInsets(insets); if (getForeground() != null) { drawableChanged(); } @@ -1592,7 +1594,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind } } - private WindowInsets updateStatusGuard(WindowInsets insets) { + private WindowInsets updateActionModeInsets(WindowInsets insets) { boolean showStatusGuard = false; // Show the status guard when the non-overlay contextual action bar is showing if (mPrimaryActionModeView != null) { @@ -1608,54 +1610,78 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind final Rect rect = mTempRect; // Apply the insets that have not been applied by the contentParent yet. - WindowInsets innerInsets = + final WindowInsets innerInsets = mWindow.mContentParent.computeSystemWindowInsets(insets, rect); - int newTopMargin = innerInsets.getSystemWindowInsetTop(); - int newLeftMargin = innerInsets.getSystemWindowInsetLeft(); - int newRightMargin = innerInsets.getSystemWindowInsetRight(); - - // Must use root window insets for the guard, because the color views consume - // the navigation bar inset if the window does not request LAYOUT_HIDE_NAV - but - // the status guard is attached at the root. - WindowInsets rootInsets = getRootWindowInsets(); - int newGuardLeftMargin = rootInsets.getSystemWindowInsetLeft(); - int newGuardRightMargin = rootInsets.getSystemWindowInsetRight(); - - if (mlp.topMargin != newTopMargin || mlp.leftMargin != newLeftMargin - || mlp.rightMargin != newRightMargin) { - mlpChanged = true; - mlp.topMargin = newTopMargin; - mlp.leftMargin = newLeftMargin; - mlp.rightMargin = newRightMargin; - } + final boolean consumesSystemWindowInsetsTop; + if (Flags.actionModeEdgeToEdge()) { + final Insets newPadding = innerInsets.getSystemWindowInsets(); + final Insets newMargin = innerInsets.getInsets( + WindowInsets.Type.navigationBars()); + + // Don't extend into navigation bar area so the width can align with status + // bar color view. + if (mlp.leftMargin != newMargin.left + || mlp.rightMargin != newMargin.right) { + mlpChanged = true; + mlp.leftMargin = newMargin.left; + mlp.rightMargin = newMargin.right; + } + + mPrimaryActionModeView.setPadding( + newPadding.left - newMargin.left, + newPadding.top, + newPadding.right - newMargin.right, + 0); + consumesSystemWindowInsetsTop = newPadding.top > 0; + } else { + int newTopMargin = innerInsets.getSystemWindowInsetTop(); + int newLeftMargin = innerInsets.getSystemWindowInsetLeft(); + int newRightMargin = innerInsets.getSystemWindowInsetRight(); + + // Must use root window insets for the guard, because the color views + // consume the navigation bar inset if the window does not request + // LAYOUT_HIDE_NAV - but the status guard is attached at the root. + WindowInsets rootInsets = getRootWindowInsets(); + int newGuardLeftMargin = rootInsets.getSystemWindowInsetLeft(); + int newGuardRightMargin = rootInsets.getSystemWindowInsetRight(); + + if (mlp.topMargin != newTopMargin || mlp.leftMargin != newLeftMargin + || mlp.rightMargin != newRightMargin) { + mlpChanged = true; + mlp.topMargin = newTopMargin; + mlp.leftMargin = newLeftMargin; + mlp.rightMargin = newRightMargin; + } - if (newTopMargin > 0 && mStatusGuard == null) { - mStatusGuard = new View(mContext); - mStatusGuard.setVisibility(GONE); - final LayoutParams lp = new LayoutParams(MATCH_PARENT, - mlp.topMargin, Gravity.LEFT | Gravity.TOP); - lp.leftMargin = newGuardLeftMargin; - lp.rightMargin = newGuardRightMargin; - addView(mStatusGuard, indexOfChild(mStatusColorViewState.view), lp); - } else if (mStatusGuard != null) { - final LayoutParams lp = (LayoutParams) - mStatusGuard.getLayoutParams(); - if (lp.height != mlp.topMargin || lp.leftMargin != newGuardLeftMargin - || lp.rightMargin != newGuardRightMargin) { - lp.height = mlp.topMargin; + if (newTopMargin > 0 && mStatusGuard == null) { + mStatusGuard = new View(mContext); + mStatusGuard.setVisibility(GONE); + final LayoutParams lp = new LayoutParams(MATCH_PARENT, + mlp.topMargin, Gravity.LEFT | Gravity.TOP); lp.leftMargin = newGuardLeftMargin; lp.rightMargin = newGuardRightMargin; - mStatusGuard.setLayoutParams(lp); + addView(mStatusGuard, indexOfChild(mStatusColorViewState.view), lp); + } else if (mStatusGuard != null) { + final LayoutParams lp = (LayoutParams) + mStatusGuard.getLayoutParams(); + if (lp.height != mlp.topMargin || lp.leftMargin != newGuardLeftMargin + || lp.rightMargin != newGuardRightMargin) { + lp.height = mlp.topMargin; + lp.leftMargin = newGuardLeftMargin; + lp.rightMargin = newGuardRightMargin; + mStatusGuard.setLayoutParams(lp); + } } - } - // The action mode's theme may differ from the app, so - // always show the status guard above it if we have one. - showStatusGuard = mStatusGuard != null; + // The action mode's theme may differ from the app, so + // always show the status guard above it if we have one. + showStatusGuard = mStatusGuard != null; - if (showStatusGuard && mStatusGuard.getVisibility() != VISIBLE) { - // If it wasn't previously shown, the color may be stale - updateStatusGuardColor(); + if (showStatusGuard && mStatusGuard.getVisibility() != VISIBLE) { + // If it wasn't previously shown, the color may be stale + updateStatusGuardColor(); + } + consumesSystemWindowInsetsTop = showStatusGuard; } // We only need to consume the insets if the action @@ -1664,14 +1690,16 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind // screen_simple_overlay_action_mode.xml). final boolean nonOverlay = (mWindow.getLocalFeaturesPrivate() & (1 << Window.FEATURE_ACTION_MODE_OVERLAY)) == 0; - if (nonOverlay && showStatusGuard) { + if (nonOverlay && consumesSystemWindowInsetsTop) { insets = insets.inset(0, insets.getSystemWindowInsetTop(), 0, 0); } } else { - // reset top margin - if (mlp.topMargin != 0 || mlp.leftMargin != 0 || mlp.rightMargin != 0) { - mlpChanged = true; - mlp.topMargin = 0; + if (!Flags.actionModeEdgeToEdge()) { + // reset top margin + if (mlp.topMargin != 0 || mlp.leftMargin != 0 || mlp.rightMargin != 0) { + mlpChanged = true; + mlp.topMargin = 0; + } } } if (mlpChanged) { @@ -1679,7 +1707,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind } } } - if (mStatusGuard != null) { + if (!Flags.actionModeEdgeToEdge() && mStatusGuard != null) { mStatusGuard.setVisibility(showStatusGuard ? VISIBLE : GONE); } return insets; @@ -2183,7 +2211,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind for (int i = getChildCount() - 1; i >= 0; i--) { View v = getChildAt(i); if (v != mStatusColorViewState.view && v != mNavigationColorViewState.view - && v != mStatusGuard) { + && (Flags.actionModeEdgeToEdge() || v != mStatusGuard)) { removeViewAt(i); } } diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index 2d989943800e..6f7e5ad51b89 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -79,6 +79,7 @@ import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.UUID; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -864,5 +865,24 @@ public abstract class PerfettoProtoLogImpl extends IProtoLogClient.Stub implemen throw new RuntimeException("Both mMessageString and mMessageHash should never be null"); } } + + /** + * This is only used by unit tests to wait until {@link #connectToConfigurationService} is + * done. Because unit tests are sensitive to concurrent accesses. + */ + @VisibleForTesting + public static void waitForInitialization() { + final IProtoLog currentInstance = ProtoLog.getSingleInstance(); + if (!(currentInstance instanceof PerfettoProtoLogImpl protoLog)) { + return; + } + try { + protoLog.mBackgroundLoggingService.submit(() -> { + Log.i(LOG_TAG, "Complete initialization"); + }).get(); + } catch (InterruptedException | ExecutionException e) { + Log.e(LOG_TAG, "Failed to wait for tracing service", e); + } + } } diff --git a/core/java/com/android/internal/widget/ActionBarContextView.java b/core/java/com/android/internal/widget/ActionBarContextView.java index 80fc218839d5..d5bb51187ba4 100644 --- a/core/java/com/android/internal/widget/ActionBarContextView.java +++ b/core/java/com/android/internal/widget/ActionBarContextView.java @@ -34,6 +34,7 @@ import android.widget.TextView; import com.android.internal.R; import com.android.internal.view.menu.MenuBuilder; +import com.android.window.flags.Flags; /** * @hide @@ -315,12 +316,14 @@ public class ActionBarContextView extends AbsActionBarView { final int contentWidth = MeasureSpec.getSize(widthMeasureSpec); - int maxHeight = mContentHeight > 0 ? - mContentHeight : MeasureSpec.getSize(heightMeasureSpec); + final int maxHeight = !Flags.actionModeEdgeToEdge() && mContentHeight > 0 + ? mContentHeight : MeasureSpec.getSize(heightMeasureSpec); final int verticalPadding = getPaddingTop() + getPaddingBottom(); int availableWidth = contentWidth - getPaddingLeft() - getPaddingRight(); - final int height = maxHeight - verticalPadding; + final int height = Flags.actionModeEdgeToEdge() + ? mContentHeight > 0 ? mContentHeight : maxHeight + : maxHeight - verticalPadding; final int childSpecHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST); if (mClose != null) { @@ -376,7 +379,8 @@ public class ActionBarContextView extends AbsActionBarView { } setMeasuredDimension(contentWidth, measuredHeight); } else { - setMeasuredDimension(contentWidth, maxHeight); + setMeasuredDimension(contentWidth, Flags.actionModeEdgeToEdge() + ? mContentHeight + verticalPadding : maxHeight); } } diff --git a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java index ff57fd4fe2ce..362b79db4003 100644 --- a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java +++ b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java @@ -294,54 +294,24 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar } } - private boolean applyInsets(View view, Rect insets, boolean toPadding, - boolean left, boolean top, boolean right, boolean bottom) { - boolean changed; - if (toPadding) { - changed = setMargin(view, EMPTY_RECT, left, top, right, bottom); - changed |= setPadding(view, insets, left, top, right, bottom); - } else { - changed = setPadding(view, EMPTY_RECT, left, top, right, bottom); - changed |= setMargin(view, insets, left, top, right, bottom); - } - return changed; - } - - private boolean setPadding(View view, Rect insets, - boolean left, boolean top, boolean right, boolean bottom) { - if ((left && view.getPaddingLeft() != insets.left) - || (top && view.getPaddingTop() != insets.top) - || (right && view.getPaddingRight() != insets.right) - || (bottom && view.getPaddingBottom() != insets.bottom)) { - view.setPadding( - left ? insets.left : view.getPaddingLeft(), - top ? insets.top : view.getPaddingTop(), - right ? insets.right : view.getPaddingRight(), - bottom ? insets.bottom : view.getPaddingBottom()); - return true; - } - return false; - } - - private boolean setMargin(View view, Rect insets, - boolean left, boolean top, boolean right, boolean bottom) { + private boolean setMargin(View view, int left, int top, int right, int bottom) { final LayoutParams lp = (LayoutParams) view.getLayoutParams(); boolean changed = false; - if (left && lp.leftMargin != insets.left) { + if (lp.leftMargin != left) { changed = true; - lp.leftMargin = insets.left; + lp.leftMargin = left; } - if (top && lp.topMargin != insets.top) { + if (lp.topMargin != top) { changed = true; - lp.topMargin = insets.top; + lp.topMargin = top; } - if (right && lp.rightMargin != insets.right) { + if (lp.rightMargin != right) { changed = true; - lp.rightMargin = insets.right; + lp.rightMargin = right; } - if (bottom && lp.bottomMargin != insets.bottom) { + if (lp.bottomMargin != bottom) { changed = true; - lp.bottomMargin = insets.bottom; + lp.bottomMargin = bottom; } return changed; } @@ -367,12 +337,30 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar final Insets sysInsets = insets.getSystemWindowInsets(); mSystemInsets.set(sysInsets.left, sysInsets.top, sysInsets.right, sysInsets.bottom); - // The top and bottom action bars are always within the content area. - boolean changed = applyInsets(mActionBarTop, mSystemInsets, - mActionBarExtendsIntoSystemInsets, true, true, true, false); - if (mActionBarBottom != null) { - changed |= applyInsets(mActionBarBottom, mSystemInsets, - mActionBarExtendsIntoSystemInsets, true, false, true, true); + boolean changed = false; + if (mActionBarExtendsIntoSystemInsets) { + // Don't extend into navigation bar area so the width can align with status bar + // color view. + final Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars()); + final int paddingLeft = sysInsets.left - navBarInsets.left; + final int paddingRight = sysInsets.right - navBarInsets.right; + mActionBarTop.setPadding(paddingLeft, sysInsets.top, paddingRight, 0); + changed |= setMargin( + mActionBarTop, navBarInsets.left, 0, navBarInsets.right, 0); + if (mActionBarBottom != null) { + mActionBarBottom.setPadding(paddingLeft, 0, paddingRight, sysInsets.bottom); + changed |= setMargin( + mActionBarBottom, navBarInsets.left, 0, navBarInsets.right, 0); + } + } else { + mActionBarTop.setPadding(0, 0, 0, 0); + changed |= setMargin( + mActionBarTop, sysInsets.left, sysInsets.top, sysInsets.right, 0); + if (mActionBarBottom != null) { + mActionBarBottom.setPadding(0, 0, 0, 0); + changed |= setMargin( + mActionBarTop, sysInsets.left, 0, sysInsets.right, sysInsets.bottom); + } } // Cannot use the result of computeSystemWindowInsets, because that consumes the @@ -521,7 +509,12 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar ); } } - setMargin(mContent, mContentInsets, true, true, true, true); + setMargin( + mContent, + mContentInsets.left, + mContentInsets.top, + mContentInsets.right, + mContentInsets.bottom); if (!mLastInnerInsets.equals(mInnerInsets)) { // If the inner insets have changed, we need to dispatch this down to diff --git a/core/java/com/android/internal/widget/NotificationExpandButton.java b/core/java/com/android/internal/widget/NotificationExpandButton.java index dd12f69a56fb..42b1bdbc51b2 100644 --- a/core/java/com/android/internal/widget/NotificationExpandButton.java +++ b/core/java/com/android/internal/widget/NotificationExpandButton.java @@ -129,6 +129,16 @@ public class NotificationExpandButton extends FrameLayout { updateExpandedState(); } + /** + * Adjust the padding at the start of the view based on the layout direction (RTL/LTR). + * This is needed because RemoteViews don't have an equivalent for + * {@link this#setPaddingRelative}. + */ + @RemotableViewMethod + public void setStartPadding(int startPadding) { + setPaddingRelative(startPadding, getPaddingTop(), getPaddingEnd(), getPaddingBottom()); + } + private void updateExpandedState() { int drawableId; int contentDescriptionId; diff --git a/core/jni/android_database_SQLiteConnection.cpp b/core/jni/android_database_SQLiteConnection.cpp index 36c08a51c66a..95bab542f9d6 100644 --- a/core/jni/android_database_SQLiteConnection.cpp +++ b/core/jni/android_database_SQLiteConnection.cpp @@ -231,12 +231,6 @@ static void nativeClose(JNIEnv* env, jclass clazz, jlong connectionPtr, jboolean } } -// This method is deprecated and should be removed when it is no longer needed by the -// robolectric tests. -static void nativeClose(JNIEnv* env, jclass clazz, jlong connectionPtr) { - nativeClose(env, clazz, connectionPtr, false); -} - static void sqliteCustomScalarFunctionCallback(sqlite3_context *context, int argc, sqlite3_value **argv) { JNIEnv* env = AndroidRuntime::getJNIEnv(); @@ -974,8 +968,6 @@ static const JNINativeMethod sMethods[] = (void*)nativeOpen }, { "nativeClose", "(JZ)V", (void*) static_cast<void(*)(JNIEnv*,jclass,jlong,jboolean)>(nativeClose) }, - { "nativeClose", "(J)V", - (void*) static_cast<void(*)(JNIEnv*,jclass,jlong)>(nativeClose) }, { "nativeRegisterCustomScalarFunction", "(JLjava/lang/String;Ljava/util/function/UnaryOperator;)V", (void*)nativeRegisterCustomScalarFunction }, { "nativeRegisterCustomAggregateFunction", "(JLjava/lang/String;Ljava/util/function/BinaryOperator;)V", diff --git a/core/jni/com_android_internal_content_FileSystemUtils.cpp b/core/jni/com_android_internal_content_FileSystemUtils.cpp index 48c92c87f54e..886b9f1ae658 100644 --- a/core/jni/com_android_internal_content_FileSystemUtils.cpp +++ b/core/jni/com_android_internal_content_FileSystemUtils.cpp @@ -201,8 +201,8 @@ bool punchHoles(const char *filePath, const uint64_t offset, return true; } -bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset, - std::vector<Elf64_Phdr> &programHeaders) { +read_elf_status_t getLoadSegmentPhdrs(const char *filePath, const uint64_t offset, + std::vector<Elf64_Phdr> &programHeaders) { // Open Elf file Elf64_Ehdr ehdr; std::ifstream inputStream(filePath, std::ifstream::in); @@ -212,13 +212,13 @@ bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset, // read executable headers inputStream.read((char *)&ehdr, sizeof(ehdr)); if (!inputStream.good()) { - return false; + return ELF_READ_ERROR; } - // only consider elf64 for punching holes + // only consider ELF64 files if (ehdr.e_ident[EI_CLASS] != ELFCLASS64) { ALOGW("Provided file is not ELF64"); - return false; + return ELF_IS_NOT_64_BIT; } // read the program headers from elf file @@ -229,7 +229,7 @@ bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset, uint64_t phOffset; if (__builtin_add_overflow(offset, programHeaderOffset, &phOffset)) { ALOGE("Overflow occurred when calculating phOffset"); - return false; + return ELF_READ_ERROR; } inputStream.seekg(phOffset); @@ -237,7 +237,7 @@ bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset, Elf64_Phdr header; inputStream.read((char *)&header, sizeof(header)); if (!inputStream.good()) { - return false; + return ELF_READ_ERROR; } if (header.p_type != PT_LOAD) { @@ -246,13 +246,14 @@ bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset, programHeaders.push_back(header); } - return true; + return ELF_READ_OK; } bool punchHolesInElf64(const char *filePath, const uint64_t offset) { std::vector<Elf64_Phdr> programHeaders; - if (!getLoadSegmentPhdrs(filePath, offset, programHeaders)) { - ALOGE("Failed to read program headers from ELF file."); + read_elf_status_t status = getLoadSegmentPhdrs(filePath, offset, programHeaders); + if (status != ELF_READ_OK) { + ALOGE("Failed to read program headers from 64 bit ELF file."); return false; } return punchHoles(filePath, offset, programHeaders); diff --git a/core/jni/com_android_internal_content_FileSystemUtils.h b/core/jni/com_android_internal_content_FileSystemUtils.h index 4a95686c5a0c..c4dc1115e6c4 100644 --- a/core/jni/com_android_internal_content_FileSystemUtils.h +++ b/core/jni/com_android_internal_content_FileSystemUtils.h @@ -22,6 +22,12 @@ namespace android { +enum read_elf_status_t { + ELF_IS_NOT_64_BIT = -2, + ELF_READ_ERROR = -1, + ELF_READ_OK = 0, +}; + /* * This function deallocates space used by zero padding at the end of LOAD segments in given * uncompressed ELF file. Read ELF headers and find out the offset and sizes of LOAD segments. @@ -39,10 +45,10 @@ bool punchHolesInElf64(const char* filePath, uint64_t offset); bool punchHolesInZip(const char* filePath, uint64_t offset, uint16_t extraFieldLen); /* - * This function reads program headers from ELF file. ELF can be specified with file path directly - * or it should be at offset inside Apk. Program headers passed to function is populated. + * This function reads program headers from 64 bit ELF file. ELF can be specified with file path + * directly or it should be at offset inside Apk. Program headers passed to function is populated. */ -bool getLoadSegmentPhdrs(const char* filePath, const uint64_t offset, - std::vector<Elf64_Phdr>& programHeaders); +read_elf_status_t getLoadSegmentPhdrs(const char* filePath, const uint64_t offset, + std::vector<Elf64_Phdr>& programHeaders); } // namespace android
\ No newline at end of file diff --git a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp index 14132e61ff0e..7cf523f18a90 100644 --- a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp +++ b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp @@ -640,7 +640,17 @@ com_android_internal_content_NativeLibraryHelper_openApkFd(JNIEnv *env, jclass, static jint checkLoadSegmentAlignment(const char* fileName, off64_t offset) { std::vector<Elf64_Phdr> programHeaders; - if (!getLoadSegmentPhdrs(fileName, offset, programHeaders)) { + read_elf_status_t status = getLoadSegmentPhdrs(fileName, offset, programHeaders); + // Ignore the ELFs which are not 64 bit. + if (status == ELF_IS_NOT_64_BIT) { + ALOGW("ELF file is not 64 Bit"); + // PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED is equivalent of skipping the current file. + // on return, flag is OR'ed with flags from other ELF files. If some app has 32 bit ELF in + // 64 bit directory, alignment of that ELF will be ignored. + return PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED; + } + + if (status == ELF_READ_ERROR) { ALOGE("Failed to read program headers from ELF file."); return PAGE_SIZE_APP_COMPAT_FLAG_ERROR; } diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index ac4bac6d206e..1caa9e7af348 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -112,7 +112,8 @@ message SecureSettingsProto { optional SettingProto autoclick_ignore_minor_cursor_movement = 63 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto autoclick_panel_position = 64 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto autoclick_revert_to_left_click = 65 [ (android.privacy).dest = DEST_AUTOMATIC ]; - + // Setting for accessibility magnification for cursor following mode. + optional SettingProto accessibility_magnification_cursor_following_mode = 66 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Accessibility accessibility = 2; diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 36b65ba43162..e16ce9849ff2 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -9292,9 +9292,6 @@ <action android:name="android.intent.action.UPDATE_PINS" /> <data android:scheme="content" android:host="*" android:mimeType="*/*" /> </intent-filter> - <intent-filter> - <action android:name="android.intent.action.BOOT_COMPLETED" /> - </intent-filter> </receiver> <receiver android:name="com.android.server.updates.IntentFirewallInstallReceiver" diff --git a/core/res/res/drawable/accessibility_autoclick_scroll_down.xml b/core/res/res/drawable/accessibility_autoclick_scroll_down.xml new file mode 100644 index 000000000000..13f1ba0dafe8 --- /dev/null +++ b/core/res/res/drawable/accessibility_autoclick_scroll_down.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/materialColorPrimary" + android:pathData="M12,20l8,-8h-5V4h-6v8H4z"/> +</vector> diff --git a/core/res/res/drawable/accessibility_autoclick_scroll_exit.xml b/core/res/res/drawable/accessibility_autoclick_scroll_exit.xml new file mode 100644 index 000000000000..e53301f25d65 --- /dev/null +++ b/core/res/res/drawable/accessibility_autoclick_scroll_exit.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/materialColorPrimary" + android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/> +</vector> diff --git a/core/res/res/drawable/accessibility_autoclick_scroll_left.xml b/core/res/res/drawable/accessibility_autoclick_scroll_left.xml new file mode 100644 index 000000000000..39475bc689d2 --- /dev/null +++ b/core/res/res/drawable/accessibility_autoclick_scroll_left.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/materialColorPrimary" + android:pathData="M4,12l8,8v-5h8v-6h-8V4z"/> +</vector> diff --git a/core/res/res/drawable/accessibility_autoclick_scroll_right.xml b/core/res/res/drawable/accessibility_autoclick_scroll_right.xml new file mode 100644 index 000000000000..bbd7b2a79459 --- /dev/null +++ b/core/res/res/drawable/accessibility_autoclick_scroll_right.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/materialColorPrimary" + android:pathData="M20,12l-8,-8v5H4v6h8v5z"/> +</vector> diff --git a/core/res/res/drawable/accessibility_autoclick_scroll_up.xml b/core/res/res/drawable/accessibility_autoclick_scroll_up.xml new file mode 100644 index 000000000000..2e2c245effa1 --- /dev/null +++ b/core/res/res/drawable/accessibility_autoclick_scroll_up.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/materialColorPrimary" + android:pathData="M12,4L4,12h5v8h6v-8h5z"/> +</vector> diff --git a/core/res/res/layout/accessibility_autoclick_scroll_panel.xml b/core/res/res/layout/accessibility_autoclick_scroll_panel.xml new file mode 100644 index 000000000000..1e093bbc7c35 --- /dev/null +++ b/core/res/res/layout/accessibility_autoclick_scroll_panel.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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/accessibility_autoclick_scroll_panel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:background="@drawable/accessibility_autoclick_type_panel_rounded_background" + android:orientation="vertical" + android:padding="16dp"> + + <!-- Up arrow --> + <LinearLayout + android:id="@+id/scroll_up_layout" + style="@style/AccessibilityAutoclickScrollPanelButtonLayoutStyle" + android:layout_gravity="center_horizontal" + android:layout_marginBottom="@dimen/accessibility_autoclick_type_panel_button_spacing"> + <ImageButton + android:id="@+id/scroll_up" + style="@style/AccessibilityAutoclickScrollPanelImageButtonStyle" + android:contentDescription="@string/accessibility_autoclick_scroll_up" + android:src="@drawable/accessibility_autoclick_scroll_up" /> + </LinearLayout> + + <!-- Middle row: Left, Exit, Right --> + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:orientation="horizontal"> + + <LinearLayout + android:id="@+id/scroll_left_layout" + style="@style/AccessibilityAutoclickScrollPanelButtonLayoutStyle" + android:layout_marginEnd="@dimen/accessibility_autoclick_type_panel_button_spacing"> + <ImageButton + android:id="@+id/scroll_left" + style="@style/AccessibilityAutoclickScrollPanelImageButtonStyle" + android:contentDescription="@string/accessibility_autoclick_scroll_left" + android:src="@drawable/accessibility_autoclick_scroll_left" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/scroll_exit_layout" + style="@style/AccessibilityAutoclickScrollPanelButtonLayoutStyle" + android:layout_marginEnd="@dimen/accessibility_autoclick_type_panel_button_spacing"> + <ImageButton + android:id="@+id/scroll_exit" + style="@style/AccessibilityAutoclickScrollPanelImageButtonStyle" + android:contentDescription="@string/accessibility_autoclick_scroll_exit" + android:src="@drawable/ic_close" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/scroll_right_layout" + style="@style/AccessibilityAutoclickScrollPanelButtonLayoutStyle"> + <ImageButton + android:id="@+id/scroll_right" + style="@style/AccessibilityAutoclickScrollPanelImageButtonStyle" + android:contentDescription="@string/accessibility_autoclick_scroll_right" + android:src="@drawable/accessibility_autoclick_scroll_right" /> + </LinearLayout> + </LinearLayout> + + <!-- Down arrow --> + <LinearLayout + android:id="@+id/scroll_down_layout" + style="@style/AccessibilityAutoclickScrollPanelButtonLayoutStyle" + android:layout_gravity="center_horizontal" + android:layout_marginTop="@dimen/accessibility_autoclick_type_panel_button_spacing"> + <ImageButton + android:id="@+id/scroll_down" + style="@style/AccessibilityAutoclickScrollPanelImageButtonStyle" + android:contentDescription="@string/accessibility_autoclick_scroll_down" + android:src="@drawable/accessibility_autoclick_scroll_down" /> + </LinearLayout> + +</LinearLayout> diff --git a/core/res/res/layout/notification_2025_expand_button.xml b/core/res/res/layout/notification_2025_expand_button.xml index 1c367544c90a..8ba844a4868b 100644 --- a/core/res/res/layout/notification_2025_expand_button.xml +++ b/core/res/res/layout/notification_2025_expand_button.xml @@ -15,6 +15,7 @@ --> <!-- extends FrameLayout --> +<!-- Note: The button's padding may be dynamically adjusted in code --> <com.android.internal.widget.NotificationExpandButton xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/expand_button" diff --git a/core/res/res/layout/notification_2025_right_icon.xml b/core/res/res/layout/notification_2025_right_icon.xml new file mode 100644 index 000000000000..24d381d10501 --- /dev/null +++ b/core/res/res/layout/notification_2025_right_icon.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> +<!-- Large icon to be used in expanded notification layouts. --> +<com.android.internal.widget.CachingIconView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/right_icon" + android:layout_width="@dimen/notification_right_icon_size" + android:layout_height="@dimen/notification_right_icon_size" + android:layout_gravity="top|end" + android:layout_marginEnd="@dimen/notification_2025_right_icon_expanded_margin_end" + android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin" + android:background="@drawable/notification_large_icon_outline" + android:clipToOutline="true" + android:importantForAccessibility="no" + android:scaleType="centerCrop" + android:maxDrawableWidth="@dimen/notification_right_icon_size" + android:maxDrawableHeight="@dimen/notification_right_icon_size" + /> diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml index d29b7af9e24e..63f32e3b3cd2 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_base.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml @@ -66,14 +66,17 @@ android:orientation="horizontal" > + <!-- + We use a smaller vertical margin than usual, to allow the content of custom views to + grow up to 48dp height when needed in collapsed notifications. + --> <LinearLayout android:id="@+id/notification_headerless_view_column" android:layout_width="0px" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_weight="1" - android:layout_marginBottom="@dimen/notification_2025_margin" - android:layout_marginTop="@dimen/notification_2025_margin" + android:layout_marginVertical="@dimen/notification_2025_reduced_margin" android:orientation="vertical" > @@ -81,6 +84,7 @@ android:id="@+id/notification_top_line" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginTop="@dimen/notification_2025_additional_margin" android:minHeight="@dimen/notification_2025_content_min_height" android:clipChildren="false" android:theme="@style/Theme.DeviceDefault.Notification" @@ -118,17 +122,10 @@ <com.android.internal.widget.NotificationVanishingFrameLayout android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/notification_2025_additional_margin" android:minHeight="@dimen/notification_headerless_line_height" > - <!-- This is the simplest way to keep this text vertically centered without - gravity="center_vertical" which causes jumpiness in expansion animations. --> - <include - layout="@layout/notification_2025_text" - android:layout_width="match_parent" - android:layout_height="@dimen/notification_text_height" - android:layout_gravity="center_vertical" - android:layout_marginTop="0dp" - /> + <include layout="@layout/notification_2025_text" /> </com.android.internal.widget.NotificationVanishingFrameLayout> <include @@ -146,9 +143,8 @@ android:layout_width="@dimen/notification_right_icon_size" android:layout_height="@dimen/notification_right_icon_size" android:layout_gravity="center_vertical|end" - android:layout_marginTop="@dimen/notification_right_icon_headerless_margin" - android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin" - android:layout_marginStart="@dimen/notification_right_icon_content_margin" + android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin" + android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin" android:background="@drawable/notification_large_icon_outline" android:clipToOutline="true" android:importantForAccessibility="no" diff --git a/core/res/res/layout/notification_2025_template_collapsed_call.xml b/core/res/res/layout/notification_2025_template_collapsed_call.xml index ee691e4d6894..fbea10d42b2b 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_call.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_call.xml @@ -128,15 +128,7 @@ android:layout_height="wrap_content" android:minHeight="@dimen/notification_headerless_line_height" > - <!-- This is the simplest way to keep this text vertically centered without - gravity="center_vertical" which causes jumpiness in expansion animations. --> - <include - layout="@layout/notification_2025_text" - android:layout_width="match_parent" - android:layout_height="@dimen/notification_text_height" - android:layout_gravity="center_vertical" - android:layout_marginTop="0dp" - /> + <include layout="@layout/notification_2025_text" /> </com.android.internal.widget.NotificationVanishingFrameLayout> </LinearLayout> @@ -147,9 +139,8 @@ android:layout_width="@dimen/notification_right_icon_size" android:layout_height="@dimen/notification_right_icon_size" android:layout_gravity="center_vertical|end" - android:layout_marginTop="@dimen/notification_right_icon_headerless_margin" - android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin" - android:layout_marginStart="@dimen/notification_right_icon_content_margin" + android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin" + android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin" android:background="@drawable/notification_large_icon_outline" android:clipToOutline="true" android:importantForAccessibility="no" diff --git a/core/res/res/layout/notification_2025_template_collapsed_conversation.xml b/core/res/res/layout/notification_2025_template_collapsed_conversation.xml index f80411103501..a6fdcd95399e 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_conversation.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_conversation.xml @@ -149,9 +149,9 @@ android:layout_width="@dimen/notification_right_icon_size" android:layout_height="@dimen/notification_right_icon_size" android:layout_gravity="center_vertical|end" - android:layout_marginTop="@dimen/notification_right_icon_headerless_margin" - android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin" - android:layout_marginStart="@dimen/notification_right_icon_content_margin" + android:layout_marginTop="@dimen/notification_2025_margin" + android:layout_marginBottom="@dimen/notification_2025_margin" + android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin" android:forceHasOverlappingRendering="false" android:spacing="0dp" android:clipChildren="false" @@ -163,9 +163,8 @@ android:layout_width="@dimen/notification_right_icon_size" android:layout_height="@dimen/notification_right_icon_size" android:layout_gravity="center_vertical|end" - android:layout_marginTop="@dimen/notification_right_icon_headerless_margin" - android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin" - android:layout_marginStart="@dimen/notification_right_icon_content_margin" + android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin" + android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin" android:background="@drawable/notification_large_icon_outline" android:clipToOutline="true" android:importantForAccessibility="no" diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml index 5beab508aecf..629af77b3dda 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_media.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml @@ -68,14 +68,17 @@ android:orientation="horizontal" > + <!-- + We use a smaller vertical margin than usual, to allow the content of custom views to + grow up to 48dp height when needed in collapsed notifications. + --> <LinearLayout android:id="@+id/notification_headerless_view_column" android:layout_width="0px" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_weight="1" - android:layout_marginBottom="@dimen/notification_2025_margin" - android:layout_marginTop="@dimen/notification_2025_margin" + android:layout_marginVertical="@dimen/notification_2025_reduced_margin" android:orientation="vertical" > @@ -83,6 +86,7 @@ android:id="@+id/notification_top_line" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginTop="@dimen/notification_2025_additional_margin" android:minHeight="@dimen/notification_headerless_line_height" android:clipChildren="false" android:theme="@style/Theme.DeviceDefault.Notification" @@ -119,17 +123,10 @@ <com.android.internal.widget.NotificationVanishingFrameLayout android:layout_width="match_parent" + android:layout_marginBottom="@dimen/notification_2025_additional_margin" android:layout_height="@dimen/notification_headerless_line_height" > - <!-- This is the simplest way to keep this text vertically centered without - gravity="center_vertical" which causes jumpiness in expansion animations. --> - <include - layout="@layout/notification_template_text" - android:layout_width="match_parent" - android:layout_height="@dimen/notification_text_height" - android:layout_gravity="center_vertical" - android:layout_marginTop="0dp" - /> + <include layout="@layout/notification_2025_text" /> </com.android.internal.widget.NotificationVanishingFrameLayout> <include @@ -147,9 +144,8 @@ android:layout_width="@dimen/notification_right_icon_size" android:layout_height="@dimen/notification_right_icon_size" android:layout_gravity="center_vertical|end" - android:layout_marginTop="@dimen/notification_right_icon_headerless_margin" - android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin" - android:layout_marginStart="@dimen/notification_right_icon_content_margin" + android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin" + android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin" android:background="@drawable/notification_large_icon_outline" android:clipToOutline="true" android:importantForAccessibility="no" diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml index d7c3263904d4..3716fa6825b3 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml @@ -159,9 +159,9 @@ android:layout_width="@dimen/notification_right_icon_size" android:layout_height="@dimen/notification_right_icon_size" android:layout_gravity="center_vertical|end" - android:layout_marginTop="@dimen/notification_right_icon_headerless_margin" - android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin" - android:layout_marginStart="@dimen/notification_right_icon_content_margin" + android:layout_marginTop="@dimen/notification_2025_margin" + android:layout_marginBottom="@dimen/notification_2025_margin" + android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin" android:forceHasOverlappingRendering="false" android:spacing="0dp" android:clipChildren="false" @@ -173,9 +173,8 @@ android:layout_width="@dimen/notification_right_icon_size" android:layout_height="@dimen/notification_right_icon_size" android:layout_gravity="center_vertical|end" - android:layout_marginTop="@dimen/notification_right_icon_headerless_margin" - android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin" - android:layout_marginStart="@dimen/notification_right_icon_content_margin" + android:layout_marginVertical="@dimen/notification_2025_right_icon_vertical_margin" + android:layout_marginStart="@dimen/notification_2025_right_icon_content_margin" android:background="@drawable/notification_large_icon_outline" android:clipToOutline="true" android:importantForAccessibility="no" diff --git a/core/res/res/layout/notification_2025_template_expanded_base.xml b/core/res/res/layout/notification_2025_template_expanded_base.xml index e12db2783191..8d99e47c5386 100644 --- a/core/res/res/layout/notification_2025_template_expanded_base.xml +++ b/core/res/res/layout/notification_2025_template_expanded_base.xml @@ -63,7 +63,7 @@ /> </LinearLayout> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> </FrameLayout> <ViewStub diff --git a/core/res/res/layout/notification_2025_template_expanded_big_picture.xml b/core/res/res/layout/notification_2025_template_expanded_big_picture.xml index fac9d1c47f41..e8e460d1b4ae 100644 --- a/core/res/res/layout/notification_2025_template_expanded_big_picture.xml +++ b/core/res/res/layout/notification_2025_template_expanded_big_picture.xml @@ -25,7 +25,7 @@ <include layout="@layout/notification_2025_template_header" /> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> <LinearLayout android:id="@+id/notification_action_list_margin_target" diff --git a/core/res/res/layout/notification_2025_template_expanded_big_text.xml b/core/res/res/layout/notification_2025_template_expanded_big_text.xml index 4a807cb674c6..b68db7f0a638 100644 --- a/core/res/res/layout/notification_2025_template_expanded_big_text.xml +++ b/core/res/res/layout/notification_2025_template_expanded_big_text.xml @@ -91,5 +91,5 @@ <include layout="@layout/notification_material_action_list" /> </com.android.internal.widget.RemeasuringLinearLayout> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> </FrameLayout> diff --git a/core/res/res/layout/notification_2025_template_expanded_call.xml b/core/res/res/layout/notification_2025_template_expanded_call.xml index bbc29664d594..7b45b55ba15b 100644 --- a/core/res/res/layout/notification_2025_template_expanded_call.xml +++ b/core/res/res/layout/notification_2025_template_expanded_call.xml @@ -68,6 +68,6 @@ </com.android.internal.widget.RemeasuringLinearLayout> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> </com.android.internal.widget.CallLayout> diff --git a/core/res/res/layout/notification_2025_template_expanded_conversation.xml b/core/res/res/layout/notification_2025_template_expanded_conversation.xml index d7e8bb3b6da2..592785d53018 100644 --- a/core/res/res/layout/notification_2025_template_expanded_conversation.xml +++ b/core/res/res/layout/notification_2025_template_expanded_conversation.xml @@ -70,6 +70,6 @@ </com.android.internal.widget.RemeasuringLinearLayout> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> </com.android.internal.widget.ConversationLayout> diff --git a/core/res/res/layout/notification_2025_template_expanded_inbox.xml b/core/res/res/layout/notification_2025_template_expanded_inbox.xml index ccab02e312cc..6459e1eab862 100644 --- a/core/res/res/layout/notification_2025_template_expanded_inbox.xml +++ b/core/res/res/layout/notification_2025_template_expanded_inbox.xml @@ -130,5 +130,5 @@ android:layout_marginTop="@dimen/notification_content_margin" /> <include layout="@layout/notification_material_action_list" /> </LinearLayout> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> </FrameLayout> diff --git a/core/res/res/layout/notification_2025_template_expanded_media.xml b/core/res/res/layout/notification_2025_template_expanded_media.xml index e90ab792581f..801e339b3a92 100644 --- a/core/res/res/layout/notification_2025_template_expanded_media.xml +++ b/core/res/res/layout/notification_2025_template_expanded_media.xml @@ -99,6 +99,6 @@ </LinearLayout> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> </com.android.internal.widget.MediaNotificationView> diff --git a/core/res/res/layout/notification_2025_template_expanded_messaging.xml b/core/res/res/layout/notification_2025_template_expanded_messaging.xml index 20abfee6a4b6..82c71527a291 100644 --- a/core/res/res/layout/notification_2025_template_expanded_messaging.xml +++ b/core/res/res/layout/notification_2025_template_expanded_messaging.xml @@ -70,6 +70,6 @@ </com.android.internal.widget.RemeasuringLinearLayout> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> </com.android.internal.widget.MessagingLayout> diff --git a/core/res/res/layout/notification_2025_template_expanded_progress.xml b/core/res/res/layout/notification_2025_template_expanded_progress.xml index 87ded8975cb0..2ff252747fd2 100644 --- a/core/res/res/layout/notification_2025_template_expanded_progress.xml +++ b/core/res/res/layout/notification_2025_template_expanded_progress.xml @@ -99,7 +99,7 @@ </LinearLayout> </LinearLayout> - <include layout="@layout/notification_template_right_icon" /> + <include layout="@layout/notification_2025_right_icon" /> </FrameLayout> <ViewStub diff --git a/core/res/res/layout/notification_2025_text.xml b/core/res/res/layout/notification_2025_text.xml index 474f6d2099c6..a725de44b0bf 100644 --- a/core/res/res/layout/notification_2025_text.xml +++ b/core/res/res/layout/notification_2025_text.xml @@ -13,14 +13,16 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> +<!-- Note that we prefer layout_gravity="center_vertical" over gravity="center_vertical", since the + latter can cause jumpiness in expansion animations. --> <com.android.internal.widget.ImageFloatingTextView xmlns:android="http://schemas.android.com/apk/res/android" style="@style/Widget.DeviceDefault.Notification.Text" android:id="@+id/text" android:layout_width="match_parent" - android:layout_height="@dimen/notification_text_height" - android:layout_gravity="top" - android:layout_marginTop="@dimen/notification_text_margin_top" + android:layout_height="wrap_content" + android:minHeight="@dimen/notification_text_height" + android:layout_gravity="center_vertical" android:ellipsize="end" android:fadingEdge="horizontal" android:gravity="top" diff --git a/core/res/res/layout/notification_template_notification_progress_bar.xml b/core/res/res/layout/notification_template_notification_progress_bar.xml index 35748962cfb2..8511d38718d1 100644 --- a/core/res/res/layout/notification_template_notification_progress_bar.xml +++ b/core/res/res/layout/notification_template_notification_progress_bar.xml @@ -20,4 +20,5 @@ android:layout_width="match_parent" android:layout_height="@dimen/notification_progress_tracker_height" style="@style/Widget.Material.Notification.NotificationProgressBar" + android:accessibilityLiveRegion="polite" /> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 9773f557dfaa..7a38dce296de 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2812,6 +2812,20 @@ <integer name="config_dreamsBatteryLevelDrainCutoff">5</integer> <!-- Limit of how long the device can remain unlocked due to attention checking. --> <integer name="config_attentionMaximumExtension">900000</integer> <!-- 15 minutes. --> + + <!-- Enables or disables the 'prevent screen timeout' feature, where when a user manually + undims the screen, the feature acquires a wakelock to prevent screen timeout. + false = disabled, true = enabled. Disabled by default. --> + <bool name="config_defaultPreventScreenTimeoutEnabled">false</bool> + <!-- Default value (in milliseconds) to prevent the screen timeout after user undims it + manually between screen dims, a sign the user is interacting with the device. --> + <integer name="config_defaultPreventScreenTimeoutForMillis">300000</integer> <!-- 5 minutes. --> + <!-- Default max duration (in milliseconds) of the time between undims to still consider them + consecutive. --> + <integer name="config_defaultMaxDurationBetweenUndimsMillis">600000</integer> <!-- 10 min. --> + <!-- Default number of user undims required to trigger preventing screen sleep. --> + <integer name="config_defaultUndimsRequired">2</integer> + <!-- Whether there is to be a chosen Dock User who is the only user allowed to dream. --> <bool name="config_dreamsOnlyEnabledForDockUser">false</bool> <!-- Whether dreams are disabled when ambient mode is suppressed. --> @@ -4170,6 +4184,11 @@ <!-- Whether device supports double tap to wake --> <bool name="config_supportDoubleTapWake">false</bool> + <!-- Whether device supports double tap to sleep. This will allow the user to enable/disable + double tap gestures in non-action areas in the lock screen and launcher workspace to go to + sleep. --> + <bool name="config_supportDoubleTapSleep">false</bool> + <!-- The RadioAccessFamilies supported by the device. Empty is viewed as "all". Only used on devices which don't support RIL_REQUEST_GET_RADIO_CAPABILITY diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index a1961aedf6b7..465e318511c6 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -390,6 +390,16 @@ <!-- The absolute size of the notification expand icon. --> <dimen name="notification_header_expand_icon_size">56dp</dimen> + <!-- Margin to allow space for the expand button when showing the right icon in expanded --> + <!-- notifications. This is equal to notification_2025_expand_button_pill_width --> + <!-- + notification_2025_margin (end padding for expand button) --> + <!-- + notification_2025_expand_button_right_icon_spacing (space between pill and icon) --> + <dimen name="notification_2025_right_icon_expanded_margin_end">52dp</dimen> + + <!-- The large icon has a smaller vertical margin than most other notification content, to --> + <!-- allow it to grow up to 48dp. --> + <dimen name="notification_2025_right_icon_vertical_margin">12dp</dimen> + <!-- the height of the expand button pill --> <dimen name="notification_expand_button_pill_height">24dp</dimen> @@ -411,15 +421,24 @@ <!-- the padding of the expand icon in the notification header --> <dimen name="notification_2025_expand_button_horizontal_icon_padding">6dp</dimen> - <!-- a smaller padding for the end of the expand button, for use when showing the number --> + <!-- smaller padding for the end of the expand icon, for use when showing the number --> <dimen name="notification_2025_expand_button_reduced_end_padding">4dp</dimen> + <!-- the space needed between the expander pill and the large icon when visible --> + <dimen name="notification_2025_expand_button_right_icon_spacing">8dp</dimen> + <!-- the size of the notification close button --> <dimen name="notification_close_button_size">16dp</dimen> <!-- Margin for all notification content --> <dimen name="notification_2025_margin">16dp</dimen> + <!-- A smaller version of the margin to be used when we need more space for the content --> + <dimen name="notification_2025_reduced_margin">12dp</dimen> + + <!-- The difference between the usual margin and the reduced margin --> + <dimen name="notification_2025_additional_margin">4dp</dimen> + <!-- Vertical margin for the headerless notification content, when content has 1 line --> <!-- 16 * 2 (margins) + 24 (1 line) = 56 (notification) --> <dimen name="notification_headerless_margin_oneline">16dp</dimen> @@ -438,7 +457,7 @@ <dimen name="notification_collapsed_height_with_summarization">156dp</dimen> <!-- Max height of a collapsed (headerless) notification with one or two lines --> - <!-- 16 * 2 (margins) + 48 (min content height) = 72 (notification) --> + <!-- 14 * 2 (reduced margins) + 48 (max collapsed content height) = 72 (notification) --> <dimen name="notification_2025_min_height">72dp</dimen> <!-- Height of a headerless notification with one line --> @@ -729,6 +748,9 @@ <!-- The accessibility autoclick panel divider height --> <dimen name="accessibility_autoclick_type_panel_divider_height">24dp</dimen> + <!-- The accessibility autoclick scroll panel button width and height --> + <dimen name="accessibility_autoclick_scroll_panel_button_size">36dp</dimen> + <!-- Margin around the various security views --> <dimen name="keyguard_muliuser_selector_margin">8dp</dimen> @@ -893,6 +915,8 @@ <dimen name="notification_right_icon_size">48dp</dimen> <!-- The margin between the right icon and the content. --> <dimen name="notification_right_icon_content_margin">12dp</dimen> + <!-- The margin between the right icon and the content. (2025 redesign version) --> + <dimen name="notification_2025_right_icon_content_margin">16dp</dimen> <!-- The top and bottom margin of the right icon in the normal notification states --> <dimen name="notification_right_icon_headerless_margin">20dp</dimen> <!-- The top margin of the right icon in the "big" notification states --> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index b3cc8837e0bf..d94d659446ac 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -6226,6 +6226,18 @@ <string name="accessibility_autoclick_pause">Pause</string> <!-- Label for autoclick position button [CHAR LIMIT=NONE] --> <string name="accessibility_autoclick_position">Position</string> + <!-- Label for autoclick scroll up button [CHAR LIMIT=NONE] --> + <string name="accessibility_autoclick_scroll_up">Scroll Up</string> + <!-- Label for autoclick scroll down button [CHAR LIMIT=NONE] --> + <string name="accessibility_autoclick_scroll_down">Scroll Down</string> + <!-- Label for autoclick scroll left button [CHAR LIMIT=NONE] --> + <string name="accessibility_autoclick_scroll_left">Scroll Left</string> + <!-- Label for autoclick scroll right button [CHAR LIMIT=NONE] --> + <string name="accessibility_autoclick_scroll_right">Scroll Right</string> + <!-- Label for autoclick scroll exit button [CHAR LIMIT=NONE] --> + <string name="accessibility_autoclick_scroll_exit">Exit Scroll Mode</string> + <!-- Label for autoclick scroll panel title [CHAR LIMIT=NONE] --> + <string name="accessibility_autoclick_scroll_panel_title">Scroll Panel</string> <!-- Text to tell the user that a package has been forced by themselves in the RESTRICTED bucket. [CHAR LIMIT=NONE] --> <string name="as_app_forced_to_restricted_bucket"> @@ -6698,6 +6710,8 @@ ul.</string> <string name="profile_label_test">Test</string> <!-- Communal profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] --> <string name="profile_label_communal">Communal</string> + <!-- Supervising profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] --> + <string name="profile_label_supervising">Supervising</string> <!-- Accessibility label for managed profile user type [CHAR LIMIT=30] --> <string name="accessibility_label_managed_profile">Work profile</string> diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml index ee1edda838fd..1c3529fa7258 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -1761,6 +1761,19 @@ please see styles_device_defaults.xml. <item name="android:tint">@color/materialColorPrimary</item> </style> + <style name="AccessibilityAutoclickScrollPanelButtonLayoutStyle"> + <item name="android:gravity">center</item> + <item name="android:layout_width">@dimen/accessibility_autoclick_scroll_panel_button_size </item> + <item name="android:layout_height">@dimen/accessibility_autoclick_scroll_panel_button_size</item> + </style> + + <style name="AccessibilityAutoclickScrollPanelImageButtonStyle"> + <item name="android:gravity">center</item> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:scaleType">center</item> + </style> + <!-- TODO(b/309578419): Make activities go edge-to-edge properly and then remove this. --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 2b7261b62c64..46d18e3d3302 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1129,6 +1129,7 @@ <java-symbol type="string" name="profile_label_work_3" /> <java-symbol type="string" name="profile_label_test" /> <java-symbol type="string" name="profile_label_communal" /> + <java-symbol type="string" name="profile_label_supervising" /> <java-symbol type="string" name="accessibility_label_managed_profile" /> <java-symbol type="string" name="accessibility_label_private_profile" /> <java-symbol type="string" name="accessibility_label_clone_profile" /> @@ -3141,6 +3142,7 @@ <java-symbol type="color" name="chooser_row_divider" /> <java-symbol type="layout" name="chooser_row_direct_share" /> <java-symbol type="bool" name="config_supportDoubleTapWake" /> + <java-symbol type="bool" name="config_supportDoubleTapSleep" /> <java-symbol type="drawable" name="ic_perm_device_info" /> <java-symbol type="string" name="config_radio_access_family" /> <java-symbol type="string" name="notification_inbox_ellipsis" /> @@ -3267,6 +3269,8 @@ <java-symbol type="dimen" name="notification_2025_content_margin_start" /> <java-symbol type="dimen" name="notification_2025_expand_button_horizontal_icon_padding" /> <java-symbol type="dimen" name="notification_2025_expand_button_reduced_end_padding" /> + <java-symbol type="dimen" name="notification_2025_expand_button_right_icon_spacing" /> + <java-symbol type="dimen" name="notification_2025_right_icon_expanded_margin_end" /> <java-symbol type="dimen" name="notification_progress_margin_horizontal" /> <java-symbol type="dimen" name="notification_header_background_height" /> <java-symbol type="dimen" name="notification_header_touchable_height" /> @@ -3957,6 +3961,7 @@ <java-symbol type="dimen" name="notification_big_picture_max_width"/> <java-symbol type="dimen" name="notification_right_icon_size"/> <java-symbol type="dimen" name="notification_right_icon_content_margin"/> + <java-symbol type="dimen" name="notification_2025_right_icon_content_margin"/> <java-symbol type="dimen" name="notification_actions_icon_drawable_size"/> <java-symbol type="dimen" name="notification_custom_view_max_image_height"/> <java-symbol type="dimen" name="notification_custom_view_max_image_width"/> @@ -4442,6 +4447,11 @@ <!-- For Attention Service --> <java-symbol type="integer" name="config_attentionMaximumExtension" /> + <java-symbol type="bool" name="config_defaultPreventScreenTimeoutEnabled" /> + <java-symbol type="integer" name="config_defaultPreventScreenTimeoutForMillis" /> + <java-symbol type="integer" name="config_defaultMaxDurationBetweenUndimsMillis" /> + <java-symbol type="integer" name="config_defaultUndimsRequired" /> + <java-symbol type="string" name="config_incidentReportApproverPackage" /> <java-symbol type="array" name="config_restrictedImagesServices" /> @@ -5666,6 +5676,32 @@ <java-symbol type="drawable" name="accessibility_autoclick_resume" /> <java-symbol type="drawable" name="ic_accessibility_autoclick" /> + <!-- Accessibility autoclick scroll panel related --> + <java-symbol type="layout" name="accessibility_autoclick_scroll_panel" /> + <java-symbol type="dimen" name="accessibility_autoclick_scroll_panel_button_size" /> + <java-symbol type="drawable" name="accessibility_autoclick_scroll_up" /> + <java-symbol type="drawable" name="accessibility_autoclick_scroll_down" /> + <java-symbol type="drawable" name="accessibility_autoclick_scroll_left" /> + <java-symbol type="drawable" name="accessibility_autoclick_scroll_right" /> + <java-symbol type="drawable" name="accessibility_autoclick_scroll_exit" /> + <java-symbol type="string" name="accessibility_autoclick_scroll_up" /> + <java-symbol type="string" name="accessibility_autoclick_scroll_down" /> + <java-symbol type="string" name="accessibility_autoclick_scroll_left" /> + <java-symbol type="string" name="accessibility_autoclick_scroll_right" /> + <java-symbol type="string" name="accessibility_autoclick_scroll_exit" /> + <java-symbol type="string" name="accessibility_autoclick_scroll_panel_title" /> + <java-symbol type="id" name="accessibility_autoclick_scroll_panel" /> + <java-symbol type="id" name="scroll_up_layout" /> + <java-symbol type="id" name="scroll_down_layout" /> + <java-symbol type="id" name="scroll_left_layout" /> + <java-symbol type="id" name="scroll_right_layout" /> + <java-symbol type="id" name="scroll_exit_layout" /> + <java-symbol type="id" name="scroll_up" /> + <java-symbol type="id" name="scroll_down" /> + <java-symbol type="id" name="scroll_left" /> + <java-symbol type="id" name="scroll_right" /> + <java-symbol type="id" name="scroll_exit" /> + <!-- For HapticFeedbackConstants configurability defined at HapticFeedbackCustomization --> <java-symbol type="string" name="config_hapticFeedbackCustomizationFile" /> <java-symbol type="xml" name="haptic_feedback_customization" /> diff --git a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java index 33a46d0fde17..6d86bd209a3d 100644 --- a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java +++ b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java @@ -20,6 +20,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import android.os.Handler; +import android.os.Looper; import android.util.PollingCheck; import android.view.View; @@ -486,6 +488,42 @@ public class AnimatorSetCallsTest { }); } + @Test + public void testCancelOnPendingEndListener() throws Throwable { + final CountDownLatch endLatch = new CountDownLatch(1); + final Handler handler = new Handler(Looper.getMainLooper()); + final boolean[] endCalledRightAfterCancel = new boolean[2]; + final AnimatorSet set = new AnimatorSet(); + final ValueAnimatorTests.MyListener asListener = new ValueAnimatorTests.MyListener(); + final ValueAnimatorTests.MyListener vaListener = new ValueAnimatorTests.MyListener(); + final ValueAnimator va = new ValueAnimator(); + va.setFloatValues(0f, 1f); + va.setDuration(30); + va.addUpdateListener(animation -> { + if (animation.getAnimatedFraction() == 1f) { + handler.post(() -> { + set.cancel(); + endCalledRightAfterCancel[0] = vaListener.endCalled; + endCalledRightAfterCancel[1] = asListener.endCalled; + endLatch.countDown(); + }); + } + }); + set.addListener(asListener); + va.addListener(vaListener); + set.play(va); + + ValueAnimator.setPostNotifyEndListenerEnabled(true); + try { + handler.post(set::start); + assertTrue(endLatch.await(1, TimeUnit.SECONDS)); + assertTrue(endCalledRightAfterCancel[0]); + assertTrue(endCalledRightAfterCancel[1]); + } finally { + ValueAnimator.setPostNotifyEndListenerEnabled(false); + } + } + private void waitForOnUiThread(PollingCheck.PollingCheckCondition condition) { final boolean[] value = new boolean[1]; PollingCheck.waitFor(() -> { diff --git a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java index 04698465e971..a55909f0c193 100644 --- a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java +++ b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java @@ -922,6 +922,36 @@ public class ValueAnimatorTests { } @Test + public void testCancelOnPendingEndListener() throws Throwable { + final CountDownLatch endLatch = new CountDownLatch(1); + final Handler handler = new Handler(Looper.getMainLooper()); + final boolean[] endCalledRightAfterCancel = new boolean[1]; + final MyListener listener = new MyListener(); + final ValueAnimator va = new ValueAnimator(); + va.setFloatValues(0f, 1f); + va.setDuration(30); + va.addUpdateListener(animation -> { + if (animation.getAnimatedFraction() == 1f) { + handler.post(() -> { + va.cancel(); + endCalledRightAfterCancel[0] = listener.endCalled; + endLatch.countDown(); + }); + } + }); + va.addListener(listener); + + ValueAnimator.setPostNotifyEndListenerEnabled(true); + try { + handler.post(va::start); + assertThat(endLatch.await(1, TimeUnit.SECONDS)).isTrue(); + assertThat(endCalledRightAfterCancel[0]).isTrue(); + } finally { + ValueAnimator.setPostNotifyEndListenerEnabled(false); + } + } + + @Test public void testZeroDuration() throws Throwable { // Run two animators with zero duration, with one running forward and the other one // backward. Check that the animations start and finish with the correct end fractions. @@ -1182,6 +1212,7 @@ public class ValueAnimatorTests { assertEquals(A1_START_VALUE, a1.getAnimatedValue()); }); } + class MyUpdateListener implements ValueAnimator.AnimatorUpdateListener { boolean wasRunning = false; long firstRunningFrameTime = -1; @@ -1207,7 +1238,7 @@ public class ValueAnimatorTests { } } - class MyListener implements Animator.AnimatorListener { + static class MyListener implements Animator.AnimatorListener { boolean startCalled = false; boolean cancelCalled = false; boolean endCalled = false; diff --git a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java index ee4761b9d024..dccbf4036b3e 100644 --- a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java +++ b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java @@ -245,6 +245,12 @@ public class RegisteredServicesCacheTest extends AndroidTestCase { SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME, new TestSerializer()); } + TestServicesCache(Injector<TestServiceType> injector, + XmlSerializerAndParser<TestServiceType> serializerAndParser) { + super(injector, SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME, + serializerAndParser); + } + @Override public TestServiceType parseServiceAttributes(Resources res, String packageName, AttributeSet attrs) { diff --git a/core/tests/coretests/src/android/text/LayoutTest.java b/core/tests/coretests/src/android/text/LayoutTest.java index 9e78af57b470..11ec9f8e1912 100644 --- a/core/tests/coretests/src/android/text/LayoutTest.java +++ b/core/tests/coretests/src/android/text/LayoutTest.java @@ -16,6 +16,9 @@ package android.text; +import static android.text.Layout.HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR; +import static android.text.Layout.HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP; + import static com.android.graphics.hwui.flags.Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT; import static org.junit.Assert.assertArrayEquals; @@ -1073,6 +1076,68 @@ public class LayoutTest { } } + @Test + @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) + public void highContrastTextEnabled_testRoundedRectSize_belowMinimum_usesMinimumValue() { + mTextPaint.setColor(Color.BLACK); + mTextPaint.setTextSize(8); // Value chosen so that N * RADIUS_FACTOR < RADIUS_MIN_DP + Layout layout = new StaticLayout("Test text", mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false); + + MockCanvas c = new MockCanvas(/* width= */ 256, /* height= */ 256); + c.setHighContrastTextEnabled(true); + layout.draw( + c, + /* highlightPaths= */ null, + /* highlightPaints= */ null, + /* selectionPath= */ null, + /* selectionPaint= */ null, + /* cursorOffsetVertical= */ 0 + ); + + final float expectedRoundedRectSize = + mTextPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP; + List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands(); + for (int i = 0; i < drawCommands.size(); i++) { + MockCanvas.DrawCommand drawCommand = drawCommands.get(i); + if (drawCommand.rect != null) { + expect.that(drawCommand.rX).isEqualTo(expectedRoundedRectSize); + expect.that(drawCommand.rY).isEqualTo(expectedRoundedRectSize); + } + } + } + + @Test + @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT) + public void highContrastTextEnabled_testRoundedRectSize_aboveMinimum_usesScaledValue() { + mTextPaint.setColor(Color.BLACK); + mTextPaint.setTextSize(50); // Value chosen so that N * RADIUS_FACTOR > RADIUS_MIN_DP + Layout layout = new StaticLayout("Test text", mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false); + + MockCanvas c = new MockCanvas(/* width= */ 256, /* height= */ 256); + c.setHighContrastTextEnabled(true); + layout.draw( + c, + /* highlightPaths= */ null, + /* highlightPaints= */ null, + /* selectionPath= */ null, + /* selectionPaint= */ null, + /* cursorOffsetVertical= */ 0 + ); + + final float expectedRoundedRectSize = + mTextPaint.getTextSize() * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR; + List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands(); + for (int i = 0; i < drawCommands.size(); i++) { + MockCanvas.DrawCommand drawCommand = drawCommands.get(i); + if (drawCommand.rect != null) { + expect.that(drawCommand.rX).isEqualTo(expectedRoundedRectSize); + expect.that(drawCommand.rY).isEqualTo(expectedRoundedRectSize); + } + } + } + private int removeAlpha(int color) { return Color.rgb( Color.red(color), @@ -1087,6 +1152,8 @@ public class LayoutTest { public final String text; public final float x; public final float y; + public final float rX; + public final float rY; public final Path path; public final RectF rect; public final Paint paint; @@ -1098,6 +1165,8 @@ public class LayoutTest { this.paint = new Paint(paint); path = null; rect = null; + this.rX = 0; + this.rY = 0; } DrawCommand(Path path, Paint paint) { @@ -1107,15 +1176,19 @@ public class LayoutTest { x = 0; text = null; rect = null; + this.rX = 0; + this.rY = 0; } - DrawCommand(RectF rect, Paint paint) { + DrawCommand(RectF rect, Paint paint, float rX, float rY) { this.rect = new RectF(rect); this.paint = new Paint(paint); path = null; y = 0; x = 0; text = null; + this.rX = rX; + this.rY = rY; } @Override @@ -1189,12 +1262,12 @@ public class LayoutTest { @Override public void drawRect(RectF rect, Paint p) { - mDrawCommands.add(new DrawCommand(rect, p)); + mDrawCommands.add(new DrawCommand(rect, p, 0, 0)); } @Override public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint) { - mDrawCommands.add(new DrawCommand(rect, paint)); + mDrawCommands.add(new DrawCommand(rect, paint, rx, ry)); } List<DrawCommand> getDrawCommands() { diff --git a/core/tests/coretests/src/android/util/ArrayMapTest.java b/core/tests/coretests/src/android/util/ArrayMapTest.java index 711ff9458e11..c7efe6f93f0d 100644 --- a/core/tests/coretests/src/android/util/ArrayMapTest.java +++ b/core/tests/coretests/src/android/util/ArrayMapTest.java @@ -14,14 +14,18 @@ package android.util; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.fail; import android.platform.test.annotations.IgnoreUnderRavenwood; +import android.platform.test.annotations.Presubmit; import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -32,6 +36,7 @@ import java.util.ConcurrentModificationException; * Unit tests for ArrayMap that don't belong in CTS. */ @LargeTest +@Presubmit @RunWith(AndroidJUnit4.class) public class ArrayMapTest { private static final String TAG = "ArrayMapTest"; @@ -51,6 +56,7 @@ public class ArrayMapTest { * @throws Exception */ @Test + @Ignore("Failing; b/399137661") @IgnoreUnderRavenwood(reason = "Long test runtime") public void testConcurrentModificationException() throws Exception { final int TEST_LEN_MS = 5000; @@ -118,4 +124,49 @@ public class ArrayMapTest { } } } + + @Test + public void testToString() { + map.put("1", "One"); + map.put("2", "Two"); + map.put("3", "Five"); + map.put("3", "Three"); + + assertThat(map.toString()).isEqualTo("{1=One, 2=Two, 3=Three}"); + assertThat(map.keySet().toString()).isEqualTo("[1, 2, 3]"); + assertThat(map.values().toString()).isEqualTo("[One, Two, Three]"); + } + + @Test + public void testToStringRecursive() { + ArrayMap<Object, Object> weird = new ArrayMap<>(); + weird.put("1", weird); + + assertThat(weird.toString()).isEqualTo("{1=(this Map)}"); + assertThat(weird.keySet().toString()).isEqualTo("[1]"); + assertThat(weird.values().toString()).isEqualTo("[{1=(this Map)}]"); + } + + @Test + public void testToStringRecursiveKeySet() { + ArrayMap<Object, Object> weird = new ArrayMap<>(); + weird.put("1", weird.keySet()); + weird.put(weird.keySet(), "2"); + + assertThat(weird.toString()).isEqualTo("{1=[1, (this KeySet)], [1, (this KeySet)]=2}"); + assertThat(weird.keySet().toString()).isEqualTo("[1, (this KeySet)]"); + assertThat(weird.values().toString()).isEqualTo("[[1, (this KeySet)], 2]"); + } + + @Test + public void testToStringRecursiveValues() { + ArrayMap<Object, Object> weird = new ArrayMap<>(); + weird.put("1", weird.values()); + weird.put(weird.values(), "2"); + + assertThat(weird.toString()).isEqualTo( + "{1=[(this ValuesCollection), 2], [(this ValuesCollection), 2]=2}"); + assertThat(weird.keySet().toString()).isEqualTo("[1, [(this ValuesCollection), 2]]"); + assertThat(weird.values().toString()).isEqualTo("[(this ValuesCollection), 2]"); + } } diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java index 4d6c30ebbe2b..215c1623a530 100644 --- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java +++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java @@ -541,24 +541,30 @@ public class WindowOnBackInvokedDispatcherTest { throws RemoteException, InterruptedException { // Setup a callback that unregisters itself after the gesture is finished but before the // progress is animated back to 0f - final AtomicBoolean unregisterOnProgressUpdate = new AtomicBoolean(false); + final AtomicBoolean unregisterOnNextCallbackInvocation = new AtomicBoolean(false); final AtomicInteger onBackInvokedCalled = new AtomicInteger(0); final CountDownLatch onBackCancelledCalled = new CountDownLatch(1); OnBackAnimationCallback onBackAnimationCallback = new OnBackAnimationCallback() { @Override public void onBackProgressed(@NonNull BackEvent backEvent) { - if (unregisterOnProgressUpdate.get()) { + if (unregisterOnNextCallbackInvocation.getAndSet(false)) { mDispatcher.unregisterOnBackInvokedCallback(this); } } @Override public void onBackInvoked() { + if (unregisterOnNextCallbackInvocation.getAndSet(false)) { + mDispatcher.unregisterOnBackInvokedCallback(this); + } onBackInvokedCalled.getAndIncrement(); } @Override public void onBackCancelled() { + if (unregisterOnNextCallbackInvocation.getAndSet(false)) { + mDispatcher.unregisterOnBackInvokedCallback(this); + } onBackCancelledCalled.countDown(); } }; @@ -572,7 +578,7 @@ public class WindowOnBackInvokedDispatcherTest { // simulate back gesture finished and onBackCancelled() called, which starts the progress // animation back to 0f. On the first progress emission, the callback will unregister itself - unregisterOnProgressUpdate.set(true); + unregisterOnNextCallbackInvocation.set(true); callbackInfo.getCallback().onBackCancelled(); waitForIdle(); onBackCancelledCalled.await(1000, TimeUnit.MILLISECONDS); diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java index db69cf2397fc..02a296892c53 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java @@ -72,7 +72,8 @@ public class ChooserActivityWorkProfileTest { private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry .getInstrumentation().getTargetContext().getUser(); - private static final UserHandle WORK_USER_HANDLE = UserHandle.of(10); + private static final UserHandle WORK_USER_HANDLE = + UserHandle.of(PERSONAL_USER_HANDLE.getIdentifier() + 1); @Rule public ActivityTestRule<ChooserWrapperActivity> mActivityRule = 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 e141f70d8abb..da25da1256b0 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -54,6 +54,8 @@ import static androidx.window.extensions.embedding.SplitPresenter.sanitizeBounds import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit; import static androidx.window.extensions.embedding.TaskFragmentContainer.OverlayContainerRestoreParams; +import static com.android.window.flags.Flags.activityEmbeddingDelayTaskFragmentFinishForActivityLaunch; + import android.annotation.CallbackExecutor; import android.app.Activity; import android.app.ActivityClient; @@ -815,11 +817,17 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE); mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */); } else if (!container.isWaitingActivityAppear()) { - // Do not finish the container before the expected activity appear until - // timeout. - mTransactionManager.getCurrentTransactionRecord() - .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE); - mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */); + if (activityEmbeddingDelayTaskFragmentFinishForActivityLaunch() + && container.hasActivityLaunchHint()) { + // If we have recently attempted to launch a new activity into this + // TaskFragment, we schedule delayed cleanup. If the new activity appears in + // this TaskFragment, we no longer need to finish the TaskFragment. + container.scheduleDelayedTaskFragmentCleanup(); + } else { + mTransactionManager.getCurrentTransactionRecord() + .setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE); + mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */); + } } } else if (wasInPip && isInPip) { // No update until exit PIP. @@ -3164,6 +3172,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // TODO(b/229680885): skip override launching TaskFragment token by split-rule options.putBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN, launchedInTaskFragment.getTaskFragmentToken()); + if (activityEmbeddingDelayTaskFragmentFinishForActivityLaunch()) { + launchedInTaskFragment.setActivityLaunchHint(); + } mCurrentIntent = intent; } else { transactionRecord.abort(); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index b3e003e7ad95..6fa855e8b6d6 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -18,6 +18,8 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static com.android.window.flags.Flags.activityEmbeddingDelayTaskFragmentFinishForActivityLaunch; + import android.app.Activity; import android.app.ActivityThread; import android.app.WindowConfiguration.WindowingMode; @@ -53,6 +55,8 @@ import java.util.Objects; class TaskFragmentContainer { private static final int APPEAR_EMPTY_TIMEOUT_MS = 3000; + private static final int DELAYED_TASK_FRAGMENT_CLEANUP_TIMEOUT_MS = 500; + /** Parcelable data of this TaskFragmentContainer. */ @NonNull private final ParcelableTaskFragmentContainerData mParcelableData; @@ -165,6 +169,18 @@ class TaskFragmentContainer { */ private boolean mLastDimOnTask; + /** The timestamp of the latest pending activity launch attempt. 0 means no pending launch. */ + private long mLastActivityLaunchTimestampMs = 0; + + /** + * The scheduled runnable for delayed TaskFragment cleanup. This is used when the TaskFragment + * becomes empty, but we expect a new activity to appear in it soon. + * + * It should be {@code null} when not scheduled. + */ + @Nullable + private Runnable mDelayedTaskFragmentCleanupRunnable; + /** * Creates a container with an existing activity that will be re-parented to it in a window * container transaction. @@ -540,6 +556,10 @@ class TaskFragmentContainer { mAppearEmptyTimeout = null; } + if (activityEmbeddingDelayTaskFragmentFinishForActivityLaunch()) { + clearActivityLaunchHintIfNecessary(mInfo, info); + } + mHasCrossProcessActivities = false; mInfo = info; if (mInfo == null || mInfo.isEmpty()) { @@ -1064,6 +1084,89 @@ class TaskFragmentContainer { return isOverlay() && mParcelableData.mAssociatedActivityToken != null; } + /** + * Indicates whether there is possibly a pending activity launching into this TaskFragment. + * + * This should only be used as a hint because we cannot reliably determine if the new activity + * is going to appear into this TaskFragment. + * + * TODO(b/293800510) improve activity launch tracking in TaskFragment. + */ + boolean hasActivityLaunchHint() { + if (mLastActivityLaunchTimestampMs == 0) { + return false; + } + if (System.currentTimeMillis() > mLastActivityLaunchTimestampMs + APPEAR_EMPTY_TIMEOUT_MS) { + // The hint has expired after APPEAR_EMPTY_TIMEOUT_MS. + mLastActivityLaunchTimestampMs = 0; + return false; + } + return true; + } + + /** Records the latest activity launch attempt. */ + void setActivityLaunchHint() { + mLastActivityLaunchTimestampMs = System.currentTimeMillis(); + } + + /** + * If we get a new info showing that the TaskFragment has more activities than the previous + * info, we clear the new activity launch hint. + * + * Note that this is not a reliable way and cannot cover situations when the attempted + * activity launch did not cause TaskFragment info activity count changes, such as trampoline + * launches or single top launches. + * + * TODO(b/293800510) improve activity launch tracking in TaskFragment. + */ + private void clearActivityLaunchHintIfNecessary( + @Nullable TaskFragmentInfo oldInfo, @NonNull TaskFragmentInfo newInfo) { + final int previousActivityCount = oldInfo == null ? 0 : oldInfo.getRunningActivityCount(); + if (newInfo.getRunningActivityCount() > previousActivityCount) { + mLastActivityLaunchTimestampMs = 0; + cancelDelayedTaskFragmentCleanup(); + } + } + + /** + * Schedules delayed TaskFragment cleanup due to pending activity launch. The scheduled cleanup + * will be canceled if a new activity appears in this TaskFragment. + */ + void scheduleDelayedTaskFragmentCleanup() { + if (mDelayedTaskFragmentCleanupRunnable != null) { + // Remove the previous callback if there is already one scheduled. + mController.getHandler().removeCallbacks(mDelayedTaskFragmentCleanupRunnable); + } + mDelayedTaskFragmentCleanupRunnable = new Runnable() { + @Override + public void run() { + synchronized (mController.mLock) { + if (mDelayedTaskFragmentCleanupRunnable != this) { + // The scheduled cleanup runnable has been canceled or rescheduled, so + // skipping. + return; + } + if (isEmpty()) { + mLastActivityLaunchTimestampMs = 0; + mController.onTaskFragmentAppearEmptyTimeout( + TaskFragmentContainer.this); + } + mDelayedTaskFragmentCleanupRunnable = null; + } + } + }; + mController.getHandler().postDelayed( + mDelayedTaskFragmentCleanupRunnable, DELAYED_TASK_FRAGMENT_CLEANUP_TIMEOUT_MS); + } + + private void cancelDelayedTaskFragmentCleanup() { + if (mDelayedTaskFragmentCleanupRunnable == null) { + return; + } + mController.getHandler().removeCallbacks(mDelayedTaskFragmentCleanupRunnable); + mDelayedTaskFragmentCleanupRunnable = null; + } + @Override public String toString() { return toString(true /* includeContainersToFinishOnExit */); diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 1e72d64397d7..ab2804626361 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -194,3 +194,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enable_gsf" + namespace: "multitasking" + description: "Applies GSF font styles to multitasking." + bug: "400534660" + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt index 24f43d347163..5777cb0cc8c5 100644 --- a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt @@ -94,6 +94,7 @@ class DragZoneFactoryScreenshotTest(private val param: Param) { private val splitScreenModeName = when (splitScreenMode) { + SplitScreenMode.UNSUPPORTED -> "_split_unsupported" SplitScreenMode.NONE -> "" SplitScreenMode.SPLIT_50_50 -> "_split_50_50" SplitScreenMode.SPLIT_10_90 -> "_split_10_90" diff --git a/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp index 03076c0940a4..50666911e313 100644 --- a/libs/WindowManager/Shell/multivalentTests/Android.bp +++ b/libs/WindowManager/Shell/multivalentTests/Android.bp @@ -51,6 +51,7 @@ android_robolectric_test { "androidx.test.ext.junit", "mockito-robolectric-prebuilt", "mockito-kotlin2", + "platform-parametric-runner-lib", "truth", "flag-junit-base", "flag-junit", @@ -74,6 +75,7 @@ android_test { "frameworks-base-testutils", "mockito-kotlin2", "mockito-target-extended-minus-junit4", + "platform-parametric-runner-lib", "truth", "platform-test-annotations", "platform-test-rules", diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt new file mode 100644 index 000000000000..bdfaef2c6960 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles + +import android.content.ComponentName +import android.content.Context +import android.platform.test.flag.junit.FlagsParameterization +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.filters.SmallTest +import com.android.wm.shell.Flags +import com.android.wm.shell.taskview.TaskView +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters + +/** Tests for [BubbleExpandedView] */ +@SmallTest +@RunWith(ParameterizedAndroidJunit4::class) +class BubbleExpandedViewTest(flags: FlagsParameterization) { + + @get:Rule + val setFlagsRule = SetFlagsRule(flags) + + private val context = ApplicationProvider.getApplicationContext<Context>() + private val componentName = ComponentName(context, "TestClass") + + @Test + fun getTaskId_onTaskCreated_returnsCorrectTaskId() { + val bubbleTaskView = BubbleTaskView(mock<TaskView>(), directExecutor()) + val expandedView = BubbleExpandedView(context).apply { + initialize( + mock<BubbleExpandedViewManager>(), + mock<BubbleStackView>(), + mock<BubblePositioner>(), + false /* isOverflow */, + bubbleTaskView, + ) + setAnimating(true) // Skips setContentVisibility for testing. + } + + bubbleTaskView.listener.onTaskCreated(123, componentName) + + assertThat(expandedView.getTaskId()).isEqualTo(123) + } + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams() = FlagsParameterization.allCombinationsOf( + Flags.FLAG_ENABLE_BUBBLE_TASK_VIEW_LISTENER, + ) + } +} diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt index 9087da34d259..636ff669d6b4 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt @@ -266,8 +266,6 @@ class BubbleTaskViewListenerTest { optionsCaptor.capture(), any()) - assertThat((intentCaptor.lastValue.flags - and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue() assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue() @@ -295,8 +293,6 @@ class BubbleTaskViewListenerTest { optionsCaptor.capture(), any()) - assertThat((intentCaptor.lastValue.flags - and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue() assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue() @@ -324,8 +320,6 @@ class BubbleTaskViewListenerTest { optionsCaptor.capture(), any()) - assertThat((intentCaptor.lastValue.flags - and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue() assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue() diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 2179128df300..5ef83826840b 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -226,7 +226,7 @@ <string name="windowing_app_handle_education_tooltip">The app menu can be found here</string> <!-- App handle education tooltip text for tooltip pointing to windowing image button --> - <string name="windowing_desktop_mode_image_button_education_tooltip">Enter desktop view to open multiple apps together</string> + <string name="windowing_desktop_mode_image_button_education_tooltip">Enter desktop windowing to open 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 full screen anytime from the app menu</string> @@ -293,7 +293,7 @@ <!-- Accessibility text for the handle fullscreen button [CHAR LIMIT=NONE] --> <string name="fullscreen_text">Fullscreen</string> <!-- Accessibility text for the handle desktop button [CHAR LIMIT=NONE] --> - <string name="desktop_text">Desktop View</string> + <string name="desktop_text">Desktop windowing</string> <!-- Accessibility text for the handle split screen button [CHAR LIMIT=NONE] --> <string name="split_screen_text">Split Screen</string> <!-- Accessibility text for the handle more options button [CHAR LIMIT=NONE] --> @@ -319,7 +319,7 @@ <!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] --> <string name="collapse_menu_text">Close Menu</string> <!-- Accessibility text for the App Header's App Chip [CHAR LIMIT=NONE] --> - <string name="desktop_mode_app_header_chip_text"><xliff:g id="app_name" example="Chrome">%1$s</xliff:g> (Desktop View)</string> + <string name="desktop_mode_app_header_chip_text"><xliff:g id="app_name" example="Chrome">%1$s</xliff:g> (Desktop windowing)</string> <!-- Maximize menu maximize button string. --> <string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string> <!-- Maximize menu snap buttons string. --> @@ -348,7 +348,7 @@ <!-- Accessibility action replacement for caption handle app chip buttons [CHAR LIMIT=NONE] --> <string name="app_handle_chip_accessibility_announce">Open Menu</string> <!-- Accessibility action replacement for caption handle menu buttons [CHAR LIMIT=NONE] --> - <string name="app_handle_menu_accessibility_announce">Enter <xliff:g id="windowing_mode" example="Desktop View">%1$s</xliff:g></string> + <string name="app_handle_menu_accessibility_announce">Enter <xliff:g id="windowing_mode" example="Desktop windowing">%1$s</xliff:g></string> <!-- Accessibility action replacement for maximize menu enter snap left button [CHAR LIMIT=NONE] --> <string name="maximize_menu_talkback_action_snap_left_text">Resize window to left</string> <!-- Accessibility action replacement for maximize menu enter snap right button [CHAR LIMIT=NONE] --> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java index 01d2201a5a0c..8bcbd2a3fc9f 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java @@ -22,4 +22,11 @@ package com.android.wm.shell.shared; public class ShellSharedConstants { public static final String KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION = "extra_shell_can_hand_off_animation"; + + /** + * Defines the max screen width or height in dp for a device to be considered a small tablet. + * + * @see android.view.WindowManager#LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP + */ + public static final int SMALL_TABLET_MAX_EDGE_DP = 960; } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TypefaceUtils.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TypefaceUtils.kt new file mode 100644 index 000000000000..9bf56b075112 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TypefaceUtils.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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 android.graphics.Typeface +import android.widget.TextView +import com.android.wm.shell.Flags + +/** + * Utility class to apply a specified typeface to a [TextView]. + * + * This class provides a method, [setTypeface], + * to easily set a pre-defined font family and style to a given [TextView]. + */ +class TypefaceUtils { + + enum class FontFamily(val value: String) { + GSF_DISPLAY_LARGE("variable-display-large"), + GSF_DISPLAY_MEDIUM("variable-display-medium"), + GSF_DISPLAY_SMALL("variable-display-small"), + GSF_HEADLINE_LARGE("variable-headline-large"), + GSF_HEADLINE_MEDIUM("variable-headline-medium"), + GSF_HEADLINE_SMALL("variable-headline-small"), + GSF_TITLE_LARGE("variable-title-large"), + GSF_TITLE_MEDIUM("variable-title-medium"), + GSF_TITLE_SMALL("variable-title-small"), + GSF_LABEL_LARGE("variable-label-large"), + GSF_LABEL_MEDIUM("variable-label-medium"), + GSF_LABEL_SMALL("variable-label-small"), + GSF_BODY_LARGE("variable-body-large"), + GSF_BODY_MEDIUM("variable-body-medium"), + GSF_BODY_SMALL("variable-body-small"), + GSF_DISPLAY_LARGE_EMPHASIZED("variable-display-large-emphasized"), + GSF_DISPLAY_MEDIUM_EMPHASIZED("variable-display-medium-emphasized"), + GSF_DISPLAY_SMALL_EMPHASIZED("variable-display-small-emphasized"), + GSF_HEADLINE_LARGE_EMPHASIZED("variable-headline-large-emphasized"), + GSF_HEADLINE_MEDIUM_EMPHASIZED("variable-headline-medium-emphasized"), + GSF_HEADLINE_SMALL_EMPHASIZED("variable-headline-small-emphasized"), + GSF_TITLE_LARGE_EMPHASIZED("variable-title-large-emphasized"), + GSF_TITLE_MEDIUM_EMPHASIZED("variable-title-medium-emphasized"), + GSF_TITLE_SMALL_EMPHASIZED("variable-title-small-emphasized"), + GSF_LABEL_LARGE_EMPHASIZED("variable-label-large-emphasized"), + GSF_LABEL_MEDIUM_EMPHASIZED("variable-label-medium-emphasized"), + GSF_LABEL_SMALL_EMPHASIZED("variable-label-small-emphasized"), + GSF_BODY_LARGE_EMPHASIZED("variable-body-large-emphasized"), + GSF_BODY_MEDIUM_EMPHASIZED("variable-body-medium-emphasized"), + GSF_BODY_SMALL_EMPHASIZED("variable-body-small-emphasized"), + } + + companion object { + /** + * Sets the typeface of the provided [textView] to the specified [fontFamily] and [fontStyle]. + * + * The typeface is only applied to the [TextView] when [Flags.enableGsf] is `true`. + * If [Flags.enableGsf] is `false`, this method has no effect. + * + * @param textView The [TextView] to which the typeface should be applied. If `null`, this method does nothing. + * @param fontFamily The desired [FontFamily] for the [TextView]. + * @param fontStyle The desired font style (e.g., [Typeface.NORMAL], [Typeface.BOLD], [Typeface.ITALIC]). Defaults to [Typeface.NORMAL]. + */ + @JvmStatic + @JvmOverloads + fun setTypeface( + textView: TextView?, + fontFamily: FontFamily, + fontStyle: Int = Typeface.NORMAL, + ) { + if (!Flags.enableGsf()) return + textView?.typeface = Typeface.create(fontFamily.name, fontStyle) + } + } +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt index 1b7c9c282304..ad2671b8135d 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt @@ -26,6 +26,8 @@ import android.view.WindowInsets import android.view.WindowManager import kotlin.math.max +import com.android.wm.shell.shared.ShellSharedConstants.SMALL_TABLET_MAX_EDGE_DP + /** Contains device configuration used for positioning bubbles on the screen. */ data class DeviceConfig( val isLargeScreen: Boolean, @@ -38,7 +40,6 @@ data class DeviceConfig( companion object { private const val LARGE_SCREEN_MIN_EDGE_DP = 600 - private const val SMALL_TABLET_MAX_EDGE_DP = 960 @JvmStatic fun create(context: Context, windowManager: WindowManager): DeviceConfig { diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt index 362a5c5c3750..afeaf70c9d62 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt @@ -303,6 +303,7 @@ class DragZoneFactory( val isVerticalSplit = deviceConfig.isSmallTablet == deviceConfig.isLandscape return if (isVerticalSplit) { when (splitScreenModeChecker.getSplitScreenMode()) { + SplitScreenMode.UNSUPPORTED -> emptyList() SplitScreenMode.SPLIT_50_50, SplitScreenMode.NONE -> listOf( @@ -360,6 +361,7 @@ class DragZoneFactory( } } else { when (splitScreenModeChecker.getSplitScreenMode()) { + SplitScreenMode.UNSUPPORTED -> emptyList() SplitScreenMode.SPLIT_50_50, SplitScreenMode.NONE -> listOf( @@ -453,6 +455,7 @@ class DragZoneFactory( // vertical split drag zones are aligned with the full screen drag zone width val splitZoneLeft = windowBounds.right / 2 - fullScreenDragZoneWidth / 2 when (splitScreenModeChecker.getSplitScreenMode()) { + SplitScreenMode.UNSUPPORTED -> emptyList() SplitScreenMode.SPLIT_50_50, SplitScreenMode.NONE -> listOf( @@ -560,7 +563,8 @@ class DragZoneFactory( NONE, SPLIT_50_50, SPLIT_10_90, - SPLIT_90_10 + SPLIT_90_10, + UNSUPPORTED } fun getSplitScreenMode(): SplitScreenMode diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt index 2bb6cf4ec3aa..73277310ffe4 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt @@ -20,6 +20,7 @@ import android.content.Context import android.graphics.Canvas import android.graphics.Paint import android.graphics.RectF +import android.util.TypedValue import android.view.View import com.android.wm.shell.shared.R @@ -37,14 +38,21 @@ class DropTargetView(context: Context) : View(context) { private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = context.getColor(com.android.internal.R.color.materialColorPrimaryContainer) style = Paint.Style.STROKE - strokeWidth = context.resources.getDimensionPixelSize(R.dimen.drop_target_stroke).toFloat() + strokeWidth = 1.dpToPx() } - private val cornerRadius = context.resources.getDimensionPixelSize( - R.dimen.drop_target_radius).toFloat() + private val cornerRadius = 28.dpToPx() private val rect = RectF(0f, 0f, 0f, 0f) + // TODO b/396539130: Use shared xml resources once we can access them in launcher + private fun Int.dpToPx() = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + this.toFloat(), + context.resources.displayMetrics + ) + override fun onDraw(canvas: Canvas) { canvas.save() canvas.drawRoundRect(rect, cornerRadius, cornerRadius, rectPaint) diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/multiinstance/ManageWindowsViewContainer.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/multiinstance/ManageWindowsViewContainer.kt index 0954b52ff151..ac54ac7a9d99 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/multiinstance/ManageWindowsViewContainer.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/multiinstance/ManageWindowsViewContainer.kt @@ -48,12 +48,14 @@ abstract class ManageWindowsViewContainer( lateinit var menuView: ManageWindowsView /** Creates the base menu view and fills it with icon views. */ - fun createMenu(snapshotList: List<Pair<Int, TaskSnapshot>>, + fun createMenu(snapshotList: List<Pair<Int, TaskSnapshot?>>, onIconClickListener: ((Int) -> Unit), onOutsideClickListener: (() -> Unit)): ManageWindowsView { - val bitmapList = snapshotList.map { (index, snapshot) -> - index to Bitmap.wrapHardwareBuffer(snapshot.hardwareBuffer, snapshot.colorSpace) - } + val bitmapList = snapshotList + .filter { it.second != null } + .map { (index, snapshot) -> + index to Bitmap.wrapHardwareBuffer(snapshot!!.hardwareBuffer, snapshot.colorSpace) + } return createAndShowMenuView( bitmapList, onIconClickListener, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 80b65d025399..53dede6bd227 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -28,7 +28,7 @@ import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME; -import static com.android.systemui.Flags.predictiveBackDelayTransition; +import static com.android.systemui.Flags.predictiveBackDelayWmTransition; import static com.android.window.flags.Flags.unifyBackNavigationTransition; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; @@ -433,7 +433,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont public void onThresholdCrossed() { mThresholdCrossed = true; BackTouchTracker activeTracker = getActiveTracker(); - if (predictiveBackDelayTransition() && activeTracker != null && mActiveCallback == null + if (predictiveBackDelayWmTransition() && activeTracker != null && mActiveCallback == null && mBackGestureStarted) { startBackNavigation(activeTracker); } @@ -494,12 +494,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (swipeEdge == EDGE_NONE) { // start animation immediately for non-gestural sources (without ACTION_MOVE // events) - if (!predictiveBackDelayTransition()) { + if (!predictiveBackDelayWmTransition()) { mThresholdCrossed = true; } mPointersPilfered = true; onGestureStarted(touchX, touchY, swipeEdge); - if (predictiveBackDelayTransition()) { + if (predictiveBackDelayWmTransition()) { onThresholdCrossed(); } mShouldStartOnNextMoveEvent = false; @@ -555,7 +555,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mPostCommitAnimationInProgress = false; mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable); startSystemAnimation(); - } else if (!predictiveBackDelayTransition()) { + } else if (!predictiveBackDelayWmTransition()) { startBackNavigation(touchTracker); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index d9489287ff42..4f30a052de80 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -375,7 +375,7 @@ public class Bubble implements BubbleViewProvider { user, icon, BubbleType.TYPE_APP, - getAppBubbleKeyForApp(intent.getPackage(), user), + getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user), mainExecutor, bgExecutor); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 50e2f4d52bf2..3e95a0b1100f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -1604,7 +1604,7 @@ public class BubbleController implements ConfigurationChangeListener, @Nullable BubbleTransitions.DragData dragData) { if (!BubbleAnythingFlagHelper.enableBubbleToFullscreen()) return; Bubble b = mBubbleData.getOrCreateBubble(taskInfo); // Removes from overflow - ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", taskInfo.taskId); + ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - taskId=%s", taskInfo.taskId); BubbleBarLocation location = null; if (dragData != null) { location = diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 2c2451cab999..8ac9230c36c3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -238,7 +238,6 @@ public class BubbleExpandedView extends LinearLayout { mContext.createContextAsUser( mBubble.getUser(), Context.CONTEXT_RESTRICTED); Intent fillInIntent = new Intent(); - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); PendingIntent pi = PendingIntent.getActivity( context, /* requestCode= */ 0, @@ -467,6 +466,11 @@ public class BubbleExpandedView extends LinearLayout { new BubbleTaskViewListener.Callback() { @Override public void onTaskCreated() { + // The taskId is saved to use for removeTask, + // preventing appearance in recent tasks. + mTaskId = ((BubbleTaskViewListener) mCurrentTaskViewListener) + .getTaskId(); + setContentVisibility(true); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java index 63d713495177..9c20e3af9ab4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java @@ -130,7 +130,6 @@ public class BubbleTaskViewListener implements TaskView.Listener { mContext.createContextAsUser( mBubble.getUser(), Context.CONTEXT_RESTRICTED); Intent fillInIntent = new Intent(); - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); // First try get pending intent from the bubble PendingIntent pi = mBubble.getPendingIntent(); if (pi == null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java index e7e7be9cdf6b..51a5b12edb84 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java @@ -159,19 +159,22 @@ public class BubbleTransitions { private final WindowContainerTransaction mPendingWct; private final boolean mReleasedOnLeft; private final float mTaskScale; + private final float mCornerRadius; private final PointF mDragPosition; /** * @param releasedOnLeft true if the bubble was released in the left drop target * @param taskScale the scale of the task when it was dragged to bubble + * @param cornerRadius the corner radius of the task when it was dragged to bubble * @param dragPosition the position of the task when it was dragged to bubble * @param wct pending operations to be applied when finishing the drag */ - public DragData(boolean releasedOnLeft, float taskScale, @Nullable PointF dragPosition, - @Nullable WindowContainerTransaction wct) { + public DragData(boolean releasedOnLeft, float taskScale, float cornerRadius, + @Nullable PointF dragPosition, @Nullable WindowContainerTransaction wct) { mPendingWct = wct; mReleasedOnLeft = releasedOnLeft; mTaskScale = taskScale; + mCornerRadius = cornerRadius; mDragPosition = dragPosition != null ? dragPosition : new PointF(0, 0); } @@ -198,6 +201,13 @@ public class BubbleTransitions { } /** + * @return the corner radius of the task when it was dragged to bubble + */ + public float getCornerRadius() { + return mCornerRadius; + } + + /** * @return position of the task when it was dragged to bubble */ public PointF getDragPosition() { @@ -362,6 +372,7 @@ public class BubbleTransitions { (int) mDragData.getDragPosition().y); startTransaction.setScale(mSnapshot, mDragData.getTaskScale(), mDragData.getTaskScale()); + startTransaction.setCornerRadius(mSnapshot, mDragData.getCornerRadius()); } // Now update state (and talk to launcher) in parallel with snapshot stuff @@ -377,12 +388,6 @@ public class BubbleTransitions { startTransaction.setPosition(mSnapshot, left, top); startTransaction.setLayer(mSnapshot, Integer.MAX_VALUE); - BubbleBarExpandedView bbev = mBubble.getBubbleBarExpandedView(); - if (bbev != null) { - // Corners get reset during the animation. Add them back - startTransaction.setCornerRadius(mSnapshot, bbev.getRestingCornerRadius()); - } - startTransaction.apply(); mTaskViewTransitions.onExternalDone(transition); @@ -634,7 +639,7 @@ public class BubbleTransitions { @Override public void continueCollapse() { mBubble.cleanupTaskView(); - if (mTaskLeash == null) return; + if (mTaskLeash == null || !mTaskLeash.isValid()) return; SurfaceControl.Transaction t = new SurfaceControl.Transaction(); t.reparent(mTaskLeash, mRootLeash); t.apply(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index 6c840f020f90..bdb21f246359 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -447,9 +447,9 @@ public class BubbleBarLayerView extends FrameLayout bubble.cleanupViews(!inTransition); endAction.run(); }; - if (mBubbleData.getBubbles().isEmpty()) { - // we're removing the last bubble. collapse the expanded view and cleanup bubble views - // at the end. + if (mBubbleData.getBubbles().isEmpty() || inTransition) { + // If we are removing the last bubble or removing the current bubble via transition, + // collapse the expanded view and clean up bubbles at the end. collapse(cleanUp); } else { cleanUp.run(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ComponentUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ComponentUtils.kt index 0d0bc9be72b3..9fefb515a539 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ComponentUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ComponentUtils.kt @@ -25,7 +25,8 @@ import com.android.wm.shell.ShellTaskOrganizer object ComponentUtils { /** Retrieves the package name from an [Intent]. */ @JvmStatic - fun getPackageName(intent: Intent?): String? = intent?.component?.packageName + fun getPackageName(intent: Intent?): String? = + intent?.component?.packageName ?: intent?.`package` /** Retrieves the package name from a [PendingIntent]. */ @JvmStatic diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HomeIntentProvider.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HomeIntentProvider.kt new file mode 100644 index 000000000000..8751b65dce94 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HomeIntentProvider.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.common + +import android.app.ActivityManager +import android.app.ActivityOptions +import android.app.PendingIntent +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.content.Context +import android.content.Intent +import android.os.UserHandle +import android.view.Display.DEFAULT_DISPLAY +import android.window.WindowContainerTransaction +import com.android.window.flags.Flags + +/** Creates home intent **/ +class HomeIntentProvider( + private val context: Context, +) { + fun addLaunchHomePendingIntent( + wct: WindowContainerTransaction, displayId: Int, userId: Int? = null + ) { + val userHandle = + if (userId != null) UserHandle.of(userId) else UserHandle.of(ActivityManager.getCurrentUser()) + + val launchHomeIntent = Intent(Intent.ACTION_MAIN).apply { + if (displayId != DEFAULT_DISPLAY) { + addCategory(Intent.CATEGORY_SECONDARY_HOME) + } else { + addCategory(Intent.CATEGORY_HOME) + } + } + val options = ActivityOptions.makeBasic().apply { + launchWindowingMode = WINDOWING_MODE_FULLSCREEN + pendingIntentBackgroundActivityStartMode = + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS + if (Flags.enablePerDisplayDesktopWallpaperActivity()) { + launchDisplayId = displayId + } + } + val pendingIntent = PendingIntent.getActivityAsUser( + context, + /* requestCode= */ 0, + launchHomeIntent, + PendingIntent.FLAG_IMMUTABLE, + /* options= */ null, + userHandle, + ) + wct.sendPendingIntent(pendingIntent, launchHomeIntent, options.toBundle()) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java index 214e6ad455a1..aeef211ae3f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java @@ -143,6 +143,9 @@ public class PipBoundsState { */ public final Rect mCachedLauncherShelfHeightKeepClearArea = new Rect(); + private final List<OnPipComponentChangedListener> mOnPipComponentChangedListeners = + new ArrayList<>(); + // the size of the current bounds relative to the max size spec private float mBoundsScale; @@ -156,9 +159,7 @@ public class PipBoundsState { // Update the relative proportion of the bounds compared to max possible size. Max size // spec takes the aspect ratio of the bounds into account, so both width and height // scale by the same factor. - addPipExclusionBoundsChangeCallback((bounds) -> { - updateBoundsScale(); - }); + addPipExclusionBoundsChangeCallback((bounds) -> updateBoundsScale()); } /** Reloads the resources. */ @@ -341,11 +342,14 @@ public class PipBoundsState { /** Set the last {@link ComponentName} to enter PIP mode. */ public void setLastPipComponentName(@Nullable ComponentName lastPipComponentName) { final boolean changed = !Objects.equals(mLastPipComponentName, lastPipComponentName); + if (!changed) return; + clearReentryState(); + setHasUserResizedPip(false); + setHasUserMovedPip(false); + final ComponentName oldComponentName = mLastPipComponentName; mLastPipComponentName = lastPipComponentName; - if (changed) { - clearReentryState(); - setHasUserResizedPip(false); - setHasUserMovedPip(false); + for (OnPipComponentChangedListener listener : mOnPipComponentChangedListeners) { + listener.onPipComponentChanged(oldComponentName, mLastPipComponentName); } } @@ -616,6 +620,21 @@ public class PipBoundsState { } } + /** Adds callback to listen on component change. */ + public void addOnPipComponentChangedListener(@NonNull OnPipComponentChangedListener listener) { + if (!mOnPipComponentChangedListeners.contains(listener)) { + mOnPipComponentChangedListeners.add(listener); + } + } + + /** Removes callback to listen on component change. */ + public void removeOnPipComponentChangedListener( + @NonNull OnPipComponentChangedListener listener) { + if (mOnPipComponentChangedListeners.contains(listener)) { + mOnPipComponentChangedListeners.remove(listener); + } + } + public LauncherState getLauncherState() { return mLauncherState; } @@ -695,7 +714,7 @@ public class PipBoundsState { * Represents the state of pip to potentially restore upon reentry. */ @VisibleForTesting - public static final class PipReentryState { + static final class PipReentryState { private static final String TAG = PipReentryState.class.getSimpleName(); private final float mSnapFraction; @@ -722,6 +741,22 @@ public class PipBoundsState { } } + /** + * Listener interface for PiP component change, i.e. the app in pip mode changes + * TODO: Move this out of PipBoundsState once pip1 is deprecated. + */ + public interface OnPipComponentChangedListener { + /** + * Callback when the component in pip mode changes. + * @param oldPipComponent previous component in pip mode, + * {@code null} if this is the very first time PiP appears. + * @param newPipComponent new component that enters pip mode. + */ + void onPipComponentChanged( + @Nullable ComponentName oldPipComponent, + @NonNull ComponentName newPipComponent); + } + /** Dumps internal state. */ public void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index e20a3d839def..fec1f56c76bb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -1466,11 +1466,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // Freeze the configuration size with offset to prevent app get a configuration // changed or relaunch. This is required to make sure client apps will calculate // insets properly after layout shifted. - if (mTargetYOffset == 0) { - mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this); - } else { - mSplitLayoutHandler.setLayoutOffsetTarget(0, mTargetYOffset, SplitLayout.this); - } + mSplitLayoutHandler.setLayoutOffsetTarget(0, mTargetYOffset, SplitLayout.this); } // Make {@link DividerView} non-interactive while IME showing in split mode. Listen to diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index 9b11e4ab16fa..4413c8715c0d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -18,6 +18,8 @@ package com.android.wm.shell.compatui; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static com.android.wm.shell.compatui.impl.CompatUIRequestsKt.DISPLAY_COMPAT_SHOW_RESTART_DIALOG; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.TaskInfo; @@ -53,7 +55,9 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.api.CompatUIEvent; import com.android.wm.shell.compatui.api.CompatUIHandler; import com.android.wm.shell.compatui.api.CompatUIInfo; +import com.android.wm.shell.compatui.api.CompatUIRequest; import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonClicked; +import com.android.wm.shell.compatui.impl.CompatUIRequests; import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.sysui.KeyguardChangeListener; @@ -245,6 +249,21 @@ public class CompatUIController implements OnDisplaysChangedListener, mCallback = callback; } + @Override + public void sendCompatUIRequest(CompatUIRequest compatUIRequest) { + switch(compatUIRequest.getRequestId()) { + case DISPLAY_COMPAT_SHOW_RESTART_DIALOG: + handleDisplayCompatShowRestartDialog(compatUIRequest.asType()); + break; + default: + } + } + + private void handleDisplayCompatShowRestartDialog( + CompatUIRequests.DisplayCompatShowRestartDialog request) { + onRestartButtonClicked(new Pair<>(request.getTaskInfo(), request.getTaskListener())); + } + /** * Called when the Task info changed. Creates and updates the compat UI if there is an * activity in size compat, or removes the UI if there is no size compat activity. @@ -254,13 +273,17 @@ public class CompatUIController implements OnDisplaysChangedListener, public void onCompatInfoChanged(@NonNull CompatUIInfo compatUIInfo) { final TaskInfo taskInfo = compatUIInfo.getTaskInfo(); final ShellTaskOrganizer.TaskListener taskListener = compatUIInfo.getListener(); - if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()) { + final boolean isInDisplayCompatMode = + taskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove(); + if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat() + && !isInDisplayCompatMode) { mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId); } mIsInDesktopMode = isInDesktopMode(taskInfo); // We close all the Compat UI educations in case TaskInfo has no configuration or // TaskListener or in desktop mode. - if (taskInfo.configuration == null || taskListener == null || mIsInDesktopMode) { + if (taskInfo.configuration == null || taskListener == null + || (mIsInDesktopMode && !isInDisplayCompatMode)) { // Null token means the current foreground activity is not in compatibility mode. removeLayouts(taskInfo.taskId); return; @@ -552,8 +575,11 @@ public class CompatUIController implements OnDisplaysChangedListener, @Nullable ShellTaskOrganizer.TaskListener taskListener) { RestartDialogWindowManager layout = mTaskIdToRestartDialogWindowManagerMap.get(taskInfo.taskId); + final boolean isInNonDisplayCompatDesktopMode = mIsInDesktopMode + && !taskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove(); if (layout != null) { - if (layout.needsToBeRecreated(taskInfo, taskListener) || mIsInDesktopMode) { + if (layout.needsToBeRecreated(taskInfo, taskListener) + || isInNonDisplayCompatDesktopMode) { mTaskIdToRestartDialogWindowManagerMap.remove(taskInfo.taskId); layout.release(); } else { @@ -568,8 +594,9 @@ public class CompatUIController implements OnDisplaysChangedListener, return; } } - if (mIsInDesktopMode) { - // Return if in desktop mode. + if (isInNonDisplayCompatDesktopMode) { + // No restart dialog can be shown in desktop mode unless the task is in display compat + // mode. return; } // Create a new UI layout. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt index 817e554b550e..f71f8099f29f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt @@ -28,6 +28,11 @@ interface CompatUIHandler { fun onCompatInfoChanged(compatUIInfo: CompatUIInfo) /** + * Invoked when another component in Shell requests a CompatUI state change. + */ + fun sendCompatUIRequest(compatUIRequest: CompatUIRequest) + + /** * Optional reference to the object responsible to send {@link CompatUIEvent} */ fun setCallback(compatUIEventSender: Consumer<CompatUIEvent>?) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.kt new file mode 100644 index 000000000000..069fd9b062a6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.compatui.api + +/** + * Abstraction for all the possible Compat UI Component requests. + */ +interface CompatUIRequest { + /** + * Unique request identifier + */ + val requestId: Int + + @Suppress("UNCHECKED_CAST") + fun <T : CompatUIRequest> asType(): T? = this as? T + + fun <T : CompatUIRequest> asType(clazz: Class<T>): T? { + return if (clazz.isInstance(this)) clazz.cast(this) else null + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt new file mode 100644 index 000000000000..da4fc99491dc --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.compatui.impl + +import android.app.TaskInfo +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.compatui.api.CompatUIRequest + +internal const val DISPLAY_COMPAT_SHOW_RESTART_DIALOG = 0 + +/** + * All the {@link CompatUIRequest} the Compat UI Framework can handle + */ +sealed class CompatUIRequests(override val requestId: Int) : CompatUIRequest { + /** Sent when the restart handle menu is clicked, and a restart dialog is requested. */ + data class DisplayCompatShowRestartDialog(val taskInfo: TaskInfo, + val taskListener: ShellTaskOrganizer.TaskListener) : + CompatUIRequests(DISPLAY_COMPAT_SHOW_RESTART_DIALOG) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt index 02db85a4f99d..7dcb16c10097 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt @@ -23,6 +23,7 @@ import com.android.wm.shell.compatui.api.CompatUIEvent import com.android.wm.shell.compatui.api.CompatUIHandler import com.android.wm.shell.compatui.api.CompatUIInfo import com.android.wm.shell.compatui.api.CompatUIRepository +import com.android.wm.shell.compatui.api.CompatUIRequest import com.android.wm.shell.compatui.api.CompatUIState import java.util.function.Consumer @@ -102,4 +103,6 @@ class DefaultCompatUIHandler( override fun setCallback(compatUIEventSender: Consumer<CompatUIEvent>?) { this.compatUIEventSender = compatUIEventSender } + + override fun sendCompatUIRequest(compatUIRequest: CompatUIRequest) {} } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/OWNERS new file mode 100644 index 000000000000..007528e2e054 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/OWNERS @@ -0,0 +1,2 @@ +# WM shell sub-module crash handling owners +uysalorhan@google.com
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/ShellCrashHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/ShellCrashHandler.kt new file mode 100644 index 000000000000..2e34d043cbe1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/crashhandling/ShellCrashHandler.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.crashhandling + +import android.app.WindowConfiguration +import android.content.Context +import android.view.Display.DEFAULT_DISPLAY +import android.window.DesktopExperienceFlags +import android.window.WindowContainerTransaction +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.HomeIntentProvider +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.sysui.ShellInit + +/** [ShellCrashHandler] for shell to use when it's being initialized. Currently it only restores + * the home task to top. + **/ +class ShellCrashHandler( + private val context: Context, + private val shellTaskOrganizer: ShellTaskOrganizer, + private val homeIntentProvider: HomeIntentProvider, + shellInit: ShellInit, +) { + init { + shellInit.addInitCallback(::onInit, this) + } + + private fun onInit() { + handleCrashIfNeeded() + } + + private fun handleCrashIfNeeded() { + // For now only handle crashes when desktop mode is enabled on the device. + if (DesktopModeStatus.canEnterDesktopMode(context) && + !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + var freeformTaskExists = false + // If there are running tasks at init, WMShell has crashed but WMCore is still alive. + for (task in shellTaskOrganizer.getRunningTasks()) { + if (task.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM) { + freeformTaskExists = true + } + + if (freeformTaskExists) { + shellTaskOrganizer.applyTransaction( + addLaunchHomePendingIntent(WindowContainerTransaction(), DEFAULT_DISPLAY) + ) + break + } + } + } + } + + private fun addLaunchHomePendingIntent( + wct: WindowContainerTransaction, displayId: Int + ): WindowContainerTransaction { + // TODO: b/400462917 - Check that crashes are also handled correctly on HSUM devices. We + // might need to pass the [userId] here to launch the correct home. + homeIntentProvider.addLaunchHomePendingIntent(wct, displayId) + return wct + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 423d1750de75..bc2ed3f35b45 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -69,6 +69,7 @@ import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.HomeIntentProvider; import com.android.wm.shell.common.LaunchAdjacentController; import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController; import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorSurface; @@ -80,6 +81,7 @@ import com.android.wm.shell.common.UserProfileContexts; import com.android.wm.shell.common.split.SplitState; import com.android.wm.shell.compatui.letterbox.LetterboxCommandHandler; import com.android.wm.shell.compatui.letterbox.LetterboxTransitionObserver; +import com.android.wm.shell.crashhandling.ShellCrashHandler; import com.android.wm.shell.dagger.back.ShellBackAnimationModule; import com.android.wm.shell.dagger.pip.PipModule; import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler; @@ -451,7 +453,8 @@ public abstract class WMShellModule { Optional<DesktopImmersiveController> desktopImmersiveController, WindowDecorViewModel windowDecorViewModel, Optional<TaskChangeListener> taskChangeListener, - FocusTransitionObserver focusTransitionObserver) { + FocusTransitionObserver focusTransitionObserver, + Optional<DesksTransitionObserver> desksTransitionObserver) { return new FreeformTaskTransitionObserver( context, shellInit, @@ -459,7 +462,8 @@ public abstract class WMShellModule { desktopImmersiveController, windowDecorViewModel, taskChangeListener, - focusTransitionObserver); + focusTransitionObserver, + desksTransitionObserver); } @WMSingleton @@ -725,9 +729,11 @@ public abstract class WMShellModule { static DesksOrganizer provideDesksOrganizer( @NonNull ShellInit shellInit, @NonNull ShellCommandHandler shellCommandHandler, - @NonNull ShellTaskOrganizer shellTaskOrganizer + @NonNull ShellTaskOrganizer shellTaskOrganizer, + @NonNull LaunchAdjacentController launchAdjacentController ) { - return new RootTaskDesksOrganizer(shellInit, shellCommandHandler, shellTaskOrganizer); + return new RootTaskDesksOrganizer(shellInit, shellCommandHandler, shellTaskOrganizer, + launchAdjacentController); } @WMSingleton @@ -753,6 +759,7 @@ public abstract class WMShellModule { ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, DragToDesktopTransitionHandler dragToDesktopTransitionHandler, @DynamicOverride DesktopUserRepositories desktopUserRepositories, + DesktopRepositoryInitializer desktopRepositoryInitializer, Optional<DesktopImmersiveController> desktopImmersiveController, DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver, LaunchAdjacentController launchAdjacentController, @@ -772,11 +779,12 @@ public abstract class WMShellModule { Optional<BubbleController> bubbleController, OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver, DesksOrganizer desksOrganizer, - DesksTransitionObserver desksTransitionObserver, + Optional<DesksTransitionObserver> desksTransitionObserver, UserProfileContexts userProfileContexts, DesktopModeCompatPolicy desktopModeCompatPolicy, DragToDisplayTransitionHandler dragToDisplayTransitionHandler, - DesktopModeMoveToDisplayTransitionHandler moveToDisplayTransitionHandler) { + DesktopModeMoveToDisplayTransitionHandler moveToDisplayTransitionHandler, + HomeIntentProvider homeIntentProvider) { return new DesktopTasksController( context, shellInit, @@ -798,6 +806,7 @@ public abstract class WMShellModule { dragToDesktopTransitionHandler, desktopImmersiveController.get(), desktopUserRepositories, + desktopRepositoryInitializer, recentsTransitionHandler, multiInstanceHelper, mainExecutor, @@ -813,11 +822,12 @@ public abstract class WMShellModule { bubbleController, overviewToDesktopTransitionObserver, desksOrganizer, - desksTransitionObserver, + desksTransitionObserver.get(), userProfileContexts, desktopModeCompatPolicy, dragToDisplayTransitionHandler, - moveToDisplayTransitionHandler); + moveToDisplayTransitionHandler, + homeIntentProvider); } @WMSingleton @@ -874,6 +884,7 @@ public abstract class WMShellModule { Transitions transitions, @DynamicOverride DesktopUserRepositories desktopUserRepositories, ShellTaskOrganizer shellTaskOrganizer, + DesksOrganizer desksOrganizer, InteractionJankMonitor interactionJankMonitor, @ShellMainThread Handler handler) { int maxTaskLimit = DesktopModeStatus.getMaxTaskLimit(context); @@ -886,6 +897,7 @@ public abstract class WMShellModule { transitions, desktopUserRepositories, shellTaskOrganizer, + desksOrganizer, maxTaskLimit <= 0 ? null : maxTaskLimit, interactionJankMonitor, context, @@ -1215,7 +1227,6 @@ public abstract class WMShellModule { Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler, Optional<BackAnimationController> backAnimationController, DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider, - @NonNull DesksTransitionObserver desksTransitionObserver, ShellInit shellInit) { return desktopUserRepositories.flatMap( repository -> @@ -1228,17 +1239,21 @@ public abstract class WMShellModule { desktopMixedTransitionHandler.get(), backAnimationController.get(), desktopWallpaperActivityTokenProvider, - desksTransitionObserver, shellInit))); } @WMSingleton @Provides - static DesksTransitionObserver provideDesksTransitionObserver( - @NonNull @DynamicOverride DesktopUserRepositories desktopUserRepositories, + static Optional<DesksTransitionObserver> provideDesksTransitionObserver( + Context context, + @DynamicOverride DesktopUserRepositories desktopUserRepositories, @NonNull DesksOrganizer desksOrganizer ) { - return new DesksTransitionObserver(desktopUserRepositories, desksOrganizer); + if (DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) { + return Optional.of( + new DesksTransitionObserver(desktopUserRepositories, desksOrganizer)); + } + return Optional.empty(); } @WMSingleton @@ -1300,10 +1315,12 @@ public abstract class WMShellModule { static Optional<DesktopDisplayEventHandler> provideDesktopDisplayEventHandler( Context context, ShellInit shellInit, + @ShellMainThread CoroutineScope mainScope, DisplayController displayController, Optional<DesktopUserRepositories> desktopUserRepositories, Optional<DesktopTasksController> desktopTasksController, - Optional<DesktopDisplayModeController> desktopDisplayModeController + Optional<DesktopDisplayModeController> desktopDisplayModeController, + DesktopRepositoryInitializer desktopRepositoryInitializer ) { if (!DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.empty(); @@ -1312,7 +1329,9 @@ public abstract class WMShellModule { new DesktopDisplayEventHandler( context, shellInit, + mainScope, displayController, + desktopRepositoryInitializer, desktopUserRepositories.get(), desktopTasksController.get(), desktopDisplayModeController.get())); @@ -1453,7 +1472,9 @@ public abstract class WMShellModule { RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, IWindowManager windowManager, ShellTaskOrganizer shellTaskOrganizer, - DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider + DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider, + InputManager inputManager, + @ShellMainThread Handler mainHandler ) { if (!DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.empty(); @@ -1465,7 +1486,9 @@ public abstract class WMShellModule { rootTaskDisplayAreaOrganizer, windowManager, shellTaskOrganizer, - desktopWallpaperActivityTokenProvider)); + desktopWallpaperActivityTokenProvider, + inputManager, + mainHandler)); } // @@ -1547,7 +1570,8 @@ public abstract class WMShellModule { Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional, Optional<DesktopDisplayEventHandler> desktopDisplayEventHandler, Optional<DesktopModeKeyGestureHandler> desktopModeKeyGestureHandler, - Optional<SystemModalsTransitionHandler> systemModalsTransitionHandler) { + Optional<SystemModalsTransitionHandler> systemModalsTransitionHandler, + ShellCrashHandler shellCrashHandler) { return new Object(); } @@ -1567,4 +1591,20 @@ public abstract class WMShellModule { return new UserProfileContexts(context, shellController, shellInit); } + @WMSingleton + @Provides + static ShellCrashHandler provideShellCrashHandler( + Context context, + ShellTaskOrganizer shellTaskOrganizer, + HomeIntentProvider homeIntentProvider, + ShellInit shellInit) { + return new ShellCrashHandler(context, shellTaskOrganizer, homeIntentProvider, shellInit); + } + + @WMSingleton + @Provides + static HomeIntentProvider provideHomeIntentProvider(Context context) { + return new HomeIntentProvider(context); + } + } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt index afc48acad4f5..683b74392fa6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt @@ -23,15 +23,21 @@ import com.android.internal.protolog.ProtoLog import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch /** Handles display events in desktop mode */ class DesktopDisplayEventHandler( private val context: Context, shellInit: ShellInit, + private val mainScope: CoroutineScope, private val displayController: DisplayController, + private val desktopRepositoryInitializer: DesktopRepositoryInitializer, private val desktopUserRepositories: DesktopUserRepositories, private val desktopTasksController: DesktopTasksController, private val desktopDisplayModeController: DesktopDisplayModeController, @@ -61,15 +67,19 @@ class DesktopDisplayEventHandler( logV("Display #$displayId does not support desks") return } - logV("Creating new desk in new display#$displayId") - // TODO: b/362720497 - when SystemUI crashes with a freeform task open for any reason, the - // task is recreated and received in [FreeformTaskListener] before this display callback - // is invoked, which results in the repository trying to add the task to a desk before the - // desk has been recreated here, which may result in a crash-loop if the repository is - // checking that the desk exists before adding a task to it. See b/391984373. - desktopTasksController.createDesk(displayId) - // TODO: b/393978539 - consider activating the desk on creation when applicable, such as - // for connected displays. + + mainScope.launch { + desktopRepositoryInitializer.isInitialized.collect { initialized -> + if (!initialized) return@collect + if (desktopRepository.getNumberOfDesks(displayId) == 0) { + logV("Creating new desk in new display#$displayId") + // TODO: b/393978539 - consider activating the desk on creation when + // applicable, such as for connected displays. + desktopTasksController.createDesk(displayId) + } + cancel() + } + } } override fun onDisplayRemoved(displayId: Int) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt index e89aafe267ed..904d86282c39 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt @@ -22,6 +22,8 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.app.WindowConfiguration.windowingModeToString import android.content.Context +import android.hardware.input.InputManager +import android.os.Handler import android.provider.Settings import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS import android.view.Display.DEFAULT_DISPLAY @@ -29,11 +31,13 @@ import android.view.IWindowManager import android.view.WindowManager.TRANSIT_CHANGE import android.window.DesktopExperienceFlags import android.window.WindowContainerTransaction +import com.android.internal.annotations.VisibleForTesting import com.android.internal.protolog.ProtoLog import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.transition.Transitions /** Controls the display windowing mode in desktop mode */ @@ -44,8 +48,26 @@ class DesktopDisplayModeController( private val windowManager: IWindowManager, private val shellTaskOrganizer: ShellTaskOrganizer, private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, + private val inputManager: InputManager, + @ShellMainThread private val mainHandler: Handler, ) { + private val onTabletModeChangedListener = + object : InputManager.OnTabletModeChangedListener { + override fun onTabletModeChanged(whenNanos: Long, inTabletMode: Boolean) { + refreshDisplayWindowingMode() + } + } + + init { + if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) { + inputManager.registerOnTabletModeChangedListener( + onTabletModeChangedListener, + mainHandler, + ) + } + } + fun refreshDisplayWindowingMode() { if (!DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue) return @@ -89,10 +111,20 @@ class DesktopDisplayModeController( transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) } - private fun getTargetWindowingModeForDefaultDisplay(): Int { + @VisibleForTesting + fun getTargetWindowingModeForDefaultDisplay(): Int { if (isExtendedDisplayEnabled() && hasExternalDisplay()) { return WINDOWING_MODE_FREEFORM } + if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) { + if (isInClamshellMode()) { + return WINDOWING_MODE_FREEFORM + } + return WINDOWING_MODE_FULLSCREEN + } + + // If form factor-based desktop first switch is disabled, use the default display windowing + // mode here to keep the freeform mode for some form factors (e.g., FEATURE_PC). return windowManager.getWindowingMode(DEFAULT_DISPLAY) } @@ -108,6 +140,8 @@ class DesktopDisplayModeController( private fun hasExternalDisplay() = rootTaskDisplayAreaOrganizer.getDisplayIds().any { it != DEFAULT_DISPLAY } + private fun isInClamshellMode() = inputManager.isInTabletMode() == InputManager.SWITCH_STATE_OFF + private fun logV(msg: String, vararg arguments: Any?) { ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt index 1f7edb413908..4646662073e6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt @@ -452,6 +452,11 @@ class DesktopMixedTransitionHandler( private fun findTaskChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? = info.changes.firstOrNull { change -> change.taskInfo?.taskId == taskId } + private fun findLaunchChange(info: TransitionInfo): TransitionInfo.Change? = + info.changes.firstOrNull { change -> + change.mode == TRANSIT_OPEN && change.taskInfo != null && change.taskInfo!!.isFreeform + } + private fun findDesktopTaskLaunchChange( info: TransitionInfo, launchTaskId: Int?, @@ -459,14 +464,18 @@ class DesktopMixedTransitionHandler( return if (launchTaskId != null) { // Launching a known task (probably from background or moving to front), so // specifically look for it. - findTaskChange(info, launchTaskId) + val launchChange = findTaskChange(info, launchTaskId) + if ( + DesktopModeFlags.ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX.isTrue && + launchChange == null + ) { + findLaunchChange(info) + } else { + launchChange + } } else { // Launching a new task, so the first opening freeform task. - info.changes.firstOrNull { change -> - change.mode == TRANSIT_OPEN && - change.taskInfo != null && - change.taskInfo!!.isFreeform - } + findLaunchChange(info) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt index 5269318943d9..1ea545f3ab67 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt @@ -56,7 +56,6 @@ class DesktopModeKeyGestureHandler( override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?): Boolean { if ( - !isKeyGestureSupported(event.keyGestureType) || !desktopTasksController.isPresent || !desktopModeWindowDecorViewModel.isPresent ) { @@ -136,19 +135,6 @@ class DesktopModeKeyGestureHandler( } } - override fun isKeyGestureSupported(gestureType: Int): Boolean = - when (gestureType) { - KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY -> - enableMoveToNextDisplayShortcut() - KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW, - KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW, - KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW, - KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW -> - DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue && - manageKeyGestures() - else -> false - } - // TODO: b/364154795 - wait for the completion of moveToNextDisplay transition, otherwise it // will pick a wrong task when a user quickly perform other actions with keyboard shortcuts // after moveToNextDisplay, and move this to FocusTransitionObserver class. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt index fbf170f13a40..91bd3c9b6c22 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.desktopmode import android.animation.Animator +import android.animation.AnimatorSet import android.animation.ValueAnimator import android.os.IBinder import android.view.Choreographer @@ -28,9 +29,7 @@ import com.android.wm.shell.shared.animation.Interpolators import com.android.wm.shell.transition.Transitions import kotlin.time.Duration.Companion.milliseconds -/** - * Transition handler for moving a window to a different display. - */ +/** Transition handler for moving a window to a different display. */ class DesktopModeMoveToDisplayTransitionHandler( private val animationTransaction: SurfaceControl.Transaction ) : Transitions.TransitionHandler { @@ -47,46 +46,52 @@ class DesktopModeMoveToDisplayTransitionHandler( finishTransaction: SurfaceControl.Transaction, finishCallback: Transitions.TransitionFinishCallback, ): Boolean { - val change = info.changes.find { it.startDisplayId != it.endDisplayId } ?: return false - ValueAnimator.ofFloat(0f, 1f) - .apply { - duration = ANIM_DURATION.inWholeMilliseconds - interpolator = Interpolators.LINEAR - addUpdateListener { animation -> - animationTransaction - .setAlpha(change.leash, animation.animatedValue as Float) - .setFrameTimeline(Choreographer.getInstance().vsyncId) - .apply() + val changes = info.changes.filter { it.startDisplayId != it.endDisplayId } + if (changes.isEmpty()) return false + for (change in changes) { + val endBounds = change.endAbsBounds + // The position should be relative to the parent. For example, in ActivityEmbedding, the + // leash surface for the embedded Activity is parented to the container. + val endPosition = change.endRelOffset + startTransaction + .setPosition(change.leash, endPosition.x.toFloat(), endPosition.y.toFloat()) + .setWindowCrop(change.leash, endBounds.width(), endBounds.height()) + } + startTransaction.apply() + + val animator = AnimatorSet() + animator.playTogether( + changes.map { + ValueAnimator.ofFloat(0f, 1f).apply { + duration = ANIM_DURATION.inWholeMilliseconds + interpolator = Interpolators.LINEAR + addUpdateListener { animation -> + animationTransaction + .setAlpha(it.leash, animation.animatedValue as Float) + .setFrameTimeline(Choreographer.getInstance().vsyncId) + .apply() + } } - addListener( - object : Animator.AnimatorListener { - override fun onAnimationStart(animation: Animator) { - val endBounds = change.endAbsBounds - startTransaction - .setPosition( - change.leash, - endBounds.left.toFloat(), - endBounds.top.toFloat(), - ) - .setWindowCrop(change.leash, endBounds.width(), endBounds.height()) - .apply() - } + } + ) + animator.addListener( + object : Animator.AnimatorListener { + override fun onAnimationStart(animation: Animator) = Unit - override fun onAnimationEnd(animation: Animator) { - finishTransaction.apply() - finishCallback.onTransitionFinished(null) - } + override fun onAnimationEnd(animation: Animator) { + finishTransaction.apply() + finishCallback.onTransitionFinished(null) + } - override fun onAnimationCancel(animation: Animator) { - finishTransaction.apply() - finishCallback.onTransitionFinished(null) - } + override fun onAnimationCancel(animation: Animator) { + finishTransaction.apply() + finishCallback.onTransitionFinished(null) + } - override fun onAnimationRepeat(animation: Animator) = Unit - } - ) + override fun onAnimationRepeat(animation: Animator) = Unit } - .start() + ) + animator.start() return true } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt index c5ee3137e5ba..fa98d0339a65 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt @@ -22,6 +22,13 @@ import android.annotation.DimenRes import android.app.ActivityManager.RunningTaskInfo import android.app.TaskInfo import android.content.Context +import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK +import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.content.pm.ActivityInfo.LAUNCH_MULTIPLE +import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE +import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK +import android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED import android.content.pm.ActivityInfo.isFixedOrientationLandscape import android.content.pm.ActivityInfo.isFixedOrientationPortrait @@ -30,7 +37,9 @@ import android.content.res.Configuration.ORIENTATION_PORTRAIT import android.graphics.Rect import android.os.SystemProperties import android.util.Size +import android.window.DesktopModeFlags import com.android.wm.shell.R +import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import kotlin.math.ceil @@ -264,6 +273,58 @@ fun getAppHeaderHeight(context: Context): Int = @DimenRes fun getAppHeaderHeightId(): Int = R.dimen.desktop_mode_freeform_decor_caption_height /** + * Returns the task bounds a launching task should inherit from an existing running instance. + * Returns null if there are no bounds to inherit. + */ +fun getInheritedExistingTaskBounds( + taskRepository: DesktopRepository, + shellTaskOrganizer: ShellTaskOrganizer, + task: RunningTaskInfo, + deskId: Int, +): Rect? { + if (!DesktopModeFlags.INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES.isTrue) return null + val activeTask = taskRepository.getExpandedTasksIdsInDeskOrdered(deskId).firstOrNull() + if (activeTask == null) return null + val lastTask = shellTaskOrganizer.getRunningTaskInfo(activeTask) + val lastTaskTopActivity = lastTask?.topActivity + val currentTaskTopActivity = task.topActivity + val intentFlags = task.baseIntent.flags + val launchMode = task.topActivityInfo?.launchMode ?: LAUNCH_MULTIPLE + return when { + // No running task activity to inherit bounds from. + lastTaskTopActivity == null -> null + // No current top activity to set bounds for. + currentTaskTopActivity == null -> null + // Top task is not an instance of the launching activity, do not inherit its bounds. + lastTaskTopActivity.packageName != currentTaskTopActivity.packageName -> null + // Top task is an instance of launching activity. Activity will be launching in a new + // task with the existing task also being closed. Inherit existing task bounds to + // prevent new task jumping. + (isLaunchingNewTask(launchMode, intentFlags) && isClosingExitingInstance(intentFlags)) -> + lastTask.configuration.windowConfiguration.bounds + else -> null + } +} + +/** + * Returns true if the launch mode or intent will result in a new task being created for the + * activity. + */ +private fun isLaunchingNewTask(launchMode: Int, intentFlags: Int) = + launchMode == LAUNCH_SINGLE_TASK || + launchMode == LAUNCH_SINGLE_INSTANCE || + launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK || + (intentFlags and FLAG_ACTIVITY_NEW_TASK) != 0 + +/** + * Returns true if the intent will result in an existing task instance being closed if a new one + * appears. + */ +private fun isClosingExitingInstance(intentFlags: Int) = + (intentFlags and FLAG_ACTIVITY_CLEAR_TASK) != 0 || + (intentFlags and FLAG_ACTIVITY_MULTIPLE_TASK) == 0 + +/** * Calculates the desired initial bounds for applications in desktop windowing. This is done as a * scale of the screen bounds. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index 70539902f651..b3b4d59090e8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -32,6 +32,7 @@ import android.content.Context; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; +import android.view.Display; import android.view.SurfaceControl; import android.window.DesktopModeFlags; @@ -114,6 +115,7 @@ public class DesktopModeVisualIndicator { private final Context mContext; private final DisplayController mDisplayController; private final ActivityManager.RunningTaskInfo mTaskInfo; + private final Display mDisplay; private IndicatorType mCurrentType; private final DragStartState mDragStartState; @@ -145,9 +147,10 @@ public class DesktopModeVisualIndicator { mCurrentType = NO_INDICATOR; mDragStartState = dragStartState; mSnapEventHandler = snapEventHandler; + mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId); mVisualIndicatorViewContainer.createView( mContext, - mDisplayController.getDisplay(mTaskInfo.displayId), + mDisplay, mDisplayController.getDisplayLayout(mTaskInfo.displayId), mTaskInfo, taskSurface @@ -175,6 +178,7 @@ public class DesktopModeVisualIndicator { /** Start the fade-in animation. */ void fadeInIndicator() { + if (mCurrentType == NO_INDICATOR) return; mVisualIndicatorViewContainer.fadeInIndicator( mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType, mTaskInfo.displayId); @@ -193,7 +197,7 @@ public class DesktopModeVisualIndicator { if (inputCoordinates.x > layout.width()) return TO_SPLIT_RIGHT_INDICATOR; IndicatorType result; if (BubbleAnythingFlagHelper.enableBubbleToFullscreen() - && !DesktopModeStatus.canEnterDesktopMode(mContext)) { + && !DesktopModeStatus.isDesktopModeSupportedOnDisplay(mContext, mDisplay)) { // If desktop is not available, default to "no indicator" result = NO_INDICATOR; } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index bbb300ea42da..8636bc1f56c2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -235,6 +235,10 @@ class DesktopRepository( /** Returns the default desk in the given display. */ private fun getDefaultDesk(displayId: Int): Desk? = desktopData.getDefaultDesk(displayId) + /** Returns whether the given desk is active in its display. */ + fun isDeskActive(deskId: Int): Boolean = + desktopData.getAllActiveDesks().any { desk -> desk.deskId == deskId } + /** Sets the given desk as the active one in the given display. */ fun setActiveDesk(displayId: Int, deskId: Int) { logD("setActiveDesk for displayId=%d and deskId=%d", displayId, deskId) @@ -485,7 +489,7 @@ class DesktopRepository( fun getExpandedTasksOrdered(displayId: Int): List<Int> = getFreeformTasksInZOrder(displayId).filter { !isMinimizedTask(it) } - @VisibleForTesting + /** Returns all active non-minimized tasks for [deskId] ordered from top to bottom. */ fun getExpandedTasksIdsInDeskOrdered(deskId: Int): List<Int> = getFreeformTasksIdsInDeskInZOrder(deskId).filter { !isMinimizedTask(it) } @@ -712,12 +716,15 @@ class DesktopRepository( } /** - * Returns the top transparent fullscreen task id for a given display's active desk, or null. + * Returns the top transparent fullscreen task id for a given display, or null. * * TODO: b/389960283 - add explicit [deskId] argument. */ fun getTopTransparentFullscreenTaskId(displayId: Int): Int? = - desktopData.getActiveDesk(displayId)?.topTransparentFullscreenTaskId + desktopData + .desksSequence(displayId) + .mapNotNull { it.topTransparentFullscreenTaskId } + .firstOrNull() /** * Clears the top transparent fullscreen task id info for a given display's active desk. @@ -814,7 +821,6 @@ class DesktopRepository( } /** Minimizes the task in its desk. */ - @VisibleForTesting fun minimizeTaskInDesk(displayId: Int, deskId: Int, taskId: Int) { logD("MinimizeTaskInDesk: displayId=%d deskId=%d, task=%d", displayId, deskId, taskId) desktopData.getDesk(deskId)?.minimizedTasks?.add(taskId) @@ -929,6 +935,12 @@ class DesktopRepository( listener.onDeskRemoved(displayId = desk.displayId, deskId = desk.deskId) } } + if ( + DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue && + DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue + ) { + removeDeskFromPersistentRepository(desk) + } return activeTasks } @@ -1027,6 +1039,24 @@ class DesktopRepository( } } + private fun removeDeskFromPersistentRepository(desk: Desk) { + mainCoroutineScope.launch { + try { + logD( + "updatePersistentRepositoryForRemovedDesk user=%d desk=%d", + userId, + desk.deskId, + ) + persistentRepository.removeDesktop(userId = userId, desktopId = desk.deskId) + } catch (throwable: Throwable) { + logE( + "An exception occurred while updating the persistent repository \n%s", + throwable.stackTrace, + ) + } + } + } + internal fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("${prefix}DesktopRepository") @@ -1045,6 +1075,7 @@ class DesktopRepository( } .forEach { (displayId, activeDeskId, desks) -> pw.println("${prefix}Display #$displayId:") + pw.println("${innerPrefix}numOfDesks=${desks.size}") pw.println("${innerPrefix}activeDesk=$activeDeskId") pw.println("${innerPrefix}desks:") val desksPrefix = "$innerPrefix " 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 301b79afd537..d0356d55035d 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 @@ -32,6 +32,8 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.app.WindowConfiguration.WindowingMode import android.content.Context import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.graphics.Point import android.graphics.PointF import android.graphics.Rect @@ -48,6 +50,7 @@ import android.view.DragEvent import android.view.MotionEvent import android.view.SurfaceControl import android.view.SurfaceControl.Transaction +import android.view.WindowManager import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_NONE @@ -84,6 +87,7 @@ import com.android.wm.shell.bubbles.BubbleController import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.ExternalInterfaceBinder +import com.android.wm.shell.common.HomeIntentProvider import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.MultiInstanceHelper.Companion.getComponent import com.android.wm.shell.common.RemoteCallable @@ -111,6 +115,9 @@ import com.android.wm.shell.desktopmode.multidesks.DeskTransition import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener +import com.android.wm.shell.desktopmode.multidesks.createDesk +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer.DeskRecreationFactory import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE @@ -190,6 +197,7 @@ class DesktopTasksController( private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler, private val desktopImmersiveController: DesktopImmersiveController, private val userRepositories: DesktopUserRepositories, + desktopRepositoryInitializer: DesktopRepositoryInitializer, private val recentsTransitionHandler: RecentsTransitionHandler, private val multiInstanceHelper: MultiInstanceHelper, @ShellMainThread private val mainExecutor: ShellExecutor, @@ -210,6 +218,7 @@ class DesktopTasksController( private val desktopModeCompatPolicy: DesktopModeCompatPolicy, private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler, private val moveToDisplayTransitionHandler: DesktopModeMoveToDisplayTransitionHandler, + private val homeIntentProvider: HomeIntentProvider, ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler, @@ -235,6 +244,10 @@ class DesktopTasksController( removeVisualIndicator() } + override fun onTransitionInterrupted() { + removeVisualIndicator() + } + private fun removeVisualIndicator() { visualIndicator?.fadeOutIndicator { releaseVisualIndicator() } } @@ -267,6 +280,19 @@ class DesktopTasksController( } userId = ActivityManager.getCurrentUser() taskRepository = userRepositories.getProfile(userId) + + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + desktopRepositoryInitializer.deskRecreationFactory = + DeskRecreationFactory { deskUserId, destinationDisplayId, deskId -> + if (deskUserId != userId) { + // TODO: b/400984250 - add multi-user support for multi-desk restoration. + logW("Tried to recreated desk of another user.") + deskId + } else { + desksOrganizer.createDesk(destinationDisplayId) + } + } + } } private fun onInit() { @@ -762,7 +788,9 @@ class DesktopTasksController( fun minimizeTask(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) { val wct = WindowContainerTransaction() - val isMinimizingToPip = taskInfo.pictureInPictureParams?.isAutoEnterEnabled() ?: false + val isMinimizingToPip = + DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue && + (taskInfo.pictureInPictureParams?.isAutoEnterEnabled() ?: false) // If task is going to PiP, start a PiP transition instead of a minimize transition if (isMinimizingToPip) { val requestInfo = @@ -961,10 +989,13 @@ class DesktopTasksController( .apply { launchWindowingMode = WINDOWING_MODE_FREEFORM } .toBundle(), ) + val deskId = taskRepository.getDeskIdForTask(taskId) ?: getDefaultDeskId(DEFAULT_DISPLAY) startLaunchTransition( TRANSIT_OPEN, wct, taskId, + deskId = deskId, + displayId = DEFAULT_DISPLAY, remoteTransition = remoteTransition, unminimizeReason = unminimizeReason, ) @@ -982,19 +1013,26 @@ class DesktopTasksController( remoteTransition: RemoteTransition? = null, unminimizeReason: UnminimizeReason = UnminimizeReason.UNKNOWN, ) { - logV("moveTaskToFront taskId=%s", taskInfo.taskId) + val deskId = + taskRepository.getDeskIdForTask(taskInfo.taskId) ?: getDefaultDeskId(taskInfo.displayId) + logV("moveTaskToFront taskId=%s deskId=%s", taskInfo.taskId, deskId) // If a task is tiled, another task should be brought to foreground with it so let // tiling controller handle the request. if (snapEventHandler.moveTaskToFrontIfTiled(taskInfo)) { return } val wct = WindowContainerTransaction() - wct.reorder(taskInfo.token, /* onTop= */ true, /* includingParents= */ true) + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + desksOrganizer.reorderTaskToFront(wct, deskId, taskInfo) + } else { + wct.reorder(taskInfo.token, /* onTop= */ true, /* includingParents= */ true) + } startLaunchTransition( transitionType = TRANSIT_TO_FRONT, wct = wct, launchingTaskId = taskInfo.taskId, remoteTransition = remoteTransition, + deskId = deskId, displayId = taskInfo.displayId, unminimizeReason = unminimizeReason, ) @@ -1006,14 +1044,22 @@ class DesktopTasksController( wct: WindowContainerTransaction, launchingTaskId: Int?, remoteTransition: RemoteTransition? = null, - displayId: Int = DEFAULT_DISPLAY, + deskId: Int, + displayId: Int, unminimizeReason: UnminimizeReason = UnminimizeReason.UNKNOWN, ): IBinder { + logV( + "startLaunchTransition type=%s launchingTaskId=%d deskId=%d displayId=%d", + WindowManager.transitTypeToString(transitionType), + launchingTaskId, + deskId, + displayId, + ) // TODO: b/397619806 - Consolidate sharable logic with [handleFreeformTaskLaunch]. var launchTransaction = wct val taskIdToMinimize = addAndGetMinimizeChanges( - displayId, + deskId, launchTransaction, newTaskId = launchingTaskId, launchingNewIntent = launchingTaskId == null, @@ -1027,21 +1073,20 @@ class DesktopTasksController( ) var activationRunOnTransitStart: RunOnTransitStart? = null val shouldActivateDesk = - (DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue || - DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) && - !isDesktopModeShowing(displayId) + when { + DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue -> + !taskRepository.isDeskActive(deskId) + DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue -> { + !isDesktopModeShowing(displayId) + } + else -> false + } if (shouldActivateDesk) { - val deskIdToActivate = - checkNotNull( - launchingTaskId?.let { taskRepository.getDeskIdForTask(it) } - ?: getDefaultDeskId(displayId) - ) val activateDeskWct = WindowContainerTransaction() // TODO: b/391485148 - pass in the launching task here to apply task-limit policy, // but make sure to not do it twice since it is also done at the start of this // function. - activationRunOnTransitStart = - addDeskActivationChanges(deskIdToActivate, activateDeskWct) + activationRunOnTransitStart = addDeskActivationChanges(deskId, activateDeskWct) // Desk activation must be handled before app launch-related transactions. activateDeskWct.merge(launchTransaction, /* transfer= */ true) launchTransaction = activateDeskWct @@ -1152,7 +1197,14 @@ class DesktopTasksController( } wct.sendPendingIntent(pendingIntent, intent, ops.toBundle()) - startLaunchTransition(TRANSIT_OPEN, wct, launchingTaskId = null) + val deskId = getDefaultDeskId(displayId) + startLaunchTransition( + TRANSIT_OPEN, + wct, + launchingTaskId = null, + deskId = deskId, + displayId = displayId, + ) } /** @@ -1169,6 +1221,11 @@ class DesktopTasksController( return } + if (splitScreenController.isTaskInSplitScreen(task.taskId)) { + moveSplitPairToDisplay(task, displayId) + return + } + val wct = WindowContainerTransaction() val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId) if (displayAreaInfo == null) { @@ -1176,39 +1233,6 @@ class DesktopTasksController( return } - // check if the task is part of splitscreen - if ( - Flags.enableNonDefaultDisplaySplit() && - Flags.enableMoveToNextDisplayShortcut() && - splitScreenController.isTaskInSplitScreen(task.taskId) - ) { - val activeDeskId = taskRepository.getActiveDeskId(displayId) - logV("moveToDisplay: moving split root to displayId=%d", displayId) - val stageCoordinatorRootTaskToken = - splitScreenController.multiDisplayProvider.getDisplayRootForDisplayId( - DEFAULT_DISPLAY - ) - wct.reparent(stageCoordinatorRootTaskToken, displayAreaInfo.token, true /* onTop */) - val deactivationRunnable = - if (activeDeskId != null) { - // Split is being placed on top of an existing desk in the target display. Make - // sure it is cleaned up. - performDesktopExitCleanUp( - wct = wct, - deskId = activeDeskId, - displayId = displayId, - willExitDesktop = true, - shouldEndUpAtHome = false, - ) - } else { - null - } - val transition = - transitions.startTransition(TRANSIT_CHANGE, wct, moveToDisplayTransitionHandler) - deactivationRunnable?.invoke(transition) - return - } - val destinationDeskId = taskRepository.getDefaultDeskId(displayId) if (destinationDeskId == null) { logW("moveToDisplay: desk not found for display: $displayId") @@ -1270,6 +1294,53 @@ class DesktopTasksController( } /** + * Move split pair associated with the [task] to display with [displayId]. + * + * No-op if task is already on that display per [RunningTaskInfo.displayId]. + */ + private fun moveSplitPairToDisplay(task: RunningTaskInfo, displayId: Int) { + if (!splitScreenController.isTaskInSplitScreen(task.taskId)) { + return + } + + if (!Flags.enableNonDefaultDisplaySplit() || !Flags.enableMoveToNextDisplayShortcut()) { + return + } + + val wct = WindowContainerTransaction() + val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId) + if (displayAreaInfo == null) { + logW("moveSplitPairToDisplay: display not found") + return + } + + val activeDeskId = taskRepository.getActiveDeskId(displayId) + logV("moveSplitPairToDisplay: moving split root to displayId=%d", displayId) + + val stageCoordinatorRootTaskToken = + splitScreenController.multiDisplayProvider.getDisplayRootForDisplayId(DEFAULT_DISPLAY) + wct.reparent(stageCoordinatorRootTaskToken, displayAreaInfo.token, true /* onTop */) + + val deactivationRunnable = + if (activeDeskId != null) { + // Split is being placed on top of an existing desk in the target display. Make + // sure it is cleaned up. + performDesktopExitCleanUp( + wct = wct, + deskId = activeDeskId, + displayId = displayId, + willExitDesktop = true, + shouldEndUpAtHome = false, + ) + } else { + null + } + val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) + deactivationRunnable?.invoke(transition) + return + } + + /** * Quick-resizes a desktop task, toggling between a fullscreen state (represented by the stable * bounds) and a free floating state (either the last saved bounds if available or the default * bounds otherwise). @@ -1664,15 +1735,7 @@ class DesktopTasksController( wct.reorder(runningTaskInfo.token, /* onTop= */ true) } else if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { // Task is not running, start it - wct.startTask( - taskId, - ActivityOptions.makeBasic() - .apply { - launchWindowingMode = WINDOWING_MODE_FREEFORM - splashScreenStyle = SPLASH_SCREEN_STYLE_ICON - } - .toBundle(), - ) + wct.startTask(taskId, createActivityOptionsForStartTask().toBundle()) } } @@ -1691,34 +1754,7 @@ class DesktopTasksController( } private fun addLaunchHomePendingIntent(wct: WindowContainerTransaction, displayId: Int) { - val userHandle = UserHandle.of(userId) - val launchHomeIntent = - Intent(Intent.ACTION_MAIN).apply { - if (displayId != DEFAULT_DISPLAY) { - addCategory(Intent.CATEGORY_SECONDARY_HOME) - } else { - addCategory(Intent.CATEGORY_HOME) - } - } - val options = - ActivityOptions.makeBasic().apply { - launchWindowingMode = WINDOWING_MODE_FULLSCREEN - pendingIntentBackgroundActivityStartMode = - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS - if (Flags.enablePerDisplayDesktopWallpaperActivity()) { - launchDisplayId = displayId - } - } - val pendingIntent = - PendingIntent.getActivityAsUser( - context, - /* requestCode= */ 0, - launchHomeIntent, - PendingIntent.FLAG_IMMUTABLE, - /* options= */ null, - userHandle, - ) - wct.sendPendingIntent(pendingIntent, launchHomeIntent, options.toBundle()) + homeIntentProvider.addLaunchHomePendingIntent(wct, displayId, userId) } private fun addWallpaperActivity(displayId: Int, wct: WindowContainerTransaction) { @@ -2103,6 +2139,7 @@ class DesktopTasksController( // TODO(b/337915660): Add a transition handler for these; animations // need updates in some cases. val baseActivity = callingTaskInfo.baseActivity ?: return + val userHandle = UserHandle.of(callingTaskInfo.userId) val fillIn: Intent = userProfileContexts .getOrCreate(callingTaskInfo.userId) @@ -2110,11 +2147,13 @@ class DesktopTasksController( .getLaunchIntentForPackage(baseActivity.packageName) ?: return fillIn.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) val launchIntent = - PendingIntent.getActivity( + PendingIntent.getActivityAsUser( context, /* requestCode= */ 0, fillIn, PendingIntent.FLAG_IMMUTABLE, + /* options= */ null, + userHandle, ) val options = createNewWindowOptions(callingTaskInfo) when (options.launchWindowingMode) { @@ -2141,10 +2180,14 @@ class DesktopTasksController( WINDOWING_MODE_FREEFORM -> { val wct = WindowContainerTransaction() wct.sendPendingIntent(launchIntent, fillIn, options.toBundle()) + val deskId = + taskRepository.getDeskIdForTask(callingTaskInfo.taskId) + ?: getDefaultDeskId(callingTaskInfo.displayId) startLaunchTransition( transitionType = TRANSIT_OPEN, wct = wct, launchingTaskId = null, + deskId = deskId, displayId = callingTaskInfo.displayId, ) } @@ -2224,6 +2267,7 @@ class DesktopTasksController( logV("skip keyguard is locked") return null } + val deskId = getDefaultDeskId(task.displayId) val wct = WindowContainerTransaction() if (shouldFreeformTaskLaunchSwitchToFullscreen(task)) { logD("Bring desktop tasks to front on transition=taskId=%d", task.taskId) @@ -2246,17 +2290,24 @@ class DesktopTasksController( runOnTransitStart?.invoke(transition) return wct } - val deskId = getDefaultDeskId(task.displayId) val runOnTransitStart = addDeskActivationChanges(deskId, wct, task) runOnTransitStart?.invoke(transition) wct.reorder(task.token, true) return wct } + val inheritedTaskBounds = + getInheritedExistingTaskBounds(taskRepository, shellTaskOrganizer, task, deskId) + if (!taskRepository.isActiveTask(task.taskId) && inheritedTaskBounds != null) { + // Inherit bounds from closing task instance to prevent application jumping different + // cascading positions. + wct.setBounds(task.token, inheritedTaskBounds) + } // TODO(b/365723620): Handle non running tasks that were launched after reboot. // If task is already visible, it must have been handled already and added to desktop mode. - // Cascade task only if it's not visible yet. + // Cascade task only if it's not visible yet and has no inherited bounds. if ( - DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue() && + inheritedTaskBounds == null && + DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue() && !taskRepository.isVisibleTask(task.taskId) ) { val displayLayout = displayController.getDisplayLayout(task.displayId) @@ -2288,7 +2339,7 @@ class DesktopTasksController( reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH, ) // 2) minimize a Task if needed. - val taskIdToMinimize = addAndGetMinimizeChanges(task.displayId, wct, task.taskId) + val taskIdToMinimize = addAndGetMinimizeChanges(deskId, wct, task.taskId) addPendingAppLaunchTransition(transition, task.taskId, taskIdToMinimize) if (taskIdToMinimize != null) { addPendingMinimizeTransition(transition, taskIdToMinimize, MinimizeReason.TASK_LIMIT) @@ -2334,7 +2385,7 @@ class DesktopTasksController( // The desk was already showing and we're launching a new Task - we // might need to minimize another Task. val taskIdToMinimize = - addAndGetMinimizeChanges(task.displayId, wct, task.taskId) + addAndGetMinimizeChanges(deskId, wct, task.taskId) taskIdToMinimize?.let { minimizingTaskId -> addPendingMinimizeTransition( transition, @@ -2492,9 +2543,17 @@ class DesktopTasksController( ) { val targetDisplayId = taskRepository.getDisplayForDesk(deskId) val displayLayout = displayController.getDisplayLayout(targetDisplayId) ?: return - val initialBounds = getInitialBounds(displayLayout, task, targetDisplayId) - if (canChangeTaskPosition(task)) { - wct.setBounds(task.token, initialBounds) + val inheritedTaskBounds = + getInheritedExistingTaskBounds(taskRepository, shellTaskOrganizer, task, deskId) + if (inheritedTaskBounds != null) { + // Inherit bounds from closing task instance to prevent application jumping different + // cascading positions. + wct.setBounds(task.token, inheritedTaskBounds) + } else { + val initialBounds = getInitialBounds(displayLayout, task, targetDisplayId) + if (canChangeTaskPosition(task)) { + wct.setBounds(task.token, initialBounds) + } } if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { desksOrganizer.moveTaskToDesk(wct = wct, deskId = deskId, task = task) @@ -2670,7 +2729,7 @@ class DesktopTasksController( /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */ private fun addAndGetMinimizeChanges( - displayId: Int, + deskId: Int, wct: WindowContainerTransaction, newTaskId: Int?, launchingNewIntent: Boolean = false, @@ -2679,7 +2738,7 @@ class DesktopTasksController( require(newTaskId == null || !launchingNewIntent) return desktopTasksLimiter .get() - .addAndGetMinimizeTaskChanges(displayId, wct, newTaskId, launchingNewIntent) + .addAndGetMinimizeTaskChanges(deskId, wct, newTaskId, launchingNewIntent) } private fun addPendingMinimizeTransition( @@ -2776,15 +2835,36 @@ class DesktopTasksController( } prepareForDeskActivation(displayId, wct) desksOrganizer.activateDesk(wct, deskId) - if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { - // TODO: 362720497 - do non-running tasks need to be restarted with |wct#startTask|? - } taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( doesAnyTaskRequireTaskbarRounding(displayId) ) - // TODO: b/362720497 - activating a desk with the intention to move a new task to - // it means we may need to minimize something in the activating desk. Do so here - // similar to how it's done in #bringDesktopAppsToFront. + val expandedTasksOrderedFrontToBack = + taskRepository.getExpandedTasksIdsInDeskOrdered(deskId = deskId) + // If we're adding a new Task we might need to minimize an old one + val taskIdToMinimize = + desktopTasksLimiter + .getOrNull() + ?.getTaskIdToMinimize(expandedTasksOrderedFrontToBack, newTaskIdInFront) + if (taskIdToMinimize != null) { + val taskToMinimize = shellTaskOrganizer.getRunningTaskInfo(taskIdToMinimize) + // TODO(b/365725441): Handle non running task minimization + if (taskToMinimize != null) { + desksOrganizer.minimizeTask(wct, deskId, taskToMinimize) + } + } + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue) { + expandedTasksOrderedFrontToBack + .filter { taskId -> taskId != taskIdToMinimize } + .reversed() + .forEach { taskId -> + val runningTaskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId) + if (runningTaskInfo == null) { + wct.startTask(taskId, createActivityOptionsForStartTask().toBundle()) + } else { + desksOrganizer.reorderTaskToFront(wct, deskId, runningTaskInfo) + } + } + } return { transition -> val activateDeskTransition = if (newTaskIdInFront != null) { @@ -2802,6 +2882,9 @@ class DesktopTasksController( ) } desksTransitionObserver.addPendingTransition(activateDeskTransition) + taskIdToMinimize?.let { minimizingTask -> + addPendingMinimizeTransition(transition, minimizingTask, MinimizeReason.TASK_LIMIT) + } } } @@ -3381,7 +3464,14 @@ class DesktopTasksController( if (windowingMode == WINDOWING_MODE_FREEFORM) { if (DesktopModeFlags.ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX.isTrue()) { // TODO b/376389593: Use a custom tab tearing transition/animation - startLaunchTransition(TRANSIT_OPEN, wct, launchingTaskId = null) + val deskId = getDefaultDeskId(DEFAULT_DISPLAY) + startLaunchTransition( + TRANSIT_OPEN, + wct, + launchingTaskId = null, + deskId = deskId, + displayId = DEFAULT_DISPLAY, + ) } else { desktopModeDragAndDropTransitionHandler.handleDropEvent(wct) } @@ -3426,6 +3516,13 @@ class DesktopTasksController( } } + private fun createActivityOptionsForStartTask(): ActivityOptions { + return ActivityOptions.makeBasic().apply { + launchWindowingMode = WINDOWING_MODE_FREEFORM + splashScreenStyle = SPLASH_SCREEN_STYLE_ICON + } + } + private fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("${prefix}DesktopTasksController") diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt index da369f094405..4ca58823b52b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -22,6 +22,7 @@ import android.os.Handler import android.os.IBinder import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_TO_BACK +import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags import android.window.TransitionInfo import android.window.WindowContainerTransaction @@ -31,6 +32,7 @@ import com.android.internal.protolog.ProtoLog import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UnminimizeReason +import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.sysui.UserChangeListener @@ -48,6 +50,7 @@ class DesktopTasksLimiter( transitions: Transitions, private val desktopUserRepositories: DesktopUserRepositories, private val shellTaskOrganizer: ShellTaskOrganizer, + private val desksOrganizer: DesksOrganizer, private val maxTasksLimit: Int?, private val interactionJankMonitor: InteractionJankMonitor, private val context: Context, @@ -258,7 +261,7 @@ class DesktopTasksLimiter( * returning the task to minimize. */ fun addAndGetMinimizeTaskChanges( - displayId: Int, + deskId: Int, wct: WindowContainerTransaction, newFrontTaskId: Int?, launchingNewIntent: Boolean = false, @@ -267,15 +270,19 @@ class DesktopTasksLimiter( val taskRepository = desktopUserRepositories.current val taskIdToMinimize = getTaskIdToMinimize( - taskRepository.getExpandedTasksOrdered(displayId), + taskRepository.getExpandedTasksIdsInDeskOrdered(deskId), newFrontTaskId, launchingNewIntent, ) - // If it's a running task, reorder it to back. taskIdToMinimize ?.let { shellTaskOrganizer.getRunningTaskInfo(it) } - // TODO: b/391485148 - this won't really work with multi-desks enabled. - ?.let { wct.reorder(it.token, /* onTop= */ false) } + ?.let { task -> + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + wct.reorder(task.token, /* onTop= */ false) + } else { + desksOrganizer.minimizeTask(wct, deskId, task) + } + } return taskIdToMinimize } 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 bd9c30e2a495..7dabeb7c9d15 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 @@ -36,7 +36,6 @@ import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.back.BackAnimationController import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.isExitDesktopModeTransition import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider -import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.desktopmode.DesktopModeStatus @@ -58,7 +57,6 @@ class DesktopTasksTransitionObserver( private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler, private val backAnimationController: BackAnimationController, private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, - private val desksTransitionObserver: DesksTransitionObserver, shellInit: ShellInit, ) : Transitions.TransitionObserver { @@ -88,7 +86,6 @@ class DesktopTasksTransitionObserver( finishTransaction: SurfaceControl.Transaction, ) { // TODO: b/332682201 Update repository state - desksTransitionObserver.onTransitionReady(transition, info) if ( DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC .isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index e943c42dcdfc..24b2e4879546 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -24,8 +24,10 @@ import android.os.SystemClock import android.os.SystemProperties import android.os.UserHandle import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction import android.view.WindowManager.TRANSIT_CLOSE import android.window.DesktopModeFlags +import android.window.DesktopModeFlags.ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX import android.window.TransitionInfo import android.window.TransitionInfo.Change import android.window.TransitionRequestInfo @@ -185,18 +187,29 @@ sealed class DragToDesktopTransitionHandler( */ fun finishDragToDesktopTransition(wct: WindowContainerTransaction): IBinder? { if (!inProgress) { + logV("finishDragToDesktop: not in progress, returning") // Don't attempt to finish a drag to desktop transition since there is no transition in // progress which means that the drag to desktop transition was never successfully // started. return null } - if (requireTransitionState().startAborted) { + val state = requireTransitionState() + if (state.startAborted) { + logV("finishDragToDesktop: start was aborted, clearing state") // Don't attempt to complete the drag-to-desktop since the start transition didn't // succeed as expected. Just reset the state as if nothing happened. clearState() return null } - return transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this) + if (state.startInterrupted) { + logV("finishDragToDesktop: start was interrupted, returning") + // We should only have interrupted the start transition after receiving a cancel/end + // request, let that existing request play out and just return here. + return null + } + state.endTransitionToken = + transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this) + return state.endTransitionToken } /** @@ -220,6 +233,11 @@ sealed class DragToDesktopTransitionHandler( clearState() return } + if (state.startInterrupted) { + // We should only have interrupted the start transition after receiving a cancel/end + // request, let that existing request play out and just return here. + return + } state.cancelState = cancelState if (state.draggedTaskChange != null && cancelState == CancelState.STANDARD_CANCEL) { @@ -227,7 +245,7 @@ sealed class DragToDesktopTransitionHandler( // transient to start and merge. Animate the cancellation (scale back to original // bounds) first before actually starting the cancel transition so that the wallpaper // is visible behind the animating task. - startCancelAnimation() + state.activeCancelAnimation = startCancelAnimation() } else if ( state.draggedTaskChange != null && (cancelState == CancelState.CANCEL_SPLIT_LEFT || @@ -255,7 +273,7 @@ sealed class DragToDesktopTransitionHandler( ) { if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) { // TODO(b/388853233): add support for dragging split task to bubble - startCancelAnimation() + state.activeCancelAnimation = startCancelAnimation() } else { // Animation is handled by BubbleController val wct = WindowContainerTransaction() @@ -327,8 +345,9 @@ sealed class DragToDesktopTransitionHandler( val taskInfo = state.draggedTaskChange?.taskInfo ?: error("Expected non-null taskInfo") val dragPosition = PointF(state.dragAnimator.position) val scale = state.dragAnimator.scale + val cornerRadius = state.dragAnimator.cornerRadius state.dragAnimator.cancelAnimator() - requestBubble(wct, taskInfo, onLeft, scale, dragPosition) + requestBubble(wct, taskInfo, onLeft, scale, cornerRadius, dragPosition) } private fun requestBubble( @@ -336,13 +355,14 @@ sealed class DragToDesktopTransitionHandler( taskInfo: RunningTaskInfo, onLeft: Boolean, taskScale: Float = 1f, + cornerRadius: Float = 0f, dragPosition: PointF = PointF(0f, 0f), ) { val controller = bubbleController.orElseThrow { IllegalStateException("BubbleController not set") } controller.expandStackAndSelectBubble( taskInfo, - BubbleTransitions.DragData(onLeft, taskScale, dragPosition, wct), + BubbleTransitions.DragData(onLeft, taskScale, cornerRadius, dragPosition, wct), ) } @@ -355,6 +375,19 @@ sealed class DragToDesktopTransitionHandler( ): Boolean { val state = requireTransitionState() + if ( + handleCancelOrExitAfterInterrupt( + transition, + info, + startTransaction, + finishTransaction, + finishCallback, + state, + ) + ) { + return true + } + val isStartDragToDesktop = info.type == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP && transition == state.startTransitionToken @@ -537,6 +570,58 @@ sealed class DragToDesktopTransitionHandler( } } + private fun handleCancelOrExitAfterInterrupt( + transition: IBinder, + info: TransitionInfo, + startTransaction: Transaction, + finishTransaction: Transaction, + finishCallback: Transitions.TransitionFinishCallback, + state: TransitionState, + ): Boolean { + if (!ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX.isTrue) { + return false + } + val isCancelDragToDesktop = + info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP && + transition == state.cancelTransitionToken + val isEndDragToDesktop = + info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP && + transition == state.endTransitionToken + // We should only receive cancel or end transitions through startAnimation() if the + // start transition was interrupted while a cancel- or end-transition had already + // been requested. Finish the cancel/end transition to avoid having to deal with more + // incoming transitions, and clear the state for the next start-drag transition. + if (!isCancelDragToDesktop && !isEndDragToDesktop) { + return false + } + if (!state.startInterrupted) { + logW( + "Not interrupted, but received startAnimation for cancel/end drag." + + "isCancel=$isCancelDragToDesktop, isEnd=$isEndDragToDesktop" + ) + return false + } + logV( + "startAnimation: interrupted -> " + + "isCancel=$isCancelDragToDesktop, isEnd=$isEndDragToDesktop" + ) + if (isEndDragToDesktop) { + setupEndDragToDesktop(info, startTransaction, finishTransaction) + animateEndDragToDesktop(startTransaction = startTransaction, finishCallback) + } else { // isCancelDragToDesktop + // Similar to when we merge the cancel transition: ensure all tasks involved in the + // cancel transition are shown, and finish the transition immediately. + info.changes.forEach { change -> + startTransaction.show(change.leash) + finishTransaction.show(change.leash) + } + } + startTransaction.apply() + finishCallback.onTransitionFinished(/* wct= */ null) + clearState() + return true + } + /** * Calculates start drag to desktop layers for transition [info]. The leash layer is calculated * based on its change position in the transition, e.g. `appLayer = appLayers - i`, where i is @@ -588,6 +673,7 @@ sealed class DragToDesktopTransitionHandler( ?: error("Start transition expected to be waiting for merge but wasn't") if (isEndTransition) { logV("mergeAnimation: end-transition, target=$mergeTarget") + state.mergedEndTransition = true setupEndDragToDesktop( info, startTransaction = startT, @@ -615,6 +701,41 @@ sealed class DragToDesktopTransitionHandler( return } logW("unhandled merge transition: transitionInfo=$info") + // Handle unknown incoming transitions by finishing the start transition. For now, only do + // this if we've already requested a cancel- or end transition. If we've already merged the + // end-transition, or if the end-transition is running on its own, then just wait until that + // finishes instead. If we've merged the cancel-transition we've finished the + // start-transition and won't reach this code. + if ( + mergeTarget == state.startTransitionToken && + isCancelOrEndTransitionRequested(state) && + !state.mergedEndTransition + ) { + interruptStartTransition(state) + } + } + + private fun isCancelOrEndTransitionRequested(state: TransitionState): Boolean = + state.cancelTransitionToken != null || state.endTransitionToken != null + + private fun interruptStartTransition(state: TransitionState) { + if (!ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX.isTrue) { + return + } + logV("interruptStartTransition") + state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null) + state.dragAnimator.cancelAnimator() + state.activeCancelAnimation?.removeAllListeners() + state.activeCancelAnimation?.cancel() + state.activeCancelAnimation = null + // Keep the transition state so we can deal with Cancel/End properly in #startAnimation. + state.startInterrupted = true + dragToDesktopStateListener?.onTransitionInterrupted() + // Cancel CUJs here as they won't be accurate now that an incoming transition is playing. + interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD) + interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE) + LatencyTracker.getInstance(context) + .onActionCancel(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG) } protected open fun setupEndDragToDesktop( @@ -781,7 +902,7 @@ sealed class DragToDesktopTransitionHandler( } ?: false } - private fun startCancelAnimation() { + private fun startCancelAnimation(): Animator { val state = requireTransitionState() val dragToDesktopAnimator = state.dragAnimator @@ -798,7 +919,7 @@ sealed class DragToDesktopTransitionHandler( val dx = targetX - x val dy = targetY - y val tx: SurfaceControl.Transaction = transactionSupplier.get() - ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f) + return ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f) .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS) .apply { addUpdateListener { animator -> @@ -816,6 +937,7 @@ sealed class DragToDesktopTransitionHandler( addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { + state.activeCancelAnimation = null dragToDesktopStateListener?.onCancelToDesktopAnimationEnd() // Start the cancel transition to restore order. startCancelDragToDesktopTransition() @@ -908,10 +1030,16 @@ sealed class DragToDesktopTransitionHandler( val dragLayer: Int, ) + /** Listener for various events happening during the DragToDesktop transition. */ interface DragToDesktopStateListener { + /** Indicates that the animation into Desktop has started. */ fun onCommitToDesktopAnimationStart() + /** Called when the animation to cancel the desktop-drag has finished. */ fun onCancelToDesktopAnimationEnd() + + /** Indicates that the drag-to-desktop transition has been interrupted. */ + fun onTransitionInterrupted() } sealed class TransitionState { @@ -928,6 +1056,10 @@ sealed class DragToDesktopTransitionHandler( abstract var cancelState: CancelState abstract var startAborted: Boolean abstract val visualIndicator: DesktopModeVisualIndicator? + abstract var startInterrupted: Boolean + abstract var endTransitionToken: IBinder? + abstract var mergedEndTransition: Boolean + abstract var activeCancelAnimation: Animator? data class FromFullscreen( override val draggedTaskId: Int, @@ -943,6 +1075,10 @@ sealed class DragToDesktopTransitionHandler( override var cancelState: CancelState = CancelState.NO_CANCEL, override var startAborted: Boolean = false, override val visualIndicator: DesktopModeVisualIndicator?, + override var startInterrupted: Boolean = false, + override var endTransitionToken: IBinder? = null, + override var mergedEndTransition: Boolean = false, + override var activeCancelAnimation: Animator? = null, var otherRootChanges: MutableList<Change> = mutableListOf(), ) : TransitionState() @@ -960,6 +1096,10 @@ sealed class DragToDesktopTransitionHandler( override var cancelState: CancelState = CancelState.NO_CANCEL, override var startAborted: Boolean = false, override val visualIndicator: DesktopModeVisualIndicator?, + override var startInterrupted: Boolean = false, + override var endTransitionToken: IBinder? = null, + override var mergedEndTransition: Boolean = false, + override var activeCancelAnimation: Animator? = null, var splitRootChange: Change? = null, var otherSplitTask: Int, ) : TransitionState() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt index fc359d7d67b6..5a988fcd1b77 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt @@ -18,6 +18,8 @@ package com.android.wm.shell.desktopmode.multidesks import android.app.ActivityManager import android.window.TransitionInfo import android.window.WindowContainerTransaction +import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback +import kotlin.coroutines.suspendCoroutine /** An organizer of desk containers in which to host child desktop windows. */ interface DesksOrganizer { @@ -40,6 +42,13 @@ interface DesksOrganizer { task: ActivityManager.RunningTaskInfo, ) + /** Reorders a desk's task to the front. */ + fun reorderTaskToFront( + wct: WindowContainerTransaction, + deskId: Int, + task: ActivityManager.RunningTaskInfo, + ) + /** Minimizes the given task of the given deskId. */ fun minimizeTask( wct: WindowContainerTransaction, @@ -47,6 +56,13 @@ interface DesksOrganizer { task: ActivityManager.RunningTaskInfo, ) + /** Unminimize the given task of the given desk. */ + fun unminimizeTask( + wct: WindowContainerTransaction, + deskId: Int, + task: ActivityManager.RunningTaskInfo, + ) + /** Whether the change is for the given desk id. */ fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean @@ -68,3 +84,9 @@ interface DesksOrganizer { fun onCreated(deskId: Int) } } + +/** Creates a new desk container in the given display. */ +suspend fun DesksOrganizer.createDesk(displayId: Int): Int = suspendCoroutine { cont -> + val onCreateCallback = OnCreateCallback { deskId -> cont.resumeWith(Result.success(deskId)) } + createDesk(displayId, onCreateCallback) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt index f576258ebdaa..49ca58e7b32a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt @@ -33,6 +33,7 @@ import androidx.core.util.forEach import com.android.internal.annotations.VisibleForTesting import com.android.internal.protolog.ProtoLog import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.LaunchAdjacentController import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.sysui.ShellCommandHandler @@ -44,6 +45,7 @@ class RootTaskDesksOrganizer( shellInit: ShellInit, shellCommandHandler: ShellCommandHandler, private val shellTaskOrganizer: ShellTaskOrganizer, + private val launchAdjacentController: LaunchAdjacentController, ) : DesksOrganizer, ShellTaskOrganizer.TaskListener { private val createDeskRootRequests = mutableListOf<CreateDeskRequest>() @@ -110,7 +112,31 @@ class RootTaskDesksOrganizer( wct.reparent(task.token, root.taskInfo.token, /* onTop= */ true) } + override fun reorderTaskToFront( + wct: WindowContainerTransaction, + deskId: Int, + task: RunningTaskInfo, + ) { + logV("reorderTaskToFront task=${task.taskId} desk=$deskId") + val root = deskRootsByDeskId[deskId] ?: error("Root not found for desk: $deskId") + if (task.taskId in root.children) { + wct.reorder(task.token, /* onTop= */ true, /* includingParents= */ true) + return + } + val minimizationRoot = + checkNotNull(deskMinimizationRootsByDeskId[deskId]) { + "Minimization root not found for desk: $deskId" + } + if (task.taskId in minimizationRoot.children) { + unminimizeTask(wct, deskId, task) + wct.reorder(task.token, /* onTop= */ true, /* includingParents= */ true) + return + } + logE("Attempted to reorder task=${task.taskId} in desk=$deskId but it was not a child") + } + override fun minimizeTask(wct: WindowContainerTransaction, deskId: Int, task: RunningTaskInfo) { + logV("minimizeTask task=${task.taskId} desk=$deskId") val deskRoot = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" } val minimizationRoot = @@ -129,6 +155,30 @@ class RootTaskDesksOrganizer( wct.reparent(task.token, minimizationRoot.token, /* onTop= */ true) } + override fun unminimizeTask( + wct: WindowContainerTransaction, + deskId: Int, + task: RunningTaskInfo, + ) { + val taskId = task.taskId + logV("unminimizeTask task=$taskId desk=$deskId") + val deskRoot = + checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" } + val minimizationRoot = + checkNotNull(deskMinimizationRootsByDeskId[deskId]) { + "Minimization root not found for desk: $deskId" + } + if (taskId in deskRoot.children) { + logV("Task #$taskId is already unminimized in desk=$deskId") + return + } + if (taskId !in minimizationRoot.children) { + logE("Attempted to unminimize task=$taskId in desk=$deskId but it was not a child") + return + } + wct.reparent(task.token, deskRoot.token, /* onTop= */ true) + } + override fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean = (isDeskRootChange(change) && change.taskId == deskId) || (getDeskMinimizationRootInChange(change)?.deskId == deskId) @@ -164,6 +214,21 @@ class RootTaskDesksOrganizer( change.mode == TRANSIT_TO_FRONT override fun onTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) { + handleTaskAppeared(taskInfo, leash) + updateLaunchAdjacentController() + } + + override fun onTaskInfoChanged(taskInfo: RunningTaskInfo) { + handleTaskInfoChanged(taskInfo) + updateLaunchAdjacentController() + } + + override fun onTaskVanished(taskInfo: RunningTaskInfo) { + handleTaskVanished(taskInfo) + updateLaunchAdjacentController() + } + + private fun handleTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) { // Check whether this task is appearing inside a desk. if (taskInfo.parentTaskId in deskRootsByDeskId) { val deskId = taskInfo.parentTaskId @@ -216,7 +281,7 @@ class RootTaskDesksOrganizer( hideMinimizationRoot(deskMinimizationRoot) } - override fun onTaskInfoChanged(taskInfo: RunningTaskInfo) { + private fun handleTaskInfoChanged(taskInfo: RunningTaskInfo) { if (deskRootsByDeskId.contains(taskInfo.taskId)) { val deskId = taskInfo.taskId deskRootsByDeskId[deskId] = deskRootsByDeskId[deskId].copy(taskInfo = taskInfo) @@ -254,7 +319,7 @@ class RootTaskDesksOrganizer( logE("onTaskInfoChanged: unknown task: ${taskInfo.taskId}") } - override fun onTaskVanished(taskInfo: RunningTaskInfo) { + private fun handleTaskVanished(taskInfo: RunningTaskInfo) { if (deskRootsByDeskId.contains(taskInfo.taskId)) { val deskId = taskInfo.taskId val deskRoot = deskRootsByDeskId[deskId] @@ -336,6 +401,18 @@ class RootTaskDesksOrganizer( deskRootsByDeskId.forEach { _, deskRoot -> deskRoot.children -= taskId } } + private fun updateLaunchAdjacentController() { + deskRootsByDeskId.forEach { deskId, root -> + if (root.taskInfo.isVisible) { + // Disable launch adjacent handling if any desk is active, otherwise the split + // launch root and the desk root will both be eligible to take launching tasks. + launchAdjacentController.launchAdjacentEnabled = false + return + } + } + launchAdjacentController.launchAdjacentEnabled = true + } + @VisibleForTesting data class DeskRoot( val deskId: Int, @@ -377,6 +454,9 @@ class RootTaskDesksOrganizer( override fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("$prefix$TAG") + pw.println( + "${innerPrefix}launchAdjacentEnabled=" + launchAdjacentController.launchAdjacentEnabled + ) pw.println("${innerPrefix}Desk Roots:") deskRootsByDeskId.forEach { deskId, root -> val minimizationRoot = deskMinimizationRootsByDeskId[deskId] diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt index 1566544f5303..f71eacab518d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt @@ -132,10 +132,7 @@ class DesktopPersistentRepository(private val dataStore: DataStore<DesktopPersis .toBuilder() .putDesktopRepoByUser( userId, - currentRepository - .toBuilder() - .putDesktop(desktopId, desktop.build()) - .build(), + currentRepository.toBuilder().putDesktop(desktopId, desktop.build()).build(), ) .build() } @@ -149,6 +146,33 @@ class DesktopPersistentRepository(private val dataStore: DataStore<DesktopPersis } } + /** Removes the desktop from the persistent repository. */ + suspend fun removeDesktop(userId: Int, desktopId: Int) { + try { + dataStore.updateData { persistentRepositories: DesktopPersistentRepositories -> + val currentRepository = + persistentRepositories.getDesktopRepoByUserOrDefault( + userId, + DesktopRepositoryState.getDefaultInstance(), + ) + persistentRepositories + .toBuilder() + .putDesktopRepoByUser( + userId, + currentRepository.toBuilder().removeDesktop(desktopId).build(), + ) + .build() + } + } catch (throwable: Throwable) { + Log.e( + TAG, + "Error in removing desktop related data, data is " + + "stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE", + throwable, + ) + } + } + suspend fun removeUsers(uids: List<Int>) { try { dataStore.updateData { persistentRepositories: DesktopPersistentRepositories -> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt index a26ebbf4c99a..8191181cac11 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt @@ -17,8 +17,22 @@ package com.android.wm.shell.desktopmode.persistence import com.android.wm.shell.desktopmode.DesktopUserRepositories +import kotlinx.coroutines.flow.StateFlow /** Interface for initializing the [DesktopUserRepositories]. */ -fun interface DesktopRepositoryInitializer { +interface DesktopRepositoryInitializer { + /** A factory used to recreate a desk from persistence. */ + var deskRecreationFactory: DeskRecreationFactory + + /** A flow that emits true when the repository has been initialized. */ + val isInitialized: StateFlow<Boolean> + + /** Initialize the user repositories from a persistent data store. */ fun initialize(userRepositories: DesktopUserRepositories) + + /** A factory for recreating desks. */ + fun interface DeskRecreationFactory { + /** Recreates a restored desk and returns the new desk id. */ + suspend fun recreateDesk(userId: Int, destinationDisplayId: Int, deskId: Int): Int + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt index 0507e59c06e1..49cb7391fe97 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt @@ -17,13 +17,19 @@ package com.android.wm.shell.desktopmode.persistence import android.content.Context +import android.view.Display import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.desktopmode.DesktopRepository import com.android.wm.shell.desktopmode.DesktopUserRepositories +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer.DeskRecreationFactory +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch /** @@ -37,62 +43,136 @@ class DesktopRepositoryInitializerImpl( private val persistentRepository: DesktopPersistentRepository, @ShellMainThread private val mainCoroutineScope: CoroutineScope, ) : DesktopRepositoryInitializer { + + override var deskRecreationFactory: DeskRecreationFactory = DefaultDeskRecreationFactory() + + private val _isInitialized = MutableStateFlow(false) + override val isInitialized: StateFlow<Boolean> = _isInitialized + override fun initialize(userRepositories: DesktopUserRepositories) { - if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) return + if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue) { + _isInitialized.value = true + return + } // TODO: b/365962554 - Handle the case that user moves to desktop before it's initialized mainCoroutineScope.launch { - val desktopUserPersistentRepositoryMap = - persistentRepository.getUserDesktopRepositoryMap() ?: return@launch - for (userId in desktopUserPersistentRepositoryMap.keys) { - val repository = userRepositories.getProfile(userId) - val desktopRepositoryState = - persistentRepository.getDesktopRepositoryState(userId) ?: continue - val desktopByDesktopIdMap = desktopRepositoryState.desktopMap - for (desktopId in desktopByDesktopIdMap.keys) { - val persistentDesktop = - persistentRepository.readDesktop(userId, desktopId) ?: continue - val maxTasks = - DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 } - ?: persistentDesktop.zOrderedTasksCount - var visibleTasksCount = 0 - repository.addDesk( - displayId = persistentDesktop.displayId, - deskId = - if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { - persistentDesktop.desktopId - } else { - // When disabled, desk ids are always the display id. - persistentDesktop.displayId - }, + try { + val desktopUserPersistentRepositoryMap = + persistentRepository.getUserDesktopRepositoryMap() ?: return@launch + for (userId in desktopUserPersistentRepositoryMap.keys) { + val repository = userRepositories.getProfile(userId) + val desktopRepositoryState = + persistentRepository.getDesktopRepositoryState(userId) ?: continue + val desksToRestore = getDesksToRestore(desktopRepositoryState, userId) + logV( + "initialize() will restore desks=%s user=%d", + desksToRestore.map { it.desktopId }, + userId, ) - persistentDesktop.zOrderedTasksList - // Reverse it so we initialize the repo from bottom to top. - .reversed() - .mapNotNull { taskId -> persistentDesktop.tasksByTaskIdMap[taskId] } - // TODO: b/362720497 - add tasks to their respective desk when multi-desk - // persistence is implemented. - .forEach { task -> - if ( - task.desktopTaskState == DesktopTaskState.VISIBLE && - visibleTasksCount < maxTasks - ) { - visibleTasksCount++ - repository.addTask( - persistentDesktop.displayId, - task.taskId, - isVisible = false, - ) - } else { - repository.addTask( - persistentDesktop.displayId, - task.taskId, + desksToRestore.forEach { persistentDesktop -> + val maxTasks = getTaskLimit(persistentDesktop) + val displayId = persistentDesktop.displayId + val deskId = persistentDesktop.desktopId + // TODO: b/401107440 - Implement desk restoration to other displays. + val newDisplayId = Display.DEFAULT_DISPLAY + val newDeskId = + deskRecreationFactory.recreateDesk( + userId = userId, + destinationDisplayId = newDisplayId, + deskId = deskId, + ) + logV( + "Recreated desk=%d in display=%d using new deskId=%d and displayId=%d", + deskId, + displayId, + newDeskId, + newDisplayId, + ) + if (newDeskId != deskId || newDisplayId != displayId) { + logV("Removing obsolete desk from persistence under deskId=%d", deskId) + persistentRepository.removeDesktop(userId, deskId) + } + + // TODO: b/393961770 - [DesktopRepository] doesn't save desks to the + // persistent repository until a task is added to them. Update it so that + // empty desks can be restored too. + repository.addDesk(displayId = displayId, deskId = newDeskId) + var visibleTasksCount = 0 + persistentDesktop.zOrderedTasksList + // Reverse it so we initialize the repo from bottom to top. + .reversed() + .mapNotNull { taskId -> persistentDesktop.tasksByTaskIdMap[taskId] } + .forEach { task -> + // Visible here means non-minimized a.k.a. expanded, it does not + // mean + // it is visible in WM (and |DesktopRepository|) terms. + val isVisible = + task.desktopTaskState == DesktopTaskState.VISIBLE && + visibleTasksCount < maxTasks + + repository.addTaskToDesk( + displayId = displayId, + deskId = newDeskId, + taskId = task.taskId, isVisible = false, ) - repository.minimizeTask(persistentDesktop.displayId, task.taskId) + + if (isVisible) { + visibleTasksCount++ + } else { + repository.minimizeTaskInDesk( + displayId = displayId, + deskId = newDeskId, + taskId = task.taskId, + ) + } } - } + } } + } finally { + _isInitialized.value = true } } } + + private suspend fun getDesksToRestore( + state: DesktopRepositoryState, + userId: Int, + ): Set<Desktop> { + // TODO: b/365873835 - what about desks that won't be restored? + // - invalid desk ids from multi-desk -> single-desk switching can be ignored / deleted. + val limitToSingleDeskPerDisplay = + !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue + return state.desktopMap.keys + .mapNotNull { deskId -> + persistentRepository.readDesktop(userId, deskId)?.takeIf { desk -> + // Do not restore invalid desks when multi-desks is disabled. This is + // possible if the feature is disabled after having created multiple desks. + val isValidSingleDesk = desk.desktopId == desk.displayId + (!limitToSingleDeskPerDisplay || isValidSingleDesk) + } + } + .toSet() + } + + private fun getTaskLimit(persistedDesk: Desktop): Int = + DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 } + ?: persistedDesk.zOrderedTasksCount + + private fun logV(msg: String, vararg arguments: Any?) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + /** A default implementation of [DeskRecreationFactory] that reuses the desk id. */ + private class DefaultDeskRecreationFactory : DeskRecreationFactory { + override suspend fun recreateDesk( + userId: Int, + destinationDisplayId: Int, + deskId: Int, + ): Int = deskId + } + + companion object { + private const val TAG = "DesktopRepositoryInitializerImpl" + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index 897e2d1601a5..2fe786531af5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -24,6 +24,7 @@ import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.util.SparseArray; import android.view.SurfaceControl; +import android.window.DesktopExperienceFlags; import android.window.DesktopModeFlags; import com.android.internal.protolog.ProtoLog; @@ -167,6 +168,11 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, } private void updateLaunchAdjacentController() { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) { + // With multiple desks, freeform tasks are children of a root task controlled by + // DesksOrganizer, so toggling launch-adjacent should be managed there. + return; + } for (int i = 0; i < mTasks.size(); i++) { if (mTasks.valueAt(i).mTaskInfo.isVisible) { mLaunchAdjacentController.setLaunchAdjacentEnabled(false); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java index 8059b94685ba..f89ba0a168d1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java @@ -29,6 +29,7 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.wm.shell.desktopmode.DesktopImmersiveController; +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.Transitions; @@ -52,6 +53,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs private final WindowDecorViewModel mWindowDecorViewModel; private final Optional<TaskChangeListener> mTaskChangeListener; private final FocusTransitionObserver mFocusTransitionObserver; + private final Optional<DesksTransitionObserver> mDesksTransitionObserver; private final Map<IBinder, List<ActivityManager.RunningTaskInfo>> mTransitionToTaskInfo = new HashMap<>(); @@ -63,12 +65,14 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs Optional<DesktopImmersiveController> desktopImmersiveController, WindowDecorViewModel windowDecorViewModel, Optional<TaskChangeListener> taskChangeListener, - FocusTransitionObserver focusTransitionObserver) { + FocusTransitionObserver focusTransitionObserver, + Optional<DesksTransitionObserver> desksTransitionObserver) { mTransitions = transitions; mDesktopImmersiveController = desktopImmersiveController; mWindowDecorViewModel = windowDecorViewModel; mTaskChangeListener = taskChangeListener; mFocusTransitionObserver = focusTransitionObserver; + mDesksTransitionObserver = desksTransitionObserver; if (FreeformComponents.requiresFreeformComponents(context)) { shellInit.addInitCallback(this::onInit, this); } @@ -85,6 +89,10 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT) { + // Update desk state first, otherwise [TaskChangeListener] may update desktop task state + // under an outdated active desk if a desk switch and a task update happen in the same + // transition, such as when unminimizing a task from an inactive desk. + mDesksTransitionObserver.ifPresent(o -> o.onTransitionReady(transition, info)); if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) { // TODO(b/367268953): Remove when DesktopTaskListener is introduced and the repository // is updated from there **before** the |mWindowDecorViewModel| methods are invoked. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java index 65099c2dfb9d..671eae3d84ef 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java @@ -153,7 +153,12 @@ public class PhonePipMenuController implements PipMenuController, mPipUiEventLogger = pipUiEventLogger; mPipTransitionState.addPipTransitionStateChangedListener(this); - + // Clear actions after exit PiP. Otherwise, next PiP could accidentally inherit the + // actions provided by the previous app in PiP mode. + mPipBoundsState.addOnPipComponentChangedListener(((oldPipComponent, newPipComponent) -> { + if (mAppActions != null) mAppActions.clear(); + mCloseAction = null; + })); mPipTaskListener.addParamsChangedListener(new PipTaskListener.PipParamsChangedCallback() { @Override public void onActionsChanged(List<RemoteAction> actions, RemoteAction closeAction) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index 383afcf6f821..f81f330e50c4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -20,6 +20,7 @@ import android.app.PictureInPictureParams; import android.content.Context; import android.graphics.Matrix; import android.graphics.Rect; +import android.os.Bundle; import android.os.SystemProperties; import android.view.SurfaceControl; import android.window.WindowContainerToken; @@ -47,7 +48,7 @@ import java.util.function.Supplier; /** * Scheduler for Shell initiated PiP transitions and animations. */ -public class PipScheduler { +public class PipScheduler implements PipTransitionState.PipTransitionStateChangedListener { private static final String TAG = PipScheduler.class.getSimpleName(); /** @@ -71,6 +72,7 @@ public class PipScheduler { private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper; @Nullable private Runnable mUpdateMovementBoundsRunnable; + @Nullable private PipAlphaAnimator mOverlayFadeoutAnimator; private PipAlphaAnimatorSupplier mPipAlphaAnimatorSupplier; private Supplier<PictureInPictureParams> mPipParamsSupplier; @@ -85,6 +87,7 @@ public class PipScheduler { mPipBoundsState = pipBoundsState; mMainExecutor = mainExecutor; mPipTransitionState = pipTransitionState; + mPipTransitionState.addPipTransitionStateChangedListener(this); mPipDesktopState = pipDesktopState; mSplitScreenControllerOptional = splitScreenControllerOptional; @@ -238,12 +241,16 @@ public class PipScheduler { void startOverlayFadeoutAnimation(@NonNull SurfaceControl overlayLeash, boolean withStartDelay, @NonNull Runnable onAnimationEnd) { - PipAlphaAnimator animator = mPipAlphaAnimatorSupplier.get(mContext, overlayLeash, + mOverlayFadeoutAnimator = mPipAlphaAnimatorSupplier.get(mContext, overlayLeash, null /* startTx */, null /* finishTx */, PipAlphaAnimator.FADE_OUT); - animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DURATION_MS); - animator.setStartDelay(withStartDelay ? EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS : 0); - animator.setAnimationEndCallback(onAnimationEnd); - animator.start(); + mOverlayFadeoutAnimator.setDuration(CONTENT_OVERLAY_FADE_OUT_DURATION_MS); + mOverlayFadeoutAnimator.setStartDelay(withStartDelay + ? EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS : 0); + mOverlayFadeoutAnimator.setAnimationEndCallback(() -> { + onAnimationEnd.run(); + mOverlayFadeoutAnimator = null; + }); + mOverlayFadeoutAnimator.start(); } void setUpdateMovementBoundsRunnable(@Nullable Runnable updateMovementBoundsRunnable) { @@ -289,6 +296,21 @@ public class PipScheduler { mSurfaceControlTransactionFactory = factory; } + @Override + public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, + @PipTransitionState.TransitionState int newState, + @android.annotation.Nullable Bundle extra) { + switch (newState) { + case PipTransitionState.EXITING_PIP: + case PipTransitionState.SCHEDULED_BOUNDS_CHANGE: + if (mOverlayFadeoutAnimator != null && mOverlayFadeoutAnimator.isStarted()) { + mOverlayFadeoutAnimator.end(); + mOverlayFadeoutAnimator = null; + } + break; + } + } + @VisibleForTesting interface PipAlphaAnimatorSupplier { PipAlphaAnimator get(@NonNull Context context, @@ -303,6 +325,17 @@ public class PipScheduler { mPipAlphaAnimatorSupplier = supplier; } + @VisibleForTesting + void setOverlayFadeoutAnimator(@NonNull PipAlphaAnimator animator) { + mOverlayFadeoutAnimator = animator; + } + + @VisibleForTesting + @Nullable + PipAlphaAnimator getOverlayFadeoutAnimator() { + return mOverlayFadeoutAnimator; + } + void setPipParamsSupplier(@NonNull Supplier<PictureInPictureParams> pipParamsSupplier) { mPipParamsSupplier = pipParamsSupplier; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java index d6634845ee21..294ef48c01d0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java @@ -61,7 +61,7 @@ public class PipTaskListener implements ShellTaskOrganizer.TaskListener, private final PipBoundsState mPipBoundsState; private final PipBoundsAlgorithm mPipBoundsAlgorithm; private final ShellExecutor mMainExecutor; - private final PictureInPictureParams mPictureInPictureParams = + private PictureInPictureParams mPictureInPictureParams = new PictureInPictureParams.Builder().build(); private boolean mWaitingForAspectRatioChange = false; @@ -92,6 +92,11 @@ public class PipTaskListener implements ShellTaskOrganizer.TaskListener, } mPipResizeAnimatorSupplier = PipResizeAnimator::new; mPipScheduler.setPipParamsSupplier(this::getPictureInPictureParams); + // Reset {@link #mPictureInPictureParams} after exiting PiP. For instance, next Activity + // with null aspect ratio would accidentally inherit the aspect ratio from a previous + // PiP Activity. + mPipBoundsState.addOnPipComponentChangedListener(((oldPipComponent, newPipComponent) -> + mPictureInPictureParams = new PictureInPictureParams.Builder().build())); } void setPictureInPictureParams(@Nullable PictureInPictureParams params) { @@ -138,9 +143,8 @@ public class PipTaskListener implements ShellTaskOrganizer.TaskListener, if (mPictureInPictureParams.hasSetAspectRatio() && mPipBoundsAlgorithm.isValidPictureInPictureAspectRatio(newAspectRatio) && PipUtils.aspectRatioChanged(newAspectRatio, mPipBoundsState.getAspectRatio())) { - mPipTransitionState.setOnIdlePipTransitionStateRunnable(() -> { - onAspectRatioChanged(newAspectRatio); - }); + mPipTransitionState.setOnIdlePipTransitionStateRunnable( + () -> onAspectRatioChanged(newAspectRatio)); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 7472b0ea56ca..c370c0cb0930 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -2920,7 +2920,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareEnterSplitScreen(out); mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), TRANSIT_SPLIT_SCREEN_PAIR_OPEN, !mIsDropEntering, SNAP_TO_2_50_50); - } else if (isSplitScreenVisible() && isOpening) { + } else if (enableFlexibleTwoAppSplit() && isSplitScreenVisible() && isOpening) { // launching into an existing split stage; possibly launchAdjacent // If we're replacing a pip-able app, we need to let mixed handler take care of // it. Otherwise we'll just treat it as an enter+resize diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt index 01fc6440712d..adc5cdf340fd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt @@ -43,7 +43,7 @@ class DesktopHandleManageWindowsMenu( private val captionWidth: Int, private val windowManagerWrapper: WindowManagerWrapper, context: Context, - snapshotList: List<Pair<Int, TaskSnapshot>>, + snapshotList: List<Pair<Int, TaskSnapshot?>>, onIconClickListener: ((Int) -> Unit), onOutsideClickListener: (() -> Unit) ) : ManageWindowsViewContainer( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt index 02a5433147ca..3a75933f59d7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt @@ -54,7 +54,7 @@ class DesktopHeaderManageWindowsMenu( private val desktopUserRepositories: DesktopUserRepositories, private val surfaceControlBuilderSupplier: Supplier<SurfaceControl.Builder>, private val surfaceControlTransactionSupplier: Supplier<SurfaceControl.Transaction>, - snapshotList: List<Pair<Int, TaskSnapshot>>, + snapshotList: List<Pair<Int, TaskSnapshot?>>, onIconClickListener: ((Int) -> Unit), onOutsideClickListener: (() -> Unit) ) : ManageWindowsViewContainer( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index ffb81f8c33e5..a1d2774ee428 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -1218,7 +1218,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, if (id == R.id.caption_handle) { handleCaptionThroughStatusBar(e, decoration); final boolean wasDragging = mIsDragging; - updateDragStatus(e.getActionMasked()); + updateDragStatus(decoration, e); final boolean upOrCancel = e.getActionMasked() == ACTION_UP || e.getActionMasked() == ACTION_CANCEL; if (wasDragging && upOrCancel) { @@ -1234,6 +1234,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, return false; } + private void setIsDragging( + @Nullable DesktopModeWindowDecoration decor, boolean isDragging) { + mIsDragging = isDragging; + if (decor == null) return; + decor.setIsDragging(isDragging); + } + private boolean handleFreeformMotionEvent(DesktopModeWindowDecoration decoration, RunningTaskInfo taskInfo, View v, MotionEvent e) { final int id = v.getId(); @@ -1253,7 +1260,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, final Rect initialBounds = mDragPositioningCallback.onDragPositioningStart( 0 /* ctrlType */, e.getDisplayId(), e.getRawX(0), e.getRawY(0)); - updateDragStatus(e.getActionMasked()); + updateDragStatus(decoration, e); mOnDragStartInitialBounds.set(initialBounds); } // Do not consume input event if a button is touched, otherwise it would @@ -1280,7 +1287,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, newTaskBounds); // Flip mIsDragging only if the bounds actually changed. if (mIsDragging || !newTaskBounds.equals(mOnDragStartInitialBounds)) { - updateDragStatus(e.getActionMasked()); + updateDragStatus(decoration, e); } return true; } @@ -1313,7 +1320,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, // onClick call that results. return false; } else { - updateDragStatus(e.getActionMasked()); + updateDragStatus(decoration, e); return true; } } @@ -1321,16 +1328,16 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, return true; } - private void updateDragStatus(int eventAction) { - switch (eventAction) { + private void updateDragStatus(DesktopModeWindowDecoration decor, MotionEvent e) { + switch (e.getActionMasked()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { - mIsDragging = false; + setIsDragging(decor, false /* isDragging */); break; } case MotionEvent.ACTION_MOVE: { - mIsDragging = true; + setIsDragging(decor, true /* isDragging */); break; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 2a5315739396..673c3f66cb6c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -29,6 +29,7 @@ import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_ import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode; import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopModeOrShowAppHandle; +import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.isDesktopModeSupportedOnDisplay; import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEventListener; @@ -207,7 +208,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository; private final DesktopUserRepositories mDesktopUserRepositories; private boolean mIsRecentsTransitionRunning = false; - + private boolean mIsDragging = false; private Runnable mLoadAppInfoRunnable; private Runnable mSetAppInfoRunnable; @@ -513,7 +514,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin updateRelayoutParams(mRelayoutParams, mContext, taskInfo, mSplitScreenController, applyStartTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, inFullImmersive, - mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus, + mIsDragging, mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus, displayExclusionRegion, mIsRecentsTransitionRunning, mDesktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds(taskInfo)); @@ -911,6 +912,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin boolean isStatusBarVisible, boolean isKeyguardVisibleAndOccluded, boolean inFullImmersiveMode, + boolean isDragging, @NonNull InsetsState displayInsetsState, boolean hasGlobalFocus, @NonNull Region displayExclusionRegion, @@ -933,9 +935,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin relayoutParams.mAsyncViewHost = isAppHandle; final boolean showCaption; - if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) { + if (DesktopModeFlags.ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX.isTrue() && isDragging) { + // If the task is being dragged, the caption should not be hidden so that it continues + // receiving input + showCaption = true; + } else if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) { if (inFullImmersiveMode) { - showCaption = isStatusBarVisible && !isKeyguardVisibleAndOccluded; + showCaption = (isStatusBarVisible && !isKeyguardVisibleAndOccluded); } else { showCaption = taskInfo.isFreeform() || (isStatusBarVisible && !isKeyguardVisibleAndOccluded); @@ -1405,7 +1411,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin supportsMultiInstance, shouldShowManageWindowsButton, shouldShowChangeAspectRatioButton, - canEnterDesktopMode(mContext), + isDesktopModeSupportedOnDisplay(mContext, mDisplay), isBrowserApp, isBrowserApp ? getAppLink() : getBrowserLink(), mResult.mCaptionWidth, @@ -1799,6 +1805,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } /** + * Declares whether the window decoration is being dragged. + */ + void setIsDragging(boolean isDragging) { + mIsDragging = isDragging; + } + + /** * Called when there is a {@link MotionEvent#ACTION_HOVER_EXIT} on the maximize window button. */ void onMaximizeButtonHoverExit() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index ce786177290f..c544468f5191 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -39,7 +39,6 @@ import android.widget.ImageButton import android.widget.ImageView import android.widget.LinearLayout import android.widget.Space -import android.widget.TextView import android.window.DesktopModeFlags import android.window.SurfaceSyncGroup import androidx.annotation.StringRes @@ -59,10 +58,10 @@ import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import com.android.wm.shell.windowdecor.common.DrawableInsets import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader import com.android.wm.shell.windowdecor.common.calculateMenuPosition -import com.android.wm.shell.windowdecor.common.DrawableInsets -import com.android.wm.shell.windowdecor.common.createRippleDrawable +import com.android.wm.shell.windowdecor.common.createBackgroundDrawable import com.android.wm.shell.windowdecor.extension.isFullscreen import com.android.wm.shell.windowdecor.extension.isMultiWindow import com.android.wm.shell.windowdecor.extension.isPinned @@ -478,13 +477,13 @@ class HandleMenu( R.dimen.desktop_mode_handle_menu_icon_button_ripple_inset_base) private val iconButtonRippleRadius = context.resources.getDimensionPixelSize( R.dimen.desktop_mode_handle_menu_icon_button_ripple_radius) - private val iconButtonDrawableInsetsBase = DrawableInsets(t=iconButtondrawableBaseInset, - b=iconButtondrawableBaseInset, l=iconButtondrawableBaseInset, - r=iconButtondrawableBaseInset) - private val iconButtonDrawableInsetsLeft = DrawableInsets(t=iconButtondrawableBaseInset, - b=iconButtondrawableBaseInset, l=iconButtondrawableShiftInset, r=0) - private val iconButtonDrawableInsetsRight = DrawableInsets(t=iconButtondrawableBaseInset, - b=iconButtondrawableBaseInset, l=0, r=iconButtondrawableShiftInset) + private val iconButtonDrawableInsetsBase = DrawableInsets(t = iconButtondrawableBaseInset, + b = iconButtondrawableBaseInset, l = iconButtondrawableBaseInset, + r = iconButtondrawableBaseInset) + private val iconButtonDrawableInsetsLeft = DrawableInsets(t = iconButtondrawableBaseInset, + b = iconButtondrawableBaseInset, l = iconButtondrawableShiftInset, r = 0) + private val iconButtonDrawableInsetsRight = DrawableInsets(t = iconButtondrawableBaseInset, + b = iconButtondrawableBaseInset, l = 0, r = iconButtondrawableShiftInset) // App Info Pill. private val appInfoPill = rootView.requireViewById<View>(R.id.app_info_pill) @@ -702,7 +701,7 @@ class HandleMenu( imageTintList = ColorStateList.valueOf(style.textColor) this.taskInfo = this@HandleMenuView.taskInfo - background = createRippleDrawable( + background = createBackgroundDrawable( color = style.textColor, cornerRadius = iconButtonRippleRadius, drawableInsets = iconButtonDrawableInsetsBase @@ -740,7 +739,7 @@ class HandleMenu( else iconButtonDrawableInsetsRight fullscreenBtn.apply { - background = createRippleDrawable( + background = createBackgroundDrawable( color = style.textColor, cornerRadius = iconButtonRippleRadius, drawableInsets = startInsets @@ -748,7 +747,7 @@ class HandleMenu( } splitscreenBtn.apply { - background = createRippleDrawable( + background = createBackgroundDrawable( color = style.textColor, cornerRadius = iconButtonRippleRadius, drawableInsets = iconButtonDrawableInsetsBase @@ -756,7 +755,7 @@ class HandleMenu( } floatingBtn.apply { - background = createRippleDrawable( + background = createBackgroundDrawable( color = style.textColor, cornerRadius = iconButtonRippleRadius, drawableInsets = iconButtonDrawableInsetsBase @@ -764,7 +763,7 @@ class HandleMenu( } desktopBtn.apply { - background = createRippleDrawable( + background = createBackgroundDrawable( color = style.textColor, cornerRadius = iconButtonRippleRadius, drawableInsets = endInsets diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt index 22bc9782170b..cf536eba8382 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt @@ -42,8 +42,6 @@ class MoveToDesktopAnimator @JvmOverloads constructor( .setDuration(ANIMATION_DURATION.toLong()) .apply { val t = SurfaceControl.Transaction() - val cornerRadius = context.resources - .getDimensionPixelSize(R.dimen.desktop_mode_dragged_task_radius).toFloat() addUpdateListener { setTaskPosition(mostRecentInput.x, mostRecentInput.y) t.setScale(taskSurface, scale, scale) @@ -57,6 +55,8 @@ class MoveToDesktopAnimator @JvmOverloads constructor( val taskId get() = taskInfo.taskId val position: PointF = PointF(0.0f, 0.0f) + val cornerRadius: Float = context.resources + .getDimensionPixelSize(R.dimen.desktop_mode_dragged_task_radius).toFloat() /** * Whether motion events from the drag gesture should affect the dragged surface or not. Used diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 7baef2b2dc97..bde46a1bc375 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -361,6 +361,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> outResult.mRootView = rootView; final boolean fontScaleChanged = mWindowDecorConfig != null && mWindowDecorConfig.fontScale != mTaskInfo.configuration.fontScale; + final boolean localeListChanged = mWindowDecorConfig != null + && !mWindowDecorConfig.getLocales() + .equals(mTaskInfo.getConfiguration().getLocales()); final int oldDensityDpi = mWindowDecorConfig != null ? mWindowDecorConfig.densityDpi : DENSITY_DPI_UNDEFINED; final int oldNightMode = mWindowDecorConfig != null @@ -376,7 +379,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> || oldLayoutResId != mLayoutResId || oldNightMode != newNightMode || mDecorWindowContext == null - || fontScaleChanged) { + || fontScaleChanged + || localeListChanged) { releaseViews(wct); if (!obtainDisplayOrRegisterListener()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt index f44b15a23b90..f08cfa987cc7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt @@ -20,7 +20,6 @@ import android.content.res.ColorStateList import android.graphics.Color import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable -import android.graphics.drawable.RippleDrawable import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.RoundRectShape @@ -48,45 +47,6 @@ fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int { } /** - * Creates a RippleDrawable with specified color, corner radius, and insets. - */ -fun createRippleDrawable( - @ColorInt color: Int, - cornerRadius: Int, - drawableInsets: DrawableInsets, -): RippleDrawable { - return RippleDrawable( - ColorStateList( - arrayOf( - intArrayOf(android.R.attr.state_hovered), - intArrayOf(android.R.attr.state_pressed), - intArrayOf(), - ), - intArrayOf( - replaceColorAlpha(color, OPACITY_11), - replaceColorAlpha(color, OPACITY_15), - Color.TRANSPARENT, - ) - ), - null /* content */, - LayerDrawable(arrayOf( - ShapeDrawable().apply { - shape = RoundRectShape( - FloatArray(8) { cornerRadius.toFloat() }, - null /* inset */, - null /* innerRadii */ - ) - paint.color = Color.WHITE - } - )).apply { - require(numberOfLayers == 1) { "Must only contain one layer" } - setLayerInset(0 /* index */, - drawableInsets.l, drawableInsets.t, drawableInsets.r, drawableInsets.b) - } - ) -} - -/** * Creates a background drawable with specified color, corner radius, and insets. */ fun createBackgroundDrawable( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt index 801048adda4d..957898fd0088 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt @@ -21,6 +21,7 @@ import android.content.Context import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.graphics.Bitmap +import android.os.LocaleList import android.os.UserHandle import androidx.tracing.Trace import com.android.internal.annotations.VisibleForTesting @@ -80,6 +81,13 @@ class WindowDecorTaskResourceLoader( */ private val existingTasks = mutableSetOf<Int>() + /** + * A map of task -> localeList to keep track of the language of app name that's currently + * cached in |taskToResourceCache|. + */ + @VisibleForTesting + val localeListOnCache = ConcurrentHashMap<Int, LocaleList>() + init { shellInit.addInitCallback(this::onInit, this) } @@ -99,11 +107,14 @@ class WindowDecorTaskResourceLoader( fun getName(taskInfo: RunningTaskInfo): CharSequence { checkWindowDecorExists(taskInfo) val cachedResources = taskToResourceCache[taskInfo.taskId] - if (cachedResources != null) { + val localeListActiveOnCacheTime = localeListOnCache[taskInfo.taskId] + if (cachedResources != null && + taskInfo.getConfiguration().getLocales().equals(localeListActiveOnCacheTime)) { return cachedResources.appName } val resources = loadAppResources(taskInfo) taskToResourceCache[taskInfo.taskId] = resources + localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales() return resources.appName } @@ -117,6 +128,7 @@ class WindowDecorTaskResourceLoader( } val resources = loadAppResources(taskInfo) taskToResourceCache[taskInfo.taskId] = resources + localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales() return resources.appIcon } @@ -130,6 +142,7 @@ class WindowDecorTaskResourceLoader( } val resources = loadAppResources(taskInfo) taskToResourceCache[taskInfo.taskId] = resources + localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales() return resources.veilIcon } @@ -142,6 +155,7 @@ class WindowDecorTaskResourceLoader( fun onWindowDecorClosed(taskInfo: RunningTaskInfo) { existingTasks.remove(taskInfo.taskId) taskToResourceCache.remove(taskInfo.taskId) + localeListOnCache.remove(taskInfo.taskId) } private fun checkWindowDecorExists(taskInfo: RunningTaskInfo) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt index c3f5b3f20f4a..30712b55bdfa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -466,11 +466,7 @@ class AppHeaderViewHolder( override fun onHandleMenuOpened() {} - override fun onHandleMenuClosed() { - openMenuButton.post { - openMenuButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) - } - } + override fun onHandleMenuClosed() {} fun onMaximizeWindowHoverExit() { maximizeButtonView.cancelHoverAnimation() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt index 3e98e4342b3f..7d9f2bf8fdf6 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/UnminimizeAppFromTaskbar.kt @@ -19,7 +19,7 @@ package com.android.wm.shell.scenarios import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation -import android.tools.device.apphelpers.BrowserAppHelper +import android.tools.device.apphelpers.GmailAppHelper import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry @@ -44,8 +44,8 @@ abstract class UnminimizeAppFromTaskbar(val rotation: Rotation = Rotation.ROTATI private val wmHelper = WindowManagerStateHelper(instrumentation) private val device = UiDevice.getInstance(instrumentation) private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) - private val browserHelper = BrowserAppHelper(instrumentation) - private val browserApp = DesktopModeAppHelper(browserHelper) + private val gmailHelper = GmailAppHelper(instrumentation) + private val gmailApp = DesktopModeAppHelper(gmailHelper) @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) @@ -59,20 +59,20 @@ abstract class UnminimizeAppFromTaskbar(val rotation: Rotation = Rotation.ROTATI ChangeDisplayOrientationRule.setRotation(rotation) testApp.enterDesktopMode(wmHelper, device) tapl.showTaskbarIfHidden() - browserApp.launchViaIntent(wmHelper) - browserApp.minimizeDesktopApp(wmHelper, device) + gmailApp.launchViaIntent(wmHelper) + gmailApp.minimizeDesktopApp(wmHelper, device) } @Test open fun unminimizeApp() { tapl.launchedAppState.taskbar - .getAppIcon(browserHelper.appName) - .launch(browserHelper.packageName) + .getAppIcon(gmailHelper.appName) + .launch(gmailHelper.packageName) } @After fun teardown() { testApp.exit(wmHelper) - browserApp.exit(wmHelper) + gmailApp.exit(wmHelper) } }
\ No newline at end of file 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 1de47df78853..e51447f5cfcb 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml @@ -50,6 +50,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> 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 34d001c858f6..7659ec903480 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml @@ -50,6 +50,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml index 34d001c858f6..7659ec903480 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml @@ -50,6 +50,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml index 9c1a8f17aeee..a4ecac9dfeb0 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml @@ -50,6 +50,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml index ae73dae99d6f..75ffdc69c73b 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml @@ -50,6 +50,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml index a136936c0838..8003cbaada50 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml @@ -50,6 +50,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java index 3c79ea7be39f..925ca0f1638d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java @@ -221,8 +221,8 @@ public class BubbleTransitionsTest extends ShellTestCase { PointF dragPosition = new PointF(10f, 20f); BubbleTransitions.DragData dragData = new BubbleTransitions.DragData( - /* releasedOnLeft= */ false, /* taskScale= */ 0.5f, dragPosition, - pendingWct); + /* releasedOnLeft= */ false, /* taskScale= */ 0.5f, /* cornerRadius= */ 10f, + dragPosition, pendingWct); final BubbleTransitions.BubbleTransition bt = mBubbleTransitions.startConvertToBubble( mBubble, taskInfo, mExpandedViewManager, mTaskViewFactory, mBubblePositioner, @@ -253,6 +253,8 @@ public class BubbleTransitionsTest extends ShellTestCase { verify(startT).setPosition(snapshot, dragPosition.x, dragPosition.y); // Snapshot has the scale of the dragged task verify(startT).setScale(snapshot, dragData.getTaskScale(), dragData.getTaskScale()); + // Snapshot has dragged task corner radius + verify(startT).setCornerRadius(snapshot, dragData.getCornerRadius()); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java index 01b76edd9b25..1066276becc7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java @@ -19,6 +19,8 @@ package com.android.wm.shell.common.pip; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -128,6 +130,31 @@ public class PipBoundsStateTest extends ShellTestCase { } @Test + public void setLastPipComponentName_notChanged_doesNotCallbackComponentChangedListener() { + mPipBoundsState.setLastPipComponentName(mTestComponentName1); + PipBoundsState.OnPipComponentChangedListener mockListener = + mock(PipBoundsState.OnPipComponentChangedListener.class); + + mPipBoundsState.addOnPipComponentChangedListener(mockListener); + mPipBoundsState.setLastPipComponentName(mTestComponentName1); + + verify(mockListener, never()).onPipComponentChanged(any(), any()); + } + + @Test + public void setLastPipComponentName_changed_callbackComponentChangedListener() { + mPipBoundsState.setLastPipComponentName(mTestComponentName1); + PipBoundsState.OnPipComponentChangedListener mockListener = + mock(PipBoundsState.OnPipComponentChangedListener.class); + + mPipBoundsState.addOnPipComponentChangedListener(mockListener); + mPipBoundsState.setLastPipComponentName(mTestComponentName2); + + verify(mockListener).onPipComponentChanged( + eq(mTestComponentName1), eq(mTestComponentName2)); + } + + @Test public void testSetLastPipComponentName_notChanged_doesNotClearReentryState() { mPipBoundsState.setLastPipComponentName(mTestComponentName1); mPipBoundsState.saveReentryState(DEFAULT_SNAP_FRACTION); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java index 598a101b8bcd..597e4a55ed0e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -59,6 +59,7 @@ import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.api.CompatUIInfo; +import com.android.wm.shell.compatui.impl.CompatUIRequests; import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.sysui.ShellController; @@ -738,6 +739,22 @@ public class CompatUIControllerTest extends ShellTestCase { verify(mController, never()).removeLayouts(taskInfo.taskId); } + @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) + public void testSendCompatUIRequest_createRestartDialog() { + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ false); + doReturn(true).when(mMockRestartDialogLayout) + .needsToBeRecreated(any(TaskInfo.class), + any(ShellTaskOrganizer.TaskListener.class)); + doReturn(true).when(mCompatUIConfiguration).isRestartDialogEnabled(); + doReturn(true).when(mCompatUIConfiguration).shouldShowRestartDialogAgain(eq(taskInfo)); + + mController.sendCompatUIRequest(new CompatUIRequests.DisplayCompatShowRestartDialog( + taskInfo, mMockTaskListener)); + verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo), + eq(mMockTaskListener)); + } + private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat) { return createTaskInfo(displayId, taskId, hasSizeCompat, /* isVisible */ false, /* isFocused */ false, /* isTopActivityTransparent */ false); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/crashhandling/ShellCrashHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/crashhandling/ShellCrashHandlerTest.kt new file mode 100644 index 000000000000..5c77f78a7dfa --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/crashhandling/ShellCrashHandlerTest.kt @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.crashhandling + +import android.app.ActivityManager.RunningTaskInfo +import android.app.PendingIntent +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.platform.test.annotations.DisableFlags +import android.view.Display.DEFAULT_DISPLAY +import android.window.IWindowContainerToken +import android.window.WindowContainerToken +import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_PENDING_INTENT +import com.android.modules.utils.testing.ExtendedMockitoRule +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.HomeIntentProvider +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.sysui.ShellInit +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import org.junit.Before +import org.junit.Rule +import org.mockito.ArgumentCaptor +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class ShellCrashHandlerTest : ShellTestCase() { + @JvmField + @Rule + val extendedMockitoRule = + ExtendedMockitoRule.Builder(this) + .mockStatic(DesktopModeStatus::class.java) + .mockStatic(PendingIntent::class.java) + .build()!! + + private val testExecutor = mock<ShellExecutor>() + private val context = mock<Context>() + private val shellTaskOrganizer = mock<ShellTaskOrganizer>() + + private lateinit var homeIntentProvider: HomeIntentProvider + private lateinit var crashHandler: ShellCrashHandler + private lateinit var shellInit: ShellInit + + + @Before + fun setup() { + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + whenever(PendingIntent.getActivity(any(), any(), any(), any(), any())).thenReturn(mock()) + + shellInit = spy(ShellInit(testExecutor)) + + homeIntentProvider = HomeIntentProvider(context) + crashHandler = ShellCrashHandler(context, shellTaskOrganizer, homeIntentProvider, shellInit) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun init_freeformTaskExists_sendsHomeIntent() { + val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(createTaskInfo(1))) + + shellInit.init() + + verify(shellTaskOrganizer).applyTransaction( + wctCaptor.capture() + ) + wctCaptor.value.assertPendingIntentAt(0, launchHomeIntent(DEFAULT_DISPLAY)) + } + + private fun launchHomeIntent(displayId: Int): Intent { + return Intent(Intent.ACTION_MAIN).apply { + if (displayId != DEFAULT_DISPLAY) { + addCategory(Intent.CATEGORY_SECONDARY_HOME) + } else { + addCategory(Intent.CATEGORY_HOME) + } + } + } + + private fun createTaskInfo(id: Int, windowingMode: Int = WINDOWING_MODE_FREEFORM) = + RunningTaskInfo().apply { + taskId = id + displayId = DEFAULT_DISPLAY + configuration.windowConfiguration.windowingMode = windowingMode + token = WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)) + baseIntent = Intent().apply { component = ComponentName("package", "component.name") } + } + + private fun WindowContainerTransaction.assertPendingIntentAt(index: Int, intent: Intent) { + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT) + assertThat(op.activityIntent?.component).isEqualTo(intent.component) + assertThat(op.activityIntent?.categories).isEqualTo(intent.categories) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt index 8ad54f5a0bb4..275d7b73a112 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt @@ -28,14 +28,22 @@ import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +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.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.spy +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.whenever @@ -46,15 +54,18 @@ import org.mockito.quality.Strictness * * Usage: atest WMShellUnitTests:DesktopDisplayEventHandlerTest */ +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidTestingRunner::class) class DesktopDisplayEventHandlerTest : ShellTestCase() { @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var displayController: DisplayController @Mock private lateinit var mockDesktopUserRepositories: DesktopUserRepositories + @Mock private lateinit var mockDesktopRepositoryInitializer: DesktopRepositoryInitializer @Mock private lateinit var mockDesktopRepository: DesktopRepository @Mock private lateinit var mockDesktopTasksController: DesktopTasksController @Mock private lateinit var desktopDisplayModeController: DesktopDisplayModeController + private val testScope = TestScope() private lateinit var mockitoSession: StaticMockitoSession private lateinit var shellInit: ShellInit @@ -77,7 +88,9 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { DesktopDisplayEventHandler( context, shellInit, + testScope.backgroundScope, displayController, + mockDesktopRepositoryInitializer, mockDesktopUserRepositories, mockDesktopTasksController, desktopDisplayModeController, @@ -89,17 +102,66 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { @After fun tearDown() { + testScope.cancel() mockitoSession.finishMocking() } @Test - fun testDisplayAdded_supportsDesks_createsDesk() { - whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) + fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_createsDesk() = + testScope.runTest { + val stateFlow = MutableStateFlow(false) + whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow) + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) - onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) + onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) + stateFlow.emit(true) + runCurrent() - verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY) - } + verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY) + } + + @Test + fun testDisplayAdded_supportsDesks_desktopRepositoryNotInitialized_doesNotCreateDesk() = + testScope.runTest { + val stateFlow = MutableStateFlow(false) + whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow) + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) + + onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) + runCurrent() + + verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY) + } + + @Test + fun testDisplayAdded_supportsDesks_desktopRepositoryInitializedTwice_createsDeskOnce() = + testScope.runTest { + val stateFlow = MutableStateFlow(false) + whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow) + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) + + onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) + stateFlow.emit(true) + stateFlow.emit(true) + runCurrent() + + verify(mockDesktopTasksController, times(1)).createDesk(DEFAULT_DISPLAY) + } + + @Test + fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_deskExists_doesNotCreateDesk() = + testScope.runTest { + val stateFlow = MutableStateFlow(false) + whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow) + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) + whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(1) + + onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) + stateFlow.emit(true) + runCurrent() + + verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY) + } @Test fun testDisplayAdded_cannotEnterDesktopMode_doesNotCreateDesk() { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt index cc37c440f650..450989dd334d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt @@ -21,9 +21,12 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.content.ContentResolver +import android.hardware.input.InputManager import android.os.Binder +import android.os.Handler 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.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS import android.view.Display.DEFAULT_DISPLAY @@ -44,6 +47,7 @@ import com.android.wm.shell.transition.Transitions import com.google.common.truth.Truth.assertThat import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameterInjector +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -64,13 +68,18 @@ import org.mockito.kotlin.whenever */ @SmallTest @RunWith(TestParameterInjector::class) -class DesktopDisplayModeControllerTest : ShellTestCase() { +class DesktopDisplayModeControllerTest( + @TestParameter(valuesProvider = FlagsParameterizationProvider::class) + flags: FlagsParameterization +) : ShellTestCase() { private val transitions = mock<Transitions>() private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>() private val mockWindowManager = mock<IWindowManager>() private val shellTaskOrganizer = mock<ShellTaskOrganizer>() private val desktopWallpaperActivityTokenProvider = mock<DesktopWallpaperActivityTokenProvider>() + private val inputManager = mock<InputManager>() + private val mainHandler = mock<Handler>() private lateinit var controller: DesktopDisplayModeController @@ -82,6 +91,10 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) private val wallpaperToken = MockToken().token() + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + @Before fun setUp() { whenever(transitions.startTransition(anyInt(), any(), isNull())).thenReturn(Binder()) @@ -95,27 +108,20 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { mockWindowManager, shellTaskOrganizer, desktopWallpaperActivityTokenProvider, + inputManager, + mainHandler, ) runningTasks.add(freeformTask) runningTasks.add(fullscreenTask) whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(ArrayList(runningTasks)) whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken) + setTabletModeStatus(SwitchState.UNKNOWN) } - private fun testDisplayWindowingModeSwitch( - defaultWindowingMode: Int, - extendedDisplayEnabled: Boolean, - expectToSwitch: Boolean, - ) { - defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode - whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(defaultWindowingMode) - val settingsSession = - ExtendedDisplaySettingsSession( - context.contentResolver, - if (extendedDisplayEnabled) 1 else 0, - ) - - settingsSession.use { + private fun testDisplayWindowingModeSwitchOnDisplayConnected(expectToSwitch: Boolean) { + defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(WINDOWING_MODE_FULLSCREEN) + ExtendedDisplaySettingsSession(context.contentResolver, 1).use { connectExternalDisplay() if (expectToSwitch) { // Assumes [connectExternalDisplay] properly triggered the switching transition. @@ -133,7 +139,7 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { assertThat(arg.firstValue.changes[wallpaperToken.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FULLSCREEN) assertThat(arg.secondValue.changes[defaultTDA.token.asBinder()]?.windowingMode) - .isEqualTo(defaultWindowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) assertThat(arg.secondValue.changes[wallpaperToken.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FULLSCREEN) } else { @@ -144,25 +150,64 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { @Test @DisableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING) - fun displayWindowingModeSwitchOnDisplayConnected_flagDisabled( - @TestParameter param: ModeSwitchTestCase - ) { - testDisplayWindowingModeSwitch( - param.defaultWindowingMode, - param.extendedDisplayEnabled, - // When the flag is disabled, never switch. - expectToSwitch = false, - ) + fun displayWindowingModeSwitchOnDisplayConnected_flagDisabled() { + // When the flag is disabled, never switch. + testDisplayWindowingModeSwitchOnDisplayConnected(/* expectToSwitch= */ false) } @Test @EnableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING) - fun displayWindowingModeSwitchOnDisplayConnected(@TestParameter param: ModeSwitchTestCase) { - testDisplayWindowingModeSwitch( - param.defaultWindowingMode, - param.extendedDisplayEnabled, - param.expectToSwitchByDefault, - ) + fun displayWindowingModeSwitchOnDisplayConnected() { + testDisplayWindowingModeSwitchOnDisplayConnected(/* expectToSwitch= */ true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING) + @DisableFlags(Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH) + fun testTargetWindowingMode_formfactorDisabled( + @TestParameter param: ExternalDisplayBasedTargetModeTestCase, + @TestParameter tabletModeStatus: SwitchState, + ) { + whenever(mockWindowManager.getWindowingMode(anyInt())) + .thenReturn(param.defaultWindowingMode) + if (param.hasExternalDisplay) { + connectExternalDisplay() + } else { + disconnectExternalDisplay() + } + setTabletModeStatus(tabletModeStatus) + + ExtendedDisplaySettingsSession( + context.contentResolver, + if (param.extendedDisplayEnabled) 1 else 0, + ) + .use { + assertThat(controller.getTargetWindowingModeForDefaultDisplay()) + .isEqualTo(param.expectedWindowingMode) + } + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING, + Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH, + ) + fun testTargetWindowingMode(@TestParameter param: FormFactorBasedTargetModeTestCase) { + if (param.hasExternalDisplay) { + connectExternalDisplay() + } else { + disconnectExternalDisplay() + } + setTabletModeStatus(param.tabletModeStatus) + + ExtendedDisplaySettingsSession( + context.contentResolver, + if (param.extendedDisplayEnabled) 1 else 0, + ) + .use { + assertThat(controller.getTargetWindowingModeForDefaultDisplay()) + .isEqualTo(param.expectedWindowingMode) + } } @Test @@ -217,6 +262,10 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { controller.refreshDisplayWindowingMode() } + private fun setTabletModeStatus(status: SwitchState) { + whenever(inputManager.isInTabletMode()).thenReturn(status.value) + } + private class ExtendedDisplaySettingsSession( private val contentResolver: ContentResolver, private val overrideValue: Int, @@ -233,33 +282,158 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { } } + private class FlagsParameterizationProvider : TestParameterValuesProvider() { + override fun provideValues( + context: TestParameterValuesProvider.Context + ): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf( + Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH + ) + } + } + companion object { const val EXTERNAL_DISPLAY_ID = 100 - enum class ModeSwitchTestCase( + enum class SwitchState(val value: Int) { + UNKNOWN(InputManager.SWITCH_STATE_UNKNOWN), + ON(InputManager.SWITCH_STATE_ON), + OFF(InputManager.SWITCH_STATE_OFF), + } + + enum class ExternalDisplayBasedTargetModeTestCase( val defaultWindowingMode: Int, + val hasExternalDisplay: Boolean, val extendedDisplayEnabled: Boolean, - val expectToSwitchByDefault: Boolean, + val expectedWindowingMode: Int, ) { - FULLSCREEN_DISPLAY( + FREEFORM_EXTERNAL_EXTENDED( + defaultWindowingMode = WINDOWING_MODE_FREEFORM, + hasExternalDisplay = true, + extendedDisplayEnabled = true, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + FULLSCREEN_EXTERNAL_EXTENDED( defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + hasExternalDisplay = true, + extendedDisplayEnabled = true, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + FREEFORM_NO_EXTERNAL_EXTENDED( + defaultWindowingMode = WINDOWING_MODE_FREEFORM, + hasExternalDisplay = false, extendedDisplayEnabled = true, - expectToSwitchByDefault = true, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + FULLSCREEN_NO_EXTERNAL_EXTENDED( + defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + hasExternalDisplay = false, + extendedDisplayEnabled = true, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + FREEFORM_EXTERNAL_MIRROR( + defaultWindowingMode = WINDOWING_MODE_FREEFORM, + hasExternalDisplay = true, + extendedDisplayEnabled = false, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, ), - FULLSCREEN_DISPLAY_MIRRORING( + FULLSCREEN_EXTERNAL_MIRROR( defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + hasExternalDisplay = true, extendedDisplayEnabled = false, - expectToSwitchByDefault = false, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - FREEFORM_DISPLAY( + FREEFORM_NO_EXTERNAL_MIRROR( defaultWindowingMode = WINDOWING_MODE_FREEFORM, + hasExternalDisplay = false, + extendedDisplayEnabled = false, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + FULLSCREEN_NO_EXTERNAL_MIRROR( + defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + hasExternalDisplay = false, + extendedDisplayEnabled = false, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + } + + enum class FormFactorBasedTargetModeTestCase( + val hasExternalDisplay: Boolean, + val extendedDisplayEnabled: Boolean, + val tabletModeStatus: SwitchState, + val expectedWindowingMode: Int, + ) { + EXTERNAL_EXTENDED_TABLET( + hasExternalDisplay = true, extendedDisplayEnabled = true, - expectToSwitchByDefault = false, + tabletModeStatus = SwitchState.ON, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, ), - FREEFORM_DISPLAY_MIRRORING( - defaultWindowingMode = WINDOWING_MODE_FREEFORM, + NO_EXTERNAL_EXTENDED_TABLET( + hasExternalDisplay = false, + extendedDisplayEnabled = true, + tabletModeStatus = SwitchState.ON, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + EXTERNAL_MIRROR_TABLET( + hasExternalDisplay = true, + extendedDisplayEnabled = false, + tabletModeStatus = SwitchState.ON, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + NO_EXTERNAL_MIRROR_TABLET( + hasExternalDisplay = false, + extendedDisplayEnabled = false, + tabletModeStatus = SwitchState.ON, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + EXTERNAL_EXTENDED_CLAMSHELL( + hasExternalDisplay = true, + extendedDisplayEnabled = true, + tabletModeStatus = SwitchState.OFF, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + NO_EXTERNAL_EXTENDED_CLAMSHELL( + hasExternalDisplay = false, + extendedDisplayEnabled = true, + tabletModeStatus = SwitchState.OFF, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + EXTERNAL_MIRROR_CLAMSHELL( + hasExternalDisplay = true, + extendedDisplayEnabled = false, + tabletModeStatus = SwitchState.OFF, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + NO_EXTERNAL_MIRROR_CLAMSHELL( + hasExternalDisplay = false, + extendedDisplayEnabled = false, + tabletModeStatus = SwitchState.OFF, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + EXTERNAL_EXTENDED_UNKNOWN( + hasExternalDisplay = true, + extendedDisplayEnabled = true, + tabletModeStatus = SwitchState.UNKNOWN, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + NO_EXTERNAL_EXTENDED_UNKNOWN( + hasExternalDisplay = false, + extendedDisplayEnabled = true, + tabletModeStatus = SwitchState.UNKNOWN, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + EXTERNAL_MIRROR_UNKNOWN( + hasExternalDisplay = true, + extendedDisplayEnabled = false, + tabletModeStatus = SwitchState.UNKNOWN, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + NO_EXTERNAL_MIRROR_UNKNOWN( + hasExternalDisplay = false, extendedDisplayEnabled = false, - expectToSwitchByDefault = false, + tabletModeStatus = SwitchState.UNKNOWN, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt index 006c3cae121c..4c18ee1500b7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt @@ -114,6 +114,8 @@ class DesktopImmersiveControllerTest : ShellTestCase() { transactionSupplier = transactionSupplier, ) desktopRepository = userRepositories.current + desktopRepository.addDesk(DEFAULT_DISPLAY, DEFAULT_DESK_ID) + desktopRepository.setActiveDesk(DEFAULT_DISPLAY, DEFAULT_DESK_ID) } @Test @@ -835,5 +837,6 @@ class DesktopImmersiveControllerTest : ShellTestCase() { companion object { private val STABLE_BOUNDS = Rect(0, 100, 2000, 1900) private val DISPLAY_BOUNDS = Rect(0, 0, 2000, 2000) + private const val DEFAULT_DESK_ID = 0 } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt index e9f92cfd7c56..0c585b3e843a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt @@ -431,6 +431,38 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX, + Flags.FLAG_ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX, + ) + fun startAndAnimateLaunchTransition_withMinimizeChange_wrongTaskId_reparentsMinimizeChange() { + val wct = WindowContainerTransaction() + val launchingTask = createTask(WINDOWING_MODE_FREEFORM) + val minimizingTask = createTask(WINDOWING_MODE_FREEFORM) + val launchTaskChange = createChange(launchingTask, mode = TRANSIT_OPEN) + val minimizeChange = createChange(minimizingTask) + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(transition) + + mixedHandler.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + taskId = Int.MAX_VALUE, + minimizingTaskId = minimizingTask.taskId, + ) + mixedHandler.startAnimation( + transition, + createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange, minimizeChange)), + SurfaceControl.Transaction(), + SurfaceControl.Transaction(), + ) {} + + verify(rootTaskDisplayAreaOrganizer) + .reparentToDisplayArea(anyInt(), eq(minimizeChange.leash), any()) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX) fun startAnimation_pendingTransition_noLaunchChange_returnsFalse() { val wct = WindowContainerTransaction() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt index fbc940663d19..6a99d4770728 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt @@ -16,8 +16,10 @@ package com.android.wm.shell.desktopmode +import android.graphics.Rect import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper +import android.view.SurfaceControl import android.view.WindowManager import android.window.TransitionInfo import androidx.test.filters.SmallTest @@ -30,6 +32,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify @SmallTest @RunWithLooper @@ -55,7 +59,9 @@ class DesktopModeMoveToDisplayTransitionHandlerTest : ShellTestCase() { info = TransitionInfo(WindowManager.TRANSIT_CHANGE, /* flags= */ 0).apply { addChange( - TransitionInfo.Change(mock(), mock()).apply { setDisplayId(1, 1) } + TransitionInfo.Change(mock(), mock()).apply { + setDisplayId(/* start= */ 1, /* end= */ 1) + } ) }, startTransaction = StubTransaction(), @@ -74,7 +80,9 @@ class DesktopModeMoveToDisplayTransitionHandlerTest : ShellTestCase() { info = TransitionInfo(WindowManager.TRANSIT_CHANGE, /* flags= */ 0).apply { addChange( - TransitionInfo.Change(mock(), mock()).apply { setDisplayId(1, 2) } + TransitionInfo.Change(mock(), mock()).apply { + setDisplayId(/* start= */ 1, /* end= */ 2) + } ) }, startTransaction = StubTransaction(), @@ -84,4 +92,77 @@ class DesktopModeMoveToDisplayTransitionHandlerTest : ShellTestCase() { assertTrue("Should animate display change transition", animates) } + + @Test + fun startAnimation_movingActivityEmbedding_shouldSetCorrectBounds() { + val leashLeft = mock<SurfaceControl>() + val leashRight = mock<SurfaceControl>() + val leashContainer = mock<SurfaceControl>() + val startTransaction = spy(StubTransaction()) + + handler.startAnimation( + transition = mock(), + info = + TransitionInfo(WindowManager.TRANSIT_CHANGE, /* flags= */ 0).apply { + addChange( + TransitionInfo.Change(mock(), mock()).apply { + flags = TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY + leash = leashLeft + setDisplayId(/* start= */ 1, /* end= */ 2) + setEndAbsBounds( + Rect( + /* left= */ 100, + /* top= */ 100, + /* right= */ 500, + /* bottom= */ 700, + ) + ) + setEndRelOffset(/* left= */ 0, /* top= */ 0) + } + ) + addChange( + TransitionInfo.Change(mock(), mock()).apply { + flags = TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY + leash = leashRight + setDisplayId(1, 2) + setEndAbsBounds( + Rect( + /* left= */ 500, + /* top= */ 100, + /* right= */ 900, + /* bottom= */ 700, + ) + ) + setEndRelOffset(/* left= */ 400, /* top= */ 0) + } + ) + addChange( + TransitionInfo.Change(mock(), mock()).apply { + flags = TransitionInfo.FLAG_TRANSLUCENT + leash = leashContainer + setDisplayId(/* start= */ 1, /* end= */ 2) + setEndAbsBounds( + Rect( + /* left= */ 100, + /* top= */ 100, + /* right= */ 900, + /* bottom= */ 700, + ) + ) + setEndRelOffset(/* left= */ 100, /* top= */ 100) + } + ) + }, + startTransaction = startTransaction, + finishTransaction = StubTransaction(), + finishCallback = mock(), + ) + + verify(startTransaction).setPosition(leashLeft, 0f, 0f) + verify(startTransaction).setPosition(leashRight, 400f, 0f) + verify(startTransaction).setPosition(leashContainer, 100f, 100f) + verify(startTransaction).setWindowCrop(leashLeft, 400, 600) + verify(startTransaction).setWindowCrop(leashRight, 400, 600) + verify(startTransaction).setWindowCrop(leashContainer, 800, 600) + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt index fe1dc29181b9..b859a00c6df4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt @@ -24,7 +24,6 @@ import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper -import android.view.Display import android.view.SurfaceControl import androidx.test.filters.SmallTest import com.android.internal.policy.SystemBarUtils @@ -67,7 +66,6 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { private lateinit var taskInfo: RunningTaskInfo @Mock private lateinit var syncQueue: SyncTransactionQueue @Mock private lateinit var displayController: DisplayController - @Mock private lateinit var display: Display @Mock private lateinit var taskSurface: SurfaceControl @Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var displayLayout: DisplayLayout @@ -83,12 +81,20 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width()) whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height()) whenever(displayLayout.stableInsets()).thenReturn(STABLE_INSETS) - whenever(displayController.getDisplay(anyInt())).thenReturn(display) whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) whenever(displayController.getDisplay(anyInt())).thenReturn(mContext.display) whenever(bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(any())) .thenReturn(Rect()) taskInfo = DesktopTestHelpers.createFullscreenTask() + + mContext.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_isDesktopModeSupported, + true, + ) + mContext.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_canInternalDisplayHostDesktops, + true, + ) } @Test @@ -260,14 +266,9 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { ) fun testDefaultIndicatorWithNoDesktop() { mContext.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_isDesktopModeSupported, + com.android.internal.R.bool.config_canInternalDisplayHostDesktops, false, ) - mContext.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_isDesktopModeDevOptionSupported, - false, - ) - // Fullscreen to center, no desktop indicator createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) var result = visualIndicator.updateIndicatorType(PointF(500f, 500f)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index de92d391645a..f84a1a38bdfc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -1203,6 +1203,17 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun removeDesk_removesFromPersistence() = + runTest(StandardTestDispatcher()) { + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 2) + + repo.removeDesk(deskId = 2) + + verify(persistentRepository).removeDesktop(DEFAULT_USER_ID, 2) + } + + @Test fun getTaskInFullImmersiveState_byDisplay() { repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) 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 8f499c959759..34c5ebd6d94d 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 @@ -22,6 +22,7 @@ import android.app.ActivityOptions import android.app.KeyguardManager import android.app.PendingIntent import android.app.PictureInPictureParams +import android.app.WindowConfiguration import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM @@ -31,6 +32,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo.CONFIG_DENSITY import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE @@ -101,6 +103,7 @@ import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.bubbles.BubbleController import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.HomeIntentProvider import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue @@ -275,6 +278,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() private lateinit var testScope: CoroutineScope private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy private lateinit var spyContext: TestableContext + private lateinit var homeIntentProvider: HomeIntentProvider private val shellExecutor = TestShellExecutor() private val bgExecutor = TestShellExecutor() @@ -294,6 +298,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() private val wallpaperToken = MockToken().token() private val homeComponentName = ComponentName(HOME_LAUNCHER_PACKAGE_NAME, /* class */ "") + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + @Before fun setUp() { Dispatchers.setMain(StandardTestDispatcher()) @@ -323,12 +331,14 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() transitions, userRepositories, shellTaskOrganizer, + desksOrganizer, MAX_TASK_LIMIT, mockInteractionJankMonitor, mContext, mockHandler, ) desktopModeCompatPolicy = spy(DesktopModeCompatPolicy(spyContext)) + homeIntentProvider = HomeIntentProvider(context) whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } whenever(transitions.startTransition(anyInt(), any(), anyOrNull())).thenAnswer { Binder() } @@ -428,6 +438,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() dragToDesktopTransitionHandler, mMockDesktopImmersiveController, userRepositories, + repositoryInitializer, recentsTransitionHandler, multiInstanceHelper, shellExecutor, @@ -448,6 +459,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() desktopModeCompatPolicy, dragToDisplayTransitionHandler, moveToDisplayTransitionHandler, + homeIntentProvider, ) @After @@ -621,11 +633,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun isDesktopModeShowing_noTasks_returnsFalse() { assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun isDesktopModeShowing_noTasksVisible_returnsFalse() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -636,6 +650,15 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun isDesktopModeShowing_noActiveDesk_returnsFalse() { + taskRepository.setDeskInactive(deskId = 0) + + assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -650,6 +673,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC, ) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun isDesktopModeShowing_topTransparentFullscreenTask_returnsTrue() { val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId) @@ -659,6 +683,20 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun isDesktopModeShowing_deskInactive_topTransparentFullscreenTask_returnsTrue() { + val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId) + taskRepository.setDeskInactive(deskId = 0) + + assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isTrue() + } + + @Test + @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, ) @@ -1047,11 +1085,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun isAnyDeskActive_noTasks_returnsFalse() { assertThat(controller.isAnyDeskActive(DEFAULT_DISPLAY)).isFalse() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun isAnyDeskActive_noActiveDesk_returnsFalse() { + taskRepository.setDeskInactive(deskId = 0) + + assertThat(controller.isAnyDeskActive(DEFAULT_DISPLAY)).isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun isAnyDeskActive_withActiveDesk_returnsTrue() { + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + + assertThat(controller.isAnyDeskActive(DEFAULT_DISPLAY)).isTrue() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun isAnyDeskActive_twoTasks_bothVisible_returnsTrue() { setUpHomeTask() @@ -1062,6 +1118,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun isInDesktop_twoTasks_oneVisible_returnsTrue() { setUpHomeTask() @@ -1072,6 +1129,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun isAnyDeskActive_twoTasksVisibleOnDifferentDisplays_returnsTrue() { taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) @@ -1091,7 +1149,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.addMoveToDeskTaskChanges(wct, task, deskId = 0) val finalBounds = findBoundsChange(wct, task) - assertThat(finalBounds).isEqualTo(Rect()) + assertThat(finalBounds).isNull() } @Test @@ -1102,7 +1160,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.addMoveToDeskTaskChanges(wct, task, deskId = 0) val finalBounds = findBoundsChange(wct, task) - assertThat(finalBounds).isEqualTo(Rect()) + assertThat(finalBounds).isNull() } @Test @@ -1113,7 +1171,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.addMoveToDeskTaskChanges(wct, task, deskId = 0) val finalBounds = findBoundsChange(wct, task) - assertThat(finalBounds).isEqualTo(Rect()) + assertThat(finalBounds).isNull() } @Test @@ -1124,7 +1182,55 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.addMoveToDeskTaskChanges(wct, task, deskId = 0) val finalBounds = findBoundsChange(wct, task) - assertThat(finalBounds).isEqualTo(Rect()) + assertThat(finalBounds).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES) + fun addMoveToDeskTaskChanges_newTaskInstance_inheritsClosingInstanceBounds() { + // Setup existing task. + val existingTask = setUpFreeformTask(active = true) + val testComponent = ComponentName(/* package */ "test.package", /* class */ "test.class") + existingTask.topActivity = testComponent + existingTask.configuration.windowConfiguration.setBounds(Rect(0, 0, 500, 500)) + // Set up new instance of already existing task. + val launchingTask = setUpFullscreenTask() + launchingTask.topActivity = testComponent + launchingTask.baseIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) + + // Move new instance to desktop. By default multi instance is not supported so first + // instance will close. + val wct = WindowContainerTransaction() + controller.addMoveToDeskTaskChanges(wct, launchingTask, deskId = 0) + + // New instance should inherit task bounds of old instance. + assertThat(findBoundsChange(wct, launchingTask)) + .isEqualTo(existingTask.configuration.windowConfiguration.bounds) + } + + @Test + @EnableFlags(Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES) + fun handleRequest_newTaskInstance_inheritsClosingInstanceBounds() { + setUpLandscapeDisplay() + // Setup existing task. + val existingTask = setUpFreeformTask(active = true) + val testComponent = ComponentName(/* package */ "test.package", /* class */ "test.class") + existingTask.topActivity = testComponent + existingTask.configuration.windowConfiguration.setBounds(Rect(0, 0, 500, 500)) + // Set up new instance of already existing task. + val launchingTask = setUpFreeformTask(active = false) + taskRepository.removeTask(launchingTask.displayId, launchingTask.taskId) + launchingTask.topActivity = testComponent + launchingTask.baseIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) + + // Move new instance to desktop. By default multi instance is not supported so first + // instance will close. + val wct = controller.handleRequest(Binder(), createTransition(launchingTask)) + + assertNotNull(wct, "should handle request") + val finalBounds = findBoundsChange(wct, launchingTask) + // New instance should inherit task bounds of old instance. + assertThat(finalBounds).isEqualTo(existingTask.configuration.windowConfiguration.bounds) } @Test @@ -1808,6 +1914,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val wallpaperToken = MockToken().token() whenever(desktopWallpaperActivityTokenProvider.getToken(SECOND_DISPLAY)) .thenReturn(wallpaperToken) + taskRepository.addDesk(SECOND_DISPLAY, deskId = 2) val task = setUpFreeformTask(displayId = SECOND_DISPLAY, deskId = 2, background = true) controller.moveTaskToDefaultDeskAndActivate( @@ -2363,6 +2470,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveTaskToFront_postsWctWithReorderOp() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -2385,9 +2493,34 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() { + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveTaskToFront_reordersToFront() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0) + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0) + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + eq(TRANSIT_TO_FRONT), + any(), + eq(task1.taskId), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(Binder()) + + controller.moveTaskToFront(task1, remoteTransition = null) + + verify(desksOrganizer).reorderTaskToFront(any(), eq(0), eq(task1)) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveTaskToFront_bringsTasksOverLimit_multiDesksDisabled_minimizesBackTask() { setUpHomeTask() - val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() } + val freeformTasks = + (1..MAX_TASK_LIMIT + 1).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0) + } whenever( desktopMixedTransitionHandler.startLaunchTransition( eq(TRANSIT_TO_FRONT), @@ -2408,6 +2541,32 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveTaskToFront_bringsTasksOverLimit_multiDesksEnabled_minimizesBackTask() { + val deskId = 0 + setUpHomeTask() + val freeformTasks = + (1..MAX_TASK_LIMIT + 1).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + } + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + eq(TRANSIT_TO_FRONT), + any(), + eq(freeformTasks[0].taskId), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(Binder()) + + controller.moveTaskToFront(freeformTasks[0], remoteTransition = null) + + val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT) + verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[1]) + } + + @Test fun moveTaskToFront_minimizedTask_marksTaskAsUnminimized() { val transition = Binder() val freeformTask = setUpFreeformTask() @@ -2496,8 +2655,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveTaskToFront_backgroundTaskBringsTasksOverLimit_minimizesBackTask() { - val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveTaskToFront_backgroundTaskBringsTasksOverLimit_multiDesksDisabled_minimizesBackTask() { + val deskId = 0 + val freeformTasks = + (1..MAX_TASK_LIMIT).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + } val task = createRecentTaskInfo(1001) whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null) whenever( @@ -2520,6 +2684,33 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveTaskToFront_backgroundTaskBringsTasksOverLimit_multiDesksEnabled_minimizesBackTask() { + val deskId = 0 + val freeformTasks = + (1..MAX_TASK_LIMIT).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + } + val task = createRecentTaskInfo(1001) + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null) + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + eq(TRANSIT_OPEN), + any(), + eq(task.taskId), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(Binder()) + + controller.moveTaskToFront(task.taskId, unminimizeReason = UnminimizeReason.UNKNOWN) + + val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN) + verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[0]) + } + + @Test fun moveToNextDisplay_noOtherDisplays() { whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY)) val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) @@ -2528,7 +2719,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveToNextDisplay_moveFromFirstToSecondDisplay() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToNextDisplay_moveFromFirstToSecondDisplay_multiDesksDisabled() { // Set up two display ids taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) whenever(rootTaskDisplayAreaOrganizer.displayIds) @@ -2554,7 +2746,27 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveToNextDisplay_moveFromSecondToFirstDisplay() { + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToNextDisplay_moveFromFirstToSecondDisplay_multiDesksEnabled() { + // Set up two display ids + val targetDeskId = 2 + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = targetDeskId) + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: second display + val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) + .thenReturn(secondDisplayArea) + + val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + controller.moveToNextDisplay(task.taskId) + + verify(desksOrganizer).moveTaskToDesk(any(), eq(targetDeskId), eq(task)) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToNextDisplay_moveFromSecondToFirstDisplay_multiDesksDisabled() { // Set up two display ids whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) @@ -2580,6 +2792,25 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToNextDisplay_moveFromSecondToFirstDisplay_multiDesksEnabled() { + // Set up two display ids + val targetDeskId = 0 + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: default display + val defaultDisplayArea = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .thenReturn(defaultDisplayArea) + + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + val task = setUpFreeformTask(displayId = SECOND_DISPLAY) + controller.moveToNextDisplay(task.taskId) + + verify(desksOrganizer).moveTaskToDesk(any(), eq(targetDeskId), eq(task)) + } + + @Test @EnableFlags( FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, @@ -2643,6 +2874,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT) fun moveToNextDisplay_sizeInDpPreserved() { // Set up two display ids + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2) whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) // Create a mock for the target display area: second display @@ -2684,6 +2916,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT) fun moveToNextDisplay_shiftWithinDestinationDisplayBounds() { // Set up two display ids + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2) whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) // Create a mock for the target display area: second display @@ -2725,6 +2958,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() // Set up two display ids whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2) // Create a mock for the target display area: second display val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) @@ -2833,7 +3067,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveToNextDisplay_toDesktopInOtherDisplay_bringsExistingTasksToFront() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToNextDisplay_toDesktopInOtherDisplay_multiDesksDisabled_bringsExistingTasksToFront() { val transition = Binder() val sourceDeskId = 0 val targetDeskId = 2 @@ -2859,6 +3094,34 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToNextDisplay_toDesktopInOtherDisplay_multiDesksEnabled_bringsExistingTasksToFront() { + val transition = Binder() + val sourceDeskId = 0 + val targetDeskId = 2 + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = targetDeskId) + taskRepository.setDeskInactive(deskId = targetDeskId) + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: second display + val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) + .thenReturn(secondDisplayArea) + whenever(transitions.startTransition(eq(TRANSIT_CHANGE), any(), anyOrNull())) + .thenReturn(transition) + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = sourceDeskId) + val task2 = setUpFreeformTask(displayId = SECOND_DISPLAY, deskId = targetDeskId) + + controller.moveToNextDisplay(task1.taskId) + + // Existing desktop task in the target display is moved to front. + val wct = getLatestTransition() + assertNotNull(wct) + verify(desksOrganizer).reorderTaskToFront(wct, targetDeskId, task2) + } + + @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, @@ -2972,7 +3235,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test fun moveToNextDisplay_movingToDesktop_sendsTaskbarRoundingUpdate() { val transition = Binder() - val sourceDeskId = 1 + val sourceDeskId = 0 val targetDeskId = 2 taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = targetDeskId) taskRepository.setDeskInactive(deskId = targetDeskId) @@ -3489,7 +3752,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTask_freeformVisible_multiDesksDisabled_returnSwitchToFreeformWCT() { val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() markTaskVisible(freeformTask) @@ -3505,7 +3769,22 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun handleRequest_fullscreenTaskWithTaskOnHome_freeformVisible_returnSwitchToFreeformWCT() { + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTask_deskActive_multiDesksEnabled_movesToDesk() { + val deskId = 0 + taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId = deskId) + setUpHomeTask() + val fullscreenTask = createFullscreenTask() + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct, "should handle request") + verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskWithTaskOnHome_freeformVisible_multiDesksDisabled_returnSwitchToFreeformWCT() { val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() markTaskVisible(freeformTask) @@ -3530,7 +3809,23 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun handleRequest_fullscreenTaskToFreeform_underTaskLimit_dontMinimize() { + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskWithTaskOnHome_activeDesk_multiDesksEnabled_movesToDesk() { + val deskId = 0 + setUpHomeTask() + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + val fullscreenTask = createFullscreenTask() + fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct, "should handle request") + verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskToDesk_underTaskLimit_multiDesksDisabled_dontMinimize() { val freeformTask = setUpFreeformTask() markTaskVisible(freeformTask) val fullscreenTask = createFullscreenTask() @@ -3543,7 +3838,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() { + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskToDesk_underTaskLimit_multiDesksEnabled_dontMinimize() { + val deskId = 5 + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + val freeformTask = + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId).also { + markTaskVisible(it) + } + + // Launch a fullscreen task while in the desk. + val fullscreenTask = createFullscreenTask() + val transition = Binder() + val wct = controller.handleRequest(transition, createTransition(fullscreenTask)) + + assertNotNull(wct) + verify(desksOrganizer, never()).minimizeTask(eq(wct), eq(deskId), any()) + assertNull(desktopTasksLimiter.getMinimizingTask(transition)) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskToDesk_bringsTasksOverLimit_multiDesksDisabled_otherTaskIsMinimized() { val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } freeformTasks.forEach { markTaskVisible(it) } val fullscreenTask = createFullscreenTask() @@ -3557,7 +3874,34 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_otherTaskIsMinimized() { + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskToDesk_bringsTasksOverLimit_multiDesksEnabled_otherTaskIsMinimized() { + val deskId = 5 + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + val freeformTasks = + (1..MAX_TASK_LIMIT).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId).also { + markTaskVisible(it) + } + } + + // Launch a fullscreen task while in the desk. + setUpHomeTask() + val fullscreenTask = createFullscreenTask() + val transition = Binder() + val wct = controller.handleRequest(transition, createTransition(fullscreenTask)) + + assertNotNull(wct) + verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[0]) + val minimizingTask = + assertNotNull(desktopTasksLimiter.getMinimizingTask(transition)?.taskId) + assertThat(minimizingTask).isEqualTo(freeformTasks[0].taskId) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_multiDesksDisabled_otherTaskIsMinimized() { val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } freeformTasks.forEach { markTaskVisible(it) } val fullscreenTask = createFullscreenTask() @@ -3578,7 +3922,31 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_existingAndNewTasksAreMinimized() { + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_multiDesksEnabled_otherTaskIsMinimized() { + val deskId = 5 + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + val freeformTasks = + (1..MAX_TASK_LIMIT).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + } + freeformTasks.forEach { markTaskVisible(it) } + val fullscreenTask = createFullscreenTask() + fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct) + // The launching task is moved to the desk. + verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask) + // The bottom-most task is minimized. + verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[0]) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_multiDesksDisabled_existingAndNewTasksAreMinimized() { val minimizedTask = setUpFreeformTask() taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = minimizedTask.taskId) val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } @@ -3604,7 +3972,90 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_multiDesksEnabled_existingAndNewTasksAreMinimized() { + // A desk with a minimized tasks, and non-minimized tasks already at the task limit. + val deskId = 5 + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + val minimizedTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + taskRepository.minimizeTaskInDesk( + displayId = DEFAULT_DISPLAY, + deskId = deskId, + taskId = minimizedTask.taskId, + ) + val freeformTasks = + (1..MAX_TASK_LIMIT).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId).also { + markTaskVisible(it) + } + } + + // Launch a fullscreen task that brings Home to front with it. + setUpHomeTask() + val fullscreenTask = createFullscreenTask() + fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct) + verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[0]) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTaskWithTaskOnHome_taskAddedToDesk() { + // A desk with a minimized tasks, and non-minimized tasks already at the task limit. + val deskId = 5 + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + + // Launch a fullscreen task that brings Home to front with it. + setUpHomeTask() + val fullscreenTask = createFullscreenTask() + fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct) + verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + ) + fun handleRequest_fullscreenTaskWithTaskOnHome_activatesDesk() { + val deskId = 5 + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId) + + // Launch a fullscreen task that brings Home to front with it. + val homeTask = setUpHomeTask() + val fullscreenTask = createFullscreenTask() + fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) + val transition = Binder() + val wct = controller.handleRequest(transition, createTransition(fullscreenTask)) + + assertNotNull(wct) + wct.assertReorder(homeTask, toTop = true) + wct.assertReorder(wallpaperToken, toTop = true) + verify(desksOrganizer).activateDesk(wct, deskId) + verify(desksTransitionsObserver) + .addPendingTransition( + DeskTransition.ActiveDeskWithTask( + token = transition, + displayId = DEFAULT_DISPLAY, + deskId = deskId, + enterTaskId = fullscreenTask.taskId, + ) + ) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() { whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) @@ -3627,7 +4078,28 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun handleRequest_fullscreenTask_noTasks_enforceDesktop_fullscreenDisplay_returnNull() { + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun handleRequest_fullscreenTask_noInDesk_enforceDesktop_freeformDisplay_movesToDesk() { + val deskId = 0 + taskRepository.setDeskInactive(deskId) + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + + val fullscreenTask = createFullscreenTask() + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct, "should handle request") + verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask) + } + + @Test + fun handleRequest_fullscreenTask_notInDesk_enforceDesktop_fullscreenDisplay_returnNull() { + taskRepository.setDeskInactive(deskId = 0) whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN @@ -3639,6 +4111,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() { val freeformTask = setUpFreeformTask() markTaskHidden(freeformTask) @@ -3647,12 +4120,23 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun handleRequest_fullscreenTask_noOtherTasks_returnNull() { val fullscreenTask = createFullscreenTask() assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTask_notInDesk_returnNull() { + taskRepository.setDeskInactive(deskId = 0) + val fullscreenTask = createFullscreenTask() + + assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() { val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY) createFreeformTask(displayId = SECOND_DISPLAY) @@ -3663,8 +4147,27 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() { - val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_fullscreenTask_deskInOtherDisplayActive_returnNull() { + taskRepository.setDeskInactive(deskId = 0) + val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2) + taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = 2) + + val result = + controller.handleRequest(Binder(), createTransition(fullscreenTaskDefaultDisplay)) + + assertThat(result).isNull() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_multiDesksDisabled_minimize() { + val deskId = 0 + val freeformTasks = + (1..MAX_TASK_LIMIT).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + } freeformTasks.forEach { markTaskVisible(it) } val newFreeformTask = createFreeformTask() @@ -3677,6 +4180,24 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_multiDesksEnabled_minimize() { + val deskId = 0 + val freeformTasks = + (1..MAX_TASK_LIMIT).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + } + freeformTasks.forEach { markTaskVisible(it) } + val newFreeformTask = createFreeformTask() + + val wct = + controller.handleRequest(Binder(), createTransition(newFreeformTask, TRANSIT_OPEN)) + + assertNotNull(wct) + verify(desksOrganizer).minimizeTask(wct, deskId, freeformTasks[0]) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun handleRequest_freeformTaskFromInactiveDesk_tracksDeskDeactivation() { val deskId = 0 val freeformTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) @@ -3691,7 +4212,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test fun handleRequest_freeformTask_relaunchActiveTask_taskBecomesUndefined() { - val freeformTask = setUpFreeformTask() + taskRepository.setDeskInactive(deskId = 0) + val freeformTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0) markTaskHidden(freeformTask) val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) @@ -3721,9 +4243,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - - val freeformTask = setUpFreeformTask() + taskRepository.setDeskInactive(deskId = 0) + val freeformTask = setUpFreeformTask(DEFAULT_DISPLAY, deskId = 0) markTaskHidden(freeformTask) + val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) assertNotNull(wct, "should handle request") @@ -3732,7 +4255,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() { val freeformTask1 = setUpFreeformTask() val freeformTask2 = createFreeformTask() @@ -3751,7 +4277,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_multiDesksDisabled_reorderedToTop() { whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val freeformTask1 = setUpFreeformTask() val freeformTask2 = createFreeformTask() @@ -3774,34 +4301,47 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() { - val task = createFreeformTask() - val result = controller.handleRequest(Binder(), createTransition(task)) + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun handleRequest_freeformTask_desktopWallpaperEnabled_notInDesk_reorderedToTop() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) + val deskId = 0 + taskRepository.setDeskInactive(deskId) + val freeformTask1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + val freeformTask2 = createFreeformTask() - assertNotNull(result, "Should handle request") - assertThat(result.hierarchyOps?.size).isEqualTo(1) - result.assertReorderAt(0, task, toTop = true) + val wct = + controller.handleRequest( + Binder(), + createTransition(freeformTask2, type = TRANSIT_TO_FRONT), + ) + + assertNotNull(wct, "Should handle request") + verify(desksOrganizer).reorderTaskToFront(wct, deskId, freeformTask1) + wct.assertReorder(freeformTask2, toTop = true) } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() { - whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() { val task = createFreeformTask() - val result = controller.handleRequest(Binder(), createTransition(task)) assertNotNull(result, "Should handle request") - assertThat(result.hierarchyOps?.size).isEqualTo(2) - // Add desktop wallpaper activity - result.assertPendingIntentAt(0, desktopWallpaperIntent) - // Bring new task to front - result.assertReorderAt(1, task, toTop = true) + assertThat(result.hierarchyOps?.size).isEqualTo(1) + result.assertReorderAt(0, task, toTop = true) } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun handleRequest_freeformTask_dskWallpaperDisabled_freeformOnOtherDisplayOnly_reorderedToTop() { val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) // Second display task @@ -3817,10 +4357,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() { + taskRepository.setDeskInactive(deskId = 0) whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) // Second display task - createFreeformTask(displayId = SECOND_DISPLAY) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2) + taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = 2) + setUpFreeformTask(displayId = SECOND_DISPLAY, deskId = 2) val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) @@ -3956,7 +4499,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - fun handleRequest_topActivityTransparentWithoutDisplay_returnSwitchToFreeformWCT() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_topActivityTransparentWithoutDisplay_multiDesksDisabled_returnSwitchToFreeformWCT() { val freeformTask = setUpFreeformTask() markTaskVisible(freeformTask) @@ -3973,6 +4517,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun handleRequest_topActivityTransparentWithoutDisplay_multiDesksEnabled_returnSwitchToFreeformWCT() { + val deskId = 0 + val freeformTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + markTaskVisible(freeformTask) + + val task = + setUpFullscreenTask().apply { + isActivityStackTransparent = true + isTopActivityNoDisplay = true + numActivities = 1 + } + + val wct = controller.handleRequest(Binder(), createTransition(task)) + + assertNotNull(wct) + verify(desksOrganizer).moveTaskToDesk(wct, deskId, task) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) @DisableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION) fun handleRequest_topActivityTransparentWithDisplay_returnSwitchToFullscreenWCT() { @@ -4031,7 +4598,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC, ) - fun handleRequest_onlyTopTransparentFullscreenTask_returnSwitchToFreeformWCT() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_onlyTopTransparentFullscreenTask_multiDesksDisabled_returnSwitchToFreeformWCT() { val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId) @@ -4043,8 +4611,28 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun handleRequest_onlyTopTransparentFullscreenTask_multiDesksEnabled_movesToDesktop() { + val deskId = 0 + val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId) + taskRepository.setDeskInactive(deskId = deskId) + + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + + val wct = controller.handleRequest(Binder(), createTransition(task)) + assertNotNull(wct) + verify(desksOrganizer).moveTaskToDesk(wct, deskId, task) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) fun handleRequest_desktopNotShowing_topTransparentFullscreenTask_returnNull() { + taskRepository.setDeskInactive(deskId = 0) val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() @@ -4098,7 +4686,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - fun handleRequest_systemUIActivityWithoutDisplay_returnSwitchToFreeformWCT() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun handleRequest_systemUIActivityWithoutDisplay_multiDesksDisabled_returnSwitchToFreeformWCT() { val freeformTask = setUpFreeformTask() markTaskVisible(freeformTask) @@ -4117,6 +4706,32 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() .isEqualTo(WINDOWING_MODE_FREEFORM) } + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun handleRequest_systemUIActivityWithoutDisplay_multiDesksEnabled_movesTaskToDesk() { + val deskId = 0 + val freeformTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + markTaskVisible(freeformTask) + + // Set task as systemUI package + val systemUIPackageName = + context.resources.getString(com.android.internal.R.string.config_systemUi) + val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "") + val task = + setUpFullscreenTask(displayId = DEFAULT_DISPLAY).apply { + baseActivity = baseComponent + isTopActivityNoDisplay = true + } + + val wct = controller.handleRequest(Binder(), createTransition(task)) + + assertNotNull(wct) + verify(desksOrganizer).moveTaskToDesk(wct, deskId, task) + } + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) fun handleRequest_defaultHomePackageWithDisplay_returnSwitchToFullscreenWCT() { val freeformTask = setUpFreeformTask() @@ -4159,6 +4774,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test fun handleRequest_systemUIActivityWithDisplay_returnSwitchToFullscreenWCT_enforcedDesktop() { + taskRepository.setDeskInactive(deskId = 0) whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM @@ -4792,6 +5408,55 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, ) + fun activateDesk_hasNonRunningTask_startsTask() { + val deskId = 0 + val nonRunningTask = + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0, background = true) + + val transition = Binder() + val deskChange = mock(TransitionInfo.Change::class.java) + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(transition) + whenever(desksOrganizer.isDeskActiveAtEnd(deskChange, deskId)).thenReturn(true) + // Make desk inactive by activating another desk. + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 1) + taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId = 1) + + controller.activateDesk(deskId, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct(TRANSIT_TO_FRONT, OneShotRemoteHandler::class.java) + assertNotNull(wct) + wct.assertLaunchTask(nonRunningTask.taskId, WINDOWING_MODE_FREEFORM) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun activateDesk_hasRunningTask_reordersTask() { + val deskId = 0 + val runningTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0) + + val transition = Binder() + val deskChange = mock(TransitionInfo.Change::class.java) + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(transition) + whenever(desksOrganizer.isDeskActiveAtEnd(deskChange, deskId)).thenReturn(true) + // Make desk inactive by activating another desk. + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 1) + taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId = 1) + + controller.activateDesk(deskId, RemoteTransition(TestRemoteTransition())) + + verify(desksOrganizer).reorderTaskToFront(any(), eq(deskId), eq(runningTask)) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun moveTaskToDesk_multipleDesks_addsPendingTransition() { val transition = Binder() whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenReturn(transition) @@ -5578,7 +6243,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) - fun openInstance_fromFreeformAddsNewWindow() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun openInstance_fromFreeformAddsNewWindow_multiDesksDisabled() { setUpLandscapeDisplay() val task = setUpFreeformTask() val taskToRequest = setUpFreeformTask() @@ -5592,7 +6258,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() ) ) .thenReturn(Binder()) + runOpenInstance(task, taskToRequest.taskId) + verify(desktopMixedTransitionHandler) .startLaunchTransition(anyInt(), any(), anyInt(), anyOrNull(), anyOrNull()) val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT) @@ -5601,10 +6269,42 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun openInstance_fromFreeformAddsNewWindow_multiDesksEnabled() { + setUpLandscapeDisplay() + val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0) + val taskToRequest = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0) + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + eq(TRANSIT_TO_FRONT), + any(), + eq(taskToRequest.taskId), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(Binder()) + + runOpenInstance(task, taskToRequest.taskId) + + verify(desktopMixedTransitionHandler) + .startLaunchTransition(anyInt(), any(), anyInt(), anyOrNull(), anyOrNull()) + verify(desksOrganizer).reorderTaskToFront(any(), eq(0), eq(taskToRequest)) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) - fun openInstance_fromFreeform_minimizesIfNeeded() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun openInstance_fromFreeform_multiDesksDisabled_minimizesIfNeeded() { setUpLandscapeDisplay() - val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() } + val deskId = 0 + val freeformTasks = + (1..MAX_TASK_LIMIT + 1).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + } val oldestTask = freeformTasks.first() val newestTask = freeformTasks.last() @@ -5630,6 +6330,40 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun openInstance_fromFreeform_multiDesksEnabled_minimizesIfNeeded() { + setUpLandscapeDisplay() + val deskId = 0 + val freeformTasks = + (1..MAX_TASK_LIMIT + 1).map { _ -> + setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId) + } + val oldestTask = freeformTasks.first() + val newestTask = freeformTasks.last() + + val transition = Binder() + val wctCaptor = argumentCaptor<WindowContainerTransaction>() + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + anyInt(), + wctCaptor.capture(), + anyInt(), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(transition) + + runOpenInstance(newestTask, freeformTasks[1].taskId) + + val wct = wctCaptor.firstValue + verify(desksOrganizer).minimizeTask(wct, deskId, oldestTask) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) fun openInstance_fromFreeform_exitsImmersiveIfNeeded() { setUpLandscapeDisplay() @@ -6447,6 +7181,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_notShowingDesktop_doesNotPlay() { + taskRepository.setDeskInactive(deskId = 0) val triggerTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) taskRepository.setTaskInFullImmersiveState( displayId = triggerTask.displayId, @@ -6523,6 +7258,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_fullscreenStaysFullscreen_doesNotPlay() { val triggerTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + taskRepository.setDeskInactive(deskId = 0) assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse() assertThat( @@ -6558,6 +7294,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_freeformExitsDesktop_doesNotPlay() { val triggerTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, active = false) + taskRepository.setDeskInactive(deskId = 0) assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse() assertThat( @@ -6587,7 +7324,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, ) fun startLaunchTransition_desktopNotShowing_movesWallpaperToFront() { - val launchingTask = createFreeformTask() + taskRepository.setDeskInactive(deskId = 0) + val launchingTask = createFreeformTask(displayId = DEFAULT_DISPLAY) val wct = WindowContainerTransaction() wct.reorder(launchingTask.token, /* onTop= */ true) whenever( @@ -6601,7 +7339,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() ) .thenReturn(Binder()) - controller.startLaunchTransition(TRANSIT_OPEN, wct, launchingTaskId = null) + controller.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + launchingTaskId = null, + deskId = 0, + displayId = DEFAULT_DISPLAY, + ) val latestWct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN) val launchingTaskReorderIndex = latestWct.indexOfReorder(launchingTask, toTop = true) @@ -6625,6 +7369,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() transitionType = TRANSIT_OPEN, wct = WindowContainerTransaction(), launchingTaskId = null, + deskId = 0, + displayId = DEFAULT_DISPLAY, ) verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(any()) @@ -6634,6 +7380,51 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun startLaunchTransition_launchingTaskFromInactiveDesk_otherDeskActive_activatesDesk() { + val activeDeskId = 4 + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = activeDeskId) + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = activeDeskId) + val inactiveDesk = 5 + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = inactiveDesk) + val launchingTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = inactiveDesk) + val transition = Binder() + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + eq(TRANSIT_OPEN), + any(), + eq(launchingTask.taskId), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(transition) + + val wct = WindowContainerTransaction() + controller.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + launchingTaskId = launchingTask.taskId, + deskId = inactiveDesk, + displayId = DEFAULT_DISPLAY, + ) + + verify(desksOrganizer).activateDesk(any(), eq(inactiveDesk)) + verify(desksTransitionsObserver) + .addPendingTransition( + DeskTransition.ActivateDesk( + token = transition, + displayId = DEFAULT_DISPLAY, + deskId = inactiveDesk, + ) + ) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, ) fun startLaunchTransition_desktopShowing_doesNotReorderWallpaper() { val wct = WindowContainerTransaction() @@ -6648,8 +7439,14 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() ) .thenReturn(Binder()) - setUpFreeformTask() - controller.startLaunchTransition(TRANSIT_OPEN, wct, launchingTaskId = null) + setUpFreeformTask(deskId = 0, displayId = DEFAULT_DISPLAY) + controller.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + launchingTaskId = null, + deskId = 0, + displayId = DEFAULT_DISPLAY, + ) val latestWct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN) assertNull(latestWct.hierarchyOps.find { op -> op.container == wallpaperToken.asBinder() }) @@ -6997,7 +7794,15 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? = - wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds + wct.changes.entries + .find { (token, change) -> + token == task.token.asBinder() && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 + } + ?.value + ?.configuration + ?.windowConfiguration + ?.bounds private fun verifyWCTNotExecuted() { verify(transitions, never()).startTransition(anyInt(), any(), isNull()) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt index 76103640c029..eeecb00b5b08 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -39,12 +39,14 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION +import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UnminimizeReason import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask +import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.shared.desktopmode.DesktopModeStatus @@ -67,10 +69,13 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.any import org.mockito.Mockito.mock import org.mockito.Mockito.spy +import org.mockito.Mockito.verify import org.mockito.Mockito.`when` +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.never import org.mockito.quality.Strictness /** @@ -84,6 +89,7 @@ import org.mockito.quality.Strictness class DesktopTasksLimiterTest : ShellTestCase() { @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer + @Mock lateinit var desksOrganizer: DesksOrganizer @Mock lateinit var transitions: Transitions @Mock lateinit var interactionJankMonitor: InteractionJankMonitor @Mock lateinit var handler: Handler @@ -128,6 +134,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { transitions, userRepositories, shellTaskOrganizer, + desksOrganizer, MAX_TASK_LIMIT, interactionJankMonitor, mContext, @@ -148,6 +155,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { transitions, userRepositories, shellTaskOrganizer, + desksOrganizer, 0, interactionJankMonitor, mContext, @@ -163,6 +171,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { transitions, userRepositories, shellTaskOrganizer, + desksOrganizer, -5, interactionJankMonitor, mContext, @@ -178,6 +187,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { transitions, userRepositories, shellTaskOrganizer, + desksOrganizer, maxTasksLimit = null, interactionJankMonitor, mContext, @@ -394,7 +404,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test - fun addAndGetMinimizeTaskChanges_tasksWithinLimit_noTaskMinimized() { + @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addAndGetMinimizeTaskChanges_tasksWithinLimit_multiDesksDisabled_noTaskMinimized() { desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } @@ -402,7 +413,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { val wct = WindowContainerTransaction() val minimizedTaskId = desktopTasksLimiter.addAndGetMinimizeTaskChanges( - displayId = DEFAULT_DISPLAY, + deskId = 0, wct = wct, newFrontTaskId = setUpFreeformTask().taskId, ) @@ -412,7 +423,27 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test - fun addAndGetMinimizeTaskChanges_tasksAboveLimit_backTaskMinimized() { + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addAndGetMinimizeTaskChanges_tasksWithinLimit_multiDesksEnabled_noTaskMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } + + val wct = WindowContainerTransaction() + val minimizedTaskId = + desktopTasksLimiter.addAndGetMinimizeTaskChanges( + deskId = 0, + wct = wct, + newFrontTaskId = setUpFreeformTask().taskId, + ) + + assertThat(minimizedTaskId).isNull() + verify(desksOrganizer, never()).minimizeTask(eq(wct), eq(0), any()) + } + + @Test + @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addAndGetMinimizeTaskChanges_tasksAboveLimit_multiDesksDisabled_backTaskMinimized() { desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) // The following list will be ordered bottom -> top, as the last task is moved to top last. @@ -421,7 +452,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { val wct = WindowContainerTransaction() val minimizedTaskId = desktopTasksLimiter.addAndGetMinimizeTaskChanges( - displayId = DEFAULT_DISPLAY, + deskId = DEFAULT_DISPLAY, wct = wct, newFrontTaskId = setUpFreeformTask().taskId, ) @@ -433,7 +464,28 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test - fun addAndGetMinimizeTaskChanges_nonMinimizedTasksWithinLimit_noTaskMinimized() { + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addAndGetMinimizeTaskChanges_tasksAboveLimit_multiDesksEnabled_backTaskMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + // The following list will be ordered bottom -> top, as the last task is moved to top last. + val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } + + val wct = WindowContainerTransaction() + val minimizedTaskId = + desktopTasksLimiter.addAndGetMinimizeTaskChanges( + deskId = DEFAULT_DISPLAY, + wct = wct, + newFrontTaskId = setUpFreeformTask().taskId, + ) + + assertThat(minimizedTaskId).isEqualTo(tasks.first().taskId) + verify(desksOrganizer).minimizeTask(wct, deskId = 0, tasks.first()) + } + + @Test + @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addAndGetMinimizeTaskChanges_nonMinimizedTasksWithinLimit_multiDesksDisabled_noTaskMinimized() { desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } @@ -442,7 +494,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { val wct = WindowContainerTransaction() val minimizedTaskId = desktopTasksLimiter.addAndGetMinimizeTaskChanges( - displayId = 0, + deskId = 0, wct = wct, newFrontTaskId = setUpFreeformTask().taskId, ) @@ -452,6 +504,26 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addAndGetMinimizeTaskChanges_nonMinimizedTasksWithinLimit_multiDesksEnabled_noTaskMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } + desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = tasks[0].taskId) + + val wct = WindowContainerTransaction() + val minimizedTaskId = + desktopTasksLimiter.addAndGetMinimizeTaskChanges( + deskId = 0, + wct = wct, + newFrontTaskId = setUpFreeformTask().taskId, + ) + + assertThat(minimizedTaskId).isNull() + verify(desksOrganizer, never()).minimizeTask(eq(wct), eq(0), any()) + } + + @Test fun getTaskToMinimize_tasksWithinLimit_returnsNull() { desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) @@ -485,6 +557,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { transitions, userRepositories, shellTaskOrganizer, + desksOrganizer, MAX_TASK_LIMIT2, interactionJankMonitor, mContext, 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 index fd8842b6d99b..a7dc706eb6c9 100644 --- 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 @@ -48,13 +48,11 @@ import com.android.wm.shell.back.BackAnimationController import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider -import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP -import com.android.wm.shell.util.StubTransaction import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import org.junit.Before @@ -95,7 +93,6 @@ class DesktopTasksTransitionObserverTest { private val backAnimationController = mock<BackAnimationController>() private val desktopWallpaperActivityTokenProvider = mock<DesktopWallpaperActivityTokenProvider>() - private val desksTransitionObserver = mock<DesksTransitionObserver>() private val wallpaperToken = MockToken().token() private lateinit var transitionObserver: DesktopTasksTransitionObserver @@ -119,7 +116,6 @@ class DesktopTasksTransitionObserverTest { mixedHandler, backAnimationController, desktopWallpaperActivityTokenProvider, - desksTransitionObserver, shellInit, ) } @@ -403,21 +399,6 @@ class DesktopTasksTransitionObserverTest { verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false) } - @Test - fun onTransitionReady_forwardsToDesksTransitionObserver() { - val transition = Binder() - val info = TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0) - - transitionObserver.onTransitionReady( - transition = transition, - info = info, - StubTransaction(), - StubTransaction(), - ) - - verify(desksTransitionObserver).onTransitionReady(transition, info) - } - private fun createBackNavigationTransition( task: RunningTaskInfo?, type: Int = TRANSIT_TO_BACK, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt index c00083b0607f..c40a04c47b9b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt @@ -41,6 +41,7 @@ object DesktopTestHelpers { .setActivityType(ACTIVITY_TYPE_STANDARD) .setWindowingMode(WINDOWING_MODE_FREEFORM) .setLastActiveTime(100) + .setUserId(DEFAULT_USER_ID) .apply { bounds?.let { setBounds(it) } } .build() @@ -50,6 +51,7 @@ object DesktopTestHelpers { .setToken(MockToken().token()) .setActivityType(ACTIVITY_TYPE_STANDARD) .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setUserId(DEFAULT_USER_ID) .setLastActiveTime(100) /** Create a task that has windowing mode set to [WINDOWING_MODE_FULLSCREEN] */ @@ -63,6 +65,7 @@ object DesktopTestHelpers { .setToken(MockToken().token()) .setActivityType(ACTIVITY_TYPE_STANDARD) .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW) + .setUserId(DEFAULT_USER_ID) .setLastActiveTime(100) .build() @@ -72,6 +75,7 @@ object DesktopTestHelpers { .setToken(MockToken().token()) .setActivityType(ACTIVITY_TYPE_HOME) .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setUserId(DEFAULT_USER_ID) .setLastActiveTime(100) .build() @@ -91,4 +95,6 @@ object DesktopTestHelpers { createSystemModalTask().apply { baseActivity = ComponentName("com.test.dummypackage", "TestClass") } + + const val DEFAULT_USER_ID = 10 } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index 0871d38ceb46..6e7adf368155 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -26,6 +26,7 @@ import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags +import com.android.window.flags.Flags.FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder @@ -34,6 +35,7 @@ import com.android.wm.shell.bubbles.BubbleTransitions import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP +import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.CancelState import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.Companion.DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT @@ -56,6 +58,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.MockitoSession +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argThat import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -118,6 +121,14 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { .strictness(Strictness.LENIENT) .mockStatic(SystemProperties::class.java) .startMocking() + whenever( + transitions.startTransition( + eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP), + /* wct= */ any(), + eq(defaultHandler), + ) + ) + .thenReturn(mock<IBinder>()) } @After @@ -679,17 +690,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { val startTransition = startDrag(defaultHandler, task) val endTransition = mock<IBinder>() defaultHandler.onTaskResizeAnimationListener = mock() - defaultHandler.mergeAnimation( + mergeAnimation( transition = endTransition, - info = - createTransitionInfo( - type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, - draggedTask = task, - ), - startT = mock<SurfaceControl.Transaction>(), - finishT = mock<SurfaceControl.Transaction>(), + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + task = task, mergeTarget = startTransition, - finishCallback = mock<Transitions.TransitionFinishCallback>(), ) defaultHandler.onTransitionConsumed(endTransition, aborted = true, mock()) @@ -701,6 +706,123 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX) + fun mergeOtherTransition_cancelAndEndNotYetRequested_doesntInterruptsStartDrag() { + val finishCallback = mock<Transitions.TransitionFinishCallback>() + val task = createTask() + defaultHandler.onTaskResizeAnimationListener = mock() + val startTransition = startDrag(defaultHandler, task, finishCallback = finishCallback) + + mergeInterruptingTransition(mergeTarget = startTransition) + + verify(finishCallback, never()).onTransitionFinished(anyOrNull()) + verify(dragAnimator, never()).cancelAnimator() + } + + @Test + @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX) + fun mergeOtherTransition_endDragAlreadyMerged_doesNotInterruptStartDrag() { + val startDragFinishCallback = mock<Transitions.TransitionFinishCallback>() + val task = createTask() + val startTransition = + startDrag(defaultHandler, task, finishCallback = startDragFinishCallback) + defaultHandler.onTaskResizeAnimationListener = mock() + mergeAnimation( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + task = task, + mergeTarget = startTransition, + ) + + mergeInterruptingTransition(mergeTarget = startTransition) + + verify(startDragFinishCallback, never()).onTransitionFinished(anyOrNull()) + } + + @Test + @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX) + fun startEndAnimation_otherTransitionInterruptedStartAfterEndRequest_finishImmediately() { + val task1 = createTask() + val startTransition = startDrag(defaultHandler, task1) + val endTransition = + defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction()) + val startTransaction = mock<SurfaceControl.Transaction>() + val endDragFinishCallback = mock<Transitions.TransitionFinishCallback>() + defaultHandler.onTaskResizeAnimationListener = mock() + mergeInterruptingTransition(mergeTarget = startTransition) + + val didAnimate = + defaultHandler.startAnimation( + transition = requireNotNull(endTransition), + info = + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + draggedTask = task1, + ), + startTransaction = startTransaction, + finishTransaction = mock(), + finishCallback = endDragFinishCallback, + ) + + assertThat(didAnimate).isTrue() + verify(startTransaction).apply() + verify(endDragFinishCallback).onTransitionFinished(anyOrNull()) + } + + @Test + @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX) + fun startDrag_otherTransitionInterruptedStartAfterEndRequested_animatesDragWhenReady() { + val task1 = createTask() + val startTransition = startDrag(defaultHandler, task1) + verify(dragAnimator).startAnimation() + val endTransition = + defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction()) + defaultHandler.onTaskResizeAnimationListener = mock() + mergeInterruptingTransition(mergeTarget = startTransition) + defaultHandler.startAnimation( + transition = requireNotNull(endTransition), + info = + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + draggedTask = task1, + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = mock(), + ) + + startDrag(defaultHandler, createTask()) + + verify(dragAnimator, times(2)).startAnimation() + } + + private fun mergeInterruptingTransition(mergeTarget: IBinder) { + defaultHandler.mergeAnimation( + transition = mock<IBinder>(), + info = createTransitionInfo(type = TRANSIT_OPEN, draggedTask = createTask()), + startT = mock(), + finishT = mock(), + mergeTarget = mergeTarget, + finishCallback = mock(), + ) + } + + private fun mergeAnimation( + transition: IBinder = mock(), + type: Int, + mergeTarget: IBinder, + task: RunningTaskInfo, + ) { + defaultHandler.mergeAnimation( + transition = transition, + info = createTransitionInfo(type = type, draggedTask = task), + startT = mock(), + finishT = mock(), + mergeTarget = mergeTarget, + finishCallback = mock(), + ) + } + + @Test fun getAnimationFraction_returnsFraction() { val fraction = SpringDragToDesktopTransitionHandler.getAnimationFraction( @@ -785,6 +907,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { finishTransaction: SurfaceControl.Transaction = mock(), homeChange: TransitionInfo.Change? = createHomeChange(), transitionRootLeash: SurfaceControl = mock(), + finishCallback: Transitions.TransitionFinishCallback = mock(), ): IBinder { whenever(dragAnimator.position).thenReturn(PointF()) // Simulate transition is started and is ready to animate. @@ -800,7 +923,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { ), startTransaction = startTransaction, finishTransaction = finishTransaction, - finishCallback = {}, + finishCallback = finishCallback, ) return transition } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt index 96b85ad2729e..9af504797182 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt @@ -15,20 +15,25 @@ */ package com.android.wm.shell.desktopmode.multidesks +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.testing.AndroidTestingRunner import android.view.Display import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo +import android.window.WindowContainerToken import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.Change import android.window.WindowContainerTransaction.HierarchyOp +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTaskOrganizer.TaskListener import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.common.LaunchAdjacentController import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskMinimizationRoot import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskRoot @@ -36,14 +41,17 @@ import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellInit import com.google.common.truth.Truth.assertThat import kotlin.test.assertNotNull +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito import org.mockito.Mockito.verify import org.mockito.kotlin.argThat import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever /** * Tests for [RootTaskDesksOrganizer]. @@ -58,57 +66,32 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { private val testShellInit = ShellInit(testExecutor) private val mockShellCommandHandler = mock<ShellCommandHandler>() private val mockShellTaskOrganizer = mock<ShellTaskOrganizer>() + private val launchAdjacentController = LaunchAdjacentController(mock()) private lateinit var organizer: RootTaskDesksOrganizer @Before fun setUp() { organizer = - RootTaskDesksOrganizer(testShellInit, mockShellCommandHandler, mockShellTaskOrganizer) - } - - @Test - fun testCreateDesk_callsBack() { - val callback = FakeOnCreateCallback() - organizer.createDesk(Display.DEFAULT_DISPLAY, callback) - - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - - assertThat(callback.created).isTrue() - assertEquals(freeformRoot.taskId, callback.deskId) + RootTaskDesksOrganizer( + testShellInit, + mockShellCommandHandler, + mockShellTaskOrganizer, + launchAdjacentController, + ) } - @Test - fun testCreateDesk_createsMinimizationRoot() { - val callback = FakeOnCreateCallback() - organizer.createDesk(Display.DEFAULT_DISPLAY, callback) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - - val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(minimizationRootTask, SurfaceControl()) - - val minimizationRoot = organizer.deskMinimizationRootsByDeskId[freeformRoot.taskId] - assertNotNull(minimizationRoot) - assertThat(minimizationRoot.deskId).isEqualTo(freeformRoot.taskId) - assertThat(minimizationRoot.rootId).isEqualTo(minimizationRootTask.taskId) - } + @Test fun testCreateDesk_createsDeskAndMinimizationRoots() = runTest { createDesk() } @Test - fun testCreateMinimizationRoot_marksHidden() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - - val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(minimizationRootTask, SurfaceControl()) + fun testCreateMinimizationRoot_marksHidden() = runTest { + val desk = createDesk() verify(mockShellTaskOrganizer) .applyTransaction( argThat { wct -> wct.changes.any { change -> - change.key == minimizationRootTask.token.asBinder() && + change.key == desk.minimizationRoot.token.asBinder() && (change.value.changeMask and Change.CHANGE_HIDDEN != 0) && change.value.hidden } @@ -117,7 +100,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testOnTaskAppeared_withoutRequest_throws() { + fun testOnTaskAppeared_withoutRequest_throws() = runTest { val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } assertThrows(Exception::class.java) { @@ -126,41 +109,25 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testOnTaskAppeared_withRequestOnlyInAnotherDisplay_throws() { - organizer.createDesk(displayId = 2, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask(Display.DEFAULT_DISPLAY).apply { parentTaskId = -1 } - - assertThrows(Exception::class.java) { - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - } - } - - @Test - fun testOnTaskAppeared_duplicateRoot_throws() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + fun testOnTaskAppeared_duplicateRoot_throws() = runTest { + val desk = createDesk() assertThrows(Exception::class.java) { - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + organizer.onTaskAppeared(desk.deskRoot.taskInfo, SurfaceControl()) } } @Test - fun testOnTaskAppeared_duplicateMinimizedRoot_throws() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - organizer.onTaskAppeared(minimizationRootTask, SurfaceControl()) + fun testOnTaskAppeared_duplicateMinimizedRoot_throws() = runTest { + val desk = createDesk() assertThrows(Exception::class.java) { - organizer.onTaskAppeared(minimizationRootTask, SurfaceControl()) + organizer.onTaskAppeared(desk.minimizationRoot.taskInfo, SurfaceControl()) } } @Test - fun testOnTaskVanished_removesRoot() { + fun testOnTaskVanished_removesRoot() = runTest { val desk = createDesk() organizer.onTaskVanished(desk.deskRoot.taskInfo) @@ -169,7 +136,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testOnTaskVanished_removesMinimizedRoot() { + fun testOnTaskVanished_removesMinimizedRoot() = runTest { val desk = createDesk() organizer.onTaskVanished(desk.deskRoot.taskInfo) @@ -179,7 +146,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testDesktopWindowAppearsInDesk() { + fun testDesktopWindowAppearsInDesk() = runTest { val desk = createDesk() val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } @@ -189,7 +156,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testDesktopWindowAppearsInDeskMinimizationRoot() { + fun testDesktopWindowAppearsInDeskMinimizationRoot() = runTest { val desk = createDesk() val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } @@ -199,7 +166,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testDesktopWindowMovesToMinimizationRoot() { + fun testDesktopWindowMovesToMinimizationRoot() = runTest { val desk = createDesk() val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } organizer.onTaskAppeared(child, SurfaceControl()) @@ -212,7 +179,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testDesktopWindowDisappearsFromDesk() { + fun testDesktopWindowDisappearsFromDesk() = runTest { val desk = createDesk() val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } @@ -223,7 +190,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testDesktopWindowDisappearsFromDeskMinimizationRoot() { + fun testDesktopWindowDisappearsFromDeskMinimizationRoot() = runTest { val desk = createDesk() val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } @@ -234,7 +201,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testRemoveDesk_removesDeskRoot() { + fun testRemoveDesk_removesDeskRoot() = runTest { val desk = createDesk() val wct = WindowContainerTransaction() @@ -250,7 +217,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testRemoveDesk_removesMinimizationRoot() { + fun testRemoveDesk_removesMinimizationRoot() = runTest { val desk = createDesk() val wct = WindowContainerTransaction() @@ -266,7 +233,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testActivateDesk() { + fun testActivateDesk() = runTest { val desk = createDesk() val wct = WindowContainerTransaction() @@ -290,7 +257,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testActivateDesk_didNotExist_throws() { + fun testActivateDesk_didNotExist_throws() = runTest { val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } val wct = WindowContainerTransaction() @@ -298,7 +265,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testMoveTaskToDesk() { + fun testMoveTaskToDesk() = runTest { val desk = createDesk() val desktopTask = createFreeformTask().apply { parentTaskId = -1 } @@ -324,7 +291,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testMoveTaskToDesk_didNotExist_throws() { + fun testMoveTaskToDesk_didNotExist_throws() = runTest { val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } val desktopTask = createFreeformTask().apply { parentTaskId = -1 } @@ -335,7 +302,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testGetDeskAtEnd() { + fun testGetDeskAtEnd() = runTest { val desk = createDesk() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } @@ -348,7 +315,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testGetDeskAtEnd_inMinimizationRoot() { + fun testGetDeskAtEnd_inMinimizationRoot() = runTest { val desk = createDesk() val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } @@ -361,27 +328,24 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun testIsDeskActiveAtEnd() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - freeformRoot.isVisibleRequested = true - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + fun testIsDeskActiveAtEnd() = runTest { + val desk = createDesk() val isActive = organizer.isDeskActiveAtEnd( change = - TransitionInfo.Change(freeformRoot.token, SurfaceControl()).apply { - taskInfo = freeformRoot + TransitionInfo.Change(desk.deskRoot.token, SurfaceControl()).apply { + taskInfo = desk.deskRoot.taskInfo mode = TRANSIT_TO_FRONT }, - deskId = freeformRoot.taskId, + deskId = desk.deskRoot.deskId, ) assertThat(isActive).isTrue() } @Test - fun deactivateDesk_clearsLaunchRoot() { + fun deactivateDesk_clearsLaunchRoot() = runTest { val wct = WindowContainerTransaction() val desk = createDesk() organizer.activateDesk(wct, desk.deskRoot.deskId) @@ -400,7 +364,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun isDeskChange_forDeskId() { + fun isDeskChange_forDeskId() = runTest { val desk = createDesk() assertThat( @@ -415,7 +379,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun isDeskChange_forDeskId_inMinimizationRoot() { + fun isDeskChange_forDeskId_inMinimizationRoot() = runTest { val desk = createDesk() assertThat( @@ -433,7 +397,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun isDeskChange_anyDesk() { + fun isDeskChange_anyDesk() = runTest { val desk = createDesk() assertThat( @@ -447,7 +411,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun isDeskChange_anyDesk_inMinimizationRoot() { + fun isDeskChange_anyDesk_inMinimizationRoot() = runTest { val desk = createDesk() assertThat( @@ -464,7 +428,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun minimizeTask() { + fun minimizeTask() = runTest { val desk = createDesk() val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } val wct = WindowContainerTransaction() @@ -473,18 +437,11 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { organizer.minimizeTask(wct, deskId = desk.deskRoot.deskId, task) - assertThat( - wct.hierarchyOps.any { hop -> - hop.isReparent && - hop.container == task.token.asBinder() && - hop.newParent == desk.minimizationRoot.token.asBinder() - } - ) - .isTrue() + assertThat(wct.hasMinimizationHops(desk, task.token)).isTrue() } @Test - fun minimizeTask_alreadyMinimized_noOp() { + fun minimizeTask_alreadyMinimized_noOp() = runTest { val desk = createDesk() val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } val wct = WindowContainerTransaction() @@ -496,7 +453,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun minimizeTask_inDifferentDesk_noOp() { + fun minimizeTask_inDifferentDesk_noOp() = runTest { val desk = createDesk() val otherDesk = createDesk() val task = createFreeformTask().apply { parentTaskId = otherDesk.deskRoot.deskId } @@ -508,30 +465,251 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { assertThat(wct.isEmpty).isTrue() } + @Test + fun unminimizeTask() = runTest { + val desk = createDesk() + val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } + val wct = WindowContainerTransaction() + organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task) + organizer.onTaskAppeared(task, SurfaceControl()) + organizer.minimizeTask(wct, deskId = desk.deskRoot.deskId, task) + task.parentTaskId = desk.minimizationRoot.rootId + organizer.onTaskInfoChanged(task) + + wct.clear() + organizer.unminimizeTask(wct, deskId = desk.deskRoot.deskId, task) + + assertThat(wct.hasUnminimizationHops(desk, task.token)).isTrue() + } + + @Test + fun unminimizeTask_alreadyUnminimized_noOp() = runTest { + val desk = createDesk() + val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } + val wct = WindowContainerTransaction() + organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task) + organizer.onTaskAppeared(task, SurfaceControl()) + + wct.clear() + organizer.unminimizeTask(wct, deskId = desk.deskRoot.deskId, task) + + assertThat(wct.hasUnminimizationHops(desk, task.token)).isFalse() + } + + @Test + fun unminimizeTask_notInDesk_noOp() = runTest { + val desk = createDesk() + val task = createFreeformTask() + val wct = WindowContainerTransaction() + + organizer.unminimizeTask(wct, deskId = desk.deskRoot.deskId, task) + + assertThat(wct.hasUnminimizationHops(desk, task.token)).isFalse() + } + + @Test + fun reorderTaskToFront() = runTest { + val desk = createDesk() + val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } + val wct = WindowContainerTransaction() + organizer.onTaskAppeared(task, SurfaceControl()) + + organizer.reorderTaskToFront(wct, desk.deskRoot.deskId, task) + + assertThat( + wct.hierarchyOps.singleOrNull { hop -> + hop.container == task.token.asBinder() && + hop.type == HIERARCHY_OP_TYPE_REORDER && + hop.toTop && + hop.includingParents() + } + ) + .isNotNull() + } + + @Test + fun reorderTaskToFront_notInDesk_noOp() = runTest { + val desk = createDesk() + val task = createFreeformTask() + val wct = WindowContainerTransaction() + + organizer.reorderTaskToFront(wct, desk.deskRoot.deskId, task) + + assertThat( + wct.hierarchyOps.singleOrNull { hop -> + hop.container == task.token.asBinder() && + hop.type == HIERARCHY_OP_TYPE_REORDER && + hop.toTop && + hop.includingParents() + } + ) + .isNull() + } + + @Test + fun reorderTaskToFront_minimized_unminimizesAndReorders() = runTest { + val desk = createDesk() + val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } + val wct = WindowContainerTransaction() + organizer.onTaskAppeared(task, SurfaceControl()) + task.parentTaskId = desk.minimizationRoot.rootId + organizer.onTaskInfoChanged(task) + + organizer.reorderTaskToFront(wct, desk.deskRoot.deskId, task) + + assertThat(wct.hasUnminimizationHops(desk, task.token)).isTrue() + assertThat( + wct.hierarchyOps.singleOrNull { hop -> + hop.container == task.token.asBinder() && + hop.type == HIERARCHY_OP_TYPE_REORDER && + hop.toTop && + hop.includingParents() + } + ) + .isNotNull() + } + + @Test + fun onTaskAppeared_visibleDesk_onlyDesk_disablesLaunchAdjacent() = runTest { + launchAdjacentController.launchAdjacentEnabled = true + + createDesk(visible = true) + + assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse() + } + + @Test + fun onTaskAppeared_invisibleDesk_onlyDesk_enablesLaunchAdjacent() = runTest { + launchAdjacentController.launchAdjacentEnabled = false + + createDesk(visible = false) + + assertThat(launchAdjacentController.launchAdjacentEnabled).isTrue() + } + + @Test + fun onTaskAppeared_invisibleDesk_otherVisibleDesk_disablesLaunchAdjacent() = runTest { + launchAdjacentController.launchAdjacentEnabled = true + + createDesk(visible = true) + createDesk(visible = false) + + assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse() + } + + @Test + fun onTaskInfoChanged_deskBecomesVisible_onlyDesk_disablesLaunchAdjacent() = runTest { + launchAdjacentController.launchAdjacentEnabled = true + + val desk = createDesk(visible = false) + desk.deskRoot.taskInfo.isVisible = true + organizer.onTaskInfoChanged(desk.deskRoot.taskInfo) + + assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse() + } + + @Test + fun onTaskInfoChanged_deskBecomesInvisible_onlyDesk_enablesLaunchAdjacent() = runTest { + launchAdjacentController.launchAdjacentEnabled = false + + val desk = createDesk(visible = true) + desk.deskRoot.taskInfo.isVisible = false + organizer.onTaskInfoChanged(desk.deskRoot.taskInfo) + + assertThat(launchAdjacentController.launchAdjacentEnabled).isTrue() + } + + @Test + fun onTaskInfoChanged_deskBecomesInvisible_otherVisibleDesk_disablesLaunchAdjacent() = runTest { + launchAdjacentController.launchAdjacentEnabled = true + + createDesk(visible = true) + val desk = createDesk(visible = true) + desk.deskRoot.taskInfo.isVisible = false + organizer.onTaskInfoChanged(desk.deskRoot.taskInfo) + + assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse() + } + + @Test + fun onTaskVanished_visibleDeskDisappears_onlyDesk_enablesLaunchAdjacent() = runTest { + launchAdjacentController.launchAdjacentEnabled = false + + val desk = createDesk(visible = true) + organizer.onTaskVanished(desk.deskRoot.taskInfo) + + assertThat(launchAdjacentController.launchAdjacentEnabled).isTrue() + } + + @Test + fun onTaskVanished_visibleDeskDisappears_otherDeskVisible_disablesLaunchAdjacent() = runTest { + launchAdjacentController.launchAdjacentEnabled = true + + createDesk(visible = true) + val desk = createDesk(visible = true) + organizer.onTaskVanished(desk.deskRoot.taskInfo) + + assertThat(launchAdjacentController.launchAdjacentEnabled).isFalse() + } + private data class DeskRoots( val deskRoot: DeskRoot, val minimizationRoot: DeskMinimizationRoot, ) - private fun createDesk(): DeskRoots { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - val minimizationRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(minimizationRoot, SurfaceControl()) - return DeskRoots( - organizer.deskRootsByDeskId[freeformRoot.taskId], - checkNotNull(organizer.deskMinimizationRootsByDeskId[freeformRoot.taskId]), - ) - } - - private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback { - var deskId: Int? = null - val created: Boolean - get() = deskId != null - - override fun onCreated(deskId: Int) { - this.deskId = deskId - } + private suspend fun createDesk(visible: Boolean = true): DeskRoots { + val freeformRootTask = + createFreeformTask().apply { + parentTaskId = -1 + isVisible = visible + isVisibleRequested = visible + } + val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 } + Mockito.reset(mockShellTaskOrganizer) + whenever( + mockShellTaskOrganizer.createRootTask( + Display.DEFAULT_DISPLAY, + WINDOWING_MODE_FREEFORM, + organizer, + true, + ) + ) + .thenAnswer { invocation -> + val listener = (invocation.arguments[2] as TaskListener) + listener.onTaskAppeared(freeformRootTask, SurfaceControl()) + } + .thenAnswer { invocation -> + val listener = (invocation.arguments[2] as TaskListener) + listener.onTaskAppeared(minimizationRootTask, SurfaceControl()) + } + val deskId = organizer.createDesk(Display.DEFAULT_DISPLAY) + assertEquals(freeformRootTask.taskId, deskId) + val deskRoot = assertNotNull(organizer.deskRootsByDeskId.get(freeformRootTask.taskId)) + val minimizationRoot = + assertNotNull(organizer.deskMinimizationRootsByDeskId[freeformRootTask.taskId]) + assertThat(minimizationRoot.deskId).isEqualTo(freeformRootTask.taskId) + assertThat(minimizationRoot.rootId).isEqualTo(minimizationRootTask.taskId) + return DeskRoots(deskRoot, minimizationRoot) } + + private fun WindowContainerTransaction.hasMinimizationHops( + desk: DeskRoots, + task: WindowContainerToken, + ): Boolean = + hierarchyOps.any { hop -> + hop.isReparent && + hop.container == task.asBinder() && + hop.newParent == desk.minimizationRoot.token.asBinder() + } + + private fun WindowContainerTransaction.hasUnminimizationHops( + desk: DeskRoots, + task: WindowContainerToken, + ): Boolean = + hierarchyOps.any { hop -> + hop.isReparent && + hop.container == task.asBinder() && + hop.newParent == desk.deskRoot.token.asBinder() && + hop.toTop + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt index dd9e6ca0deae..4440d4e801fe 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.desktopmode.persistence import android.os.UserManager -import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY @@ -82,10 +81,27 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { } @Test - @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, FLAG_ENABLE_DESKTOP_WINDOWING_HSUM) - /** TODO: b/362720497 - add multi-desk version when implemented. */ - @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) - fun initWithPersistence_multipleUsers_addedCorrectly_multiDesksDisabled() = + fun init_updatesFlow() = + runTest(StandardTestDispatcher()) { + whenever(persistentRepository.getUserDesktopRepositoryMap()) + .thenReturn(mapOf(USER_ID_1 to desktopRepositoryState1)) + whenever(persistentRepository.getDesktopRepositoryState(USER_ID_1)) + .thenReturn(desktopRepositoryState1) + whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_1)).thenReturn(desktop1) + whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_2)).thenReturn(desktop2) + + repositoryInitializer.initialize(desktopUserRepositories) + + assertThat(repositoryInitializer.isInitialized.value).isTrue() + } + + @Test + @EnableFlags( + FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, + FLAG_ENABLE_DESKTOP_WINDOWING_HSUM, + FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun initWithPersistence_multipleUsers_addedCorrectly() = runTest(StandardTestDispatcher()) { whenever(persistentRepository.getUserDesktopRepositoryMap()) .thenReturn( @@ -104,50 +120,74 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { repositoryInitializer.initialize(desktopUserRepositories) - // Desktop Repository currently returns all tasks across desktops for a specific user - // since the repository currently doesn't handle desktops. This test logic should be - // updated - // once the repository handles multiple desktops. assertThat( - desktopUserRepositories.getProfile(USER_ID_1).getActiveTasks(DEFAULT_DISPLAY) + desktopUserRepositories + .getProfile(USER_ID_1) + .getActiveTaskIdsInDesk(DESKTOP_ID_1) ) - .containsExactly(1, 3, 4, 5) + .containsExactly(1, 3) .inOrder() assertThat( desktopUserRepositories .getProfile(USER_ID_1) - .getExpandedTasksOrdered(DEFAULT_DISPLAY) + .getActiveTaskIdsInDesk(DESKTOP_ID_2) ) - .containsExactly(5, 1) + .containsExactly(4, 5) .inOrder() assertThat( - desktopUserRepositories.getProfile(USER_ID_1).getMinimizedTasks(DEFAULT_DISPLAY) + desktopUserRepositories + .getProfile(USER_ID_2) + .getActiveTaskIdsInDesk(DESKTOP_ID_3) ) - .containsExactly(3, 4) + .containsExactly(7, 8) + .inOrder() + assertThat( + desktopUserRepositories + .getProfile(USER_ID_1) + .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_1) + ) + .containsExactly(1) .inOrder() - assertThat( - desktopUserRepositories.getProfile(USER_ID_2).getActiveTasks(DEFAULT_DISPLAY) + desktopUserRepositories + .getProfile(USER_ID_1) + .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_2) ) - .containsExactly(7, 8) + .containsExactly(5) .inOrder() assertThat( desktopUserRepositories .getProfile(USER_ID_2) - .getExpandedTasksOrdered(DEFAULT_DISPLAY) + .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_3) + ) + .containsExactly(7) + .inOrder() + assertThat( + desktopUserRepositories + .getProfile(USER_ID_1) + .getMinimizedTaskIdsInDesk(DESKTOP_ID_1) + ) + .containsExactly(3) + .inOrder() + assertThat( + desktopUserRepositories + .getProfile(USER_ID_1) + .getMinimizedTaskIdsInDesk(DESKTOP_ID_2) ) - .contains(7) + .containsExactly(4) + .inOrder() assertThat( - desktopUserRepositories.getProfile(USER_ID_2).getMinimizedTasks(DEFAULT_DISPLAY) + desktopUserRepositories + .getProfile(USER_ID_2) + .getMinimizedTaskIdsInDesk(DESKTOP_ID_3) ) .containsExactly(8) + .inOrder() } @Test - @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) - /** TODO: b/362720497 - add multi-desk version when implemented. */ - @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) - fun initWithPersistence_singleUser_addedCorrectly_multiDesksDisabled() = + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun initWithPersistence_singleUser_addedCorrectly() = runTest(StandardTestDispatcher()) { whenever(persistentRepository.getUserDesktopRepositoryMap()) .thenReturn(mapOf(USER_ID_1 to desktopRepositoryState1)) @@ -161,23 +201,44 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { assertThat( desktopUserRepositories .getProfile(USER_ID_1) - .getActiveTaskIdsInDesk(deskId = DEFAULT_DISPLAY) + .getActiveTaskIdsInDesk(DESKTOP_ID_1) + ) + .containsExactly(1, 3) + .inOrder() + assertThat( + desktopUserRepositories + .getProfile(USER_ID_1) + .getActiveTaskIdsInDesk(DESKTOP_ID_2) + ) + .containsExactly(4, 5) + .inOrder() + assertThat( + desktopUserRepositories + .getProfile(USER_ID_1) + .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_1) + ) + .containsExactly(1) + .inOrder() + assertThat( + desktopUserRepositories + .getProfile(USER_ID_1) + .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_2) ) - .containsExactly(1, 3, 4, 5) + .containsExactly(5) .inOrder() assertThat( desktopUserRepositories .getProfile(USER_ID_1) - .getExpandedTasksIdsInDeskOrdered(deskId = DEFAULT_DISPLAY) + .getMinimizedTaskIdsInDesk(DESKTOP_ID_1) ) - .containsExactly(5, 1) + .containsExactly(3) .inOrder() assertThat( desktopUserRepositories .getProfile(USER_ID_1) - .getMinimizedTaskIdsInDesk(deskId = DEFAULT_DISPLAY) + .getMinimizedTaskIdsInDesk(DESKTOP_ID_2) ) - .containsExactly(3, 4) + .containsExactly(4) .inOrder() } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java index 9509aaf20c9b..52c5ad1f8e49 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java @@ -41,6 +41,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; @@ -198,6 +199,7 @@ public final class FreeformTaskListenerTests extends ShellTestCase { } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) public void visibilityTaskChanged_visible_setLaunchAdjacentDisabled() { ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); @@ -209,6 +211,7 @@ public final class FreeformTaskListenerTests extends ShellTestCase { } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) public void visibilityTaskChanged_notVisible_setLaunchAdjacentEnabled() { ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java index bc918450a3cf..714e5f486285 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java @@ -44,10 +44,12 @@ import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.desktopmode.DesktopImmersiveController; +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.TransitionInfoBuilder; import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.util.StubTransaction; import com.android.wm.shell.windowdecor.WindowDecorViewModel; import org.junit.Before; @@ -68,6 +70,7 @@ public class FreeformTaskTransitionObserverTest extends ShellTestCase { @Mock private WindowDecorViewModel mWindowDecorViewModel; @Mock private TaskChangeListener mTaskChangeListener; @Mock private FocusTransitionObserver mFocusTransitionObserver; + @Mock private DesksTransitionObserver mDesksTransitionObserver; private FreeformTaskTransitionObserver mTransitionObserver; @@ -88,7 +91,8 @@ public class FreeformTaskTransitionObserverTest extends ShellTestCase { Optional.of(mDesktopImmersiveController), mWindowDecorViewModel, Optional.of(mTaskChangeListener), - mFocusTransitionObserver); + mFocusTransitionObserver, + Optional.of(mDesksTransitionObserver)); final ArgumentCaptor<Runnable> initRunnableCaptor = ArgumentCaptor.forClass(Runnable.class); verify(mShellInit).addInitCallback(initRunnableCaptor.capture(), same(mTransitionObserver)); @@ -357,6 +361,18 @@ public class FreeformTaskTransitionObserverTest extends ShellTestCase { verify(mDesktopImmersiveController).onTransitionFinished(transition, /* aborted= */ false); } + @Test + public void onTransitionReady_forwardsToDesksTransitionObserver() { + final IBinder transition = mock(IBinder.class); + final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, /* flags= */ 0) + .build(); + + mTransitionObserver.onTransitionReady(transition, info, new StubTransaction(), + new StubTransaction()); + + verify(mDesksTransitionObserver).onTransitionReady(transition, info); + } + private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) { final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.taskId = taskId; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java index 275e4882a79d..42f65dd71f16 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java @@ -17,6 +17,7 @@ package com.android.wm.shell.pip2.phone; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -86,6 +87,7 @@ public class PipSchedulerTest { @Mock private SurfaceControl.Transaction mMockTransaction; @Mock private PipAlphaAnimator mMockAlphaAnimator; @Mock private SplitScreenController mMockSplitScreenController; + @Mock private SurfaceControl mMockLeash; @Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor; @Captor private ArgumentCaptor<WindowContainerTransaction> mWctArgumentCaptor; @@ -315,6 +317,30 @@ public class PipSchedulerTest { verify(mMockAlphaAnimator, never()).start(); } + @Test + public void onPipTransitionStateChanged_exiting_endAnimation() { + mPipScheduler.setOverlayFadeoutAnimator(mMockAlphaAnimator); + when(mMockAlphaAnimator.isStarted()).thenReturn(true); + mPipScheduler.onPipTransitionStateChanged(PipTransitionState.ENTERED_PIP, + PipTransitionState.EXITING_PIP, null); + + verify(mMockAlphaAnimator, times(1)).end(); + assertNull("mOverlayFadeoutAnimator should be reset to null", + mPipScheduler.getOverlayFadeoutAnimator()); + } + + @Test + public void onPipTransitionStateChanged_scheduledBoundsChange_endAnimation() { + mPipScheduler.setOverlayFadeoutAnimator(mMockAlphaAnimator); + when(mMockAlphaAnimator.isStarted()).thenReturn(true); + mPipScheduler.onPipTransitionStateChanged(PipTransitionState.ENTERED_PIP, + PipTransitionState.SCHEDULED_BOUNDS_CHANGE, null); + + verify(mMockAlphaAnimator, times(1)).end(); + assertNull("mOverlayFadeoutAnimator should be reset to null", + mPipScheduler.getOverlayFadeoutAnimator()); + } + private void setNullPipTaskToken() { when(mMockPipTransitionState.getPipTaskToken()).thenReturn(null); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java index 333569a7206e..5029371c3419 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java @@ -24,6 +24,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.mockito.kotlin.MatchersKt.eq; import static org.mockito.kotlin.VerificationKt.clearInvocations; @@ -35,7 +36,9 @@ import android.app.ActivityManager; import android.app.PendingIntent; import android.app.PictureInPictureParams; import android.app.RemoteAction; +import android.content.ComponentName; import android.content.Context; +import android.content.res.Resources; import android.graphics.Rect; import android.graphics.drawable.Icon; import android.os.Bundle; @@ -48,8 +51,10 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.pip.PhoneSizeSpecSource; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.pip2.animation.PipResizeAnimator; import org.junit.Before; @@ -107,6 +112,16 @@ public class PipTaskListenerTest { } @Test + public void constructor_addOnPipComponentChangedListener() { + mPipTaskListener = new PipTaskListener(mMockContext, mMockShellTaskOrganizer, + mMockPipTransitionState, mMockPipScheduler, mMockPipBoundsState, + mMockPipBoundsAlgorithm, mMockShellExecutor); + + verify(mMockPipBoundsState).addOnPipComponentChangedListener( + any(PipBoundsState.OnPipComponentChangedListener.class)); + } + + @Test public void setPictureInPictureParams_updatePictureInPictureParams() { mPipTaskListener = new PipTaskListener(mMockContext, mMockShellTaskOrganizer, mMockPipTransitionState, mMockPipScheduler, mMockPipBoundsState, @@ -359,6 +374,26 @@ public class PipTaskListenerTest { verify(mMockPipResizeAnimator, times(0)).start(); } + @Test + public void onPipComponentChanged_clearPictureInPictureParams() { + when(mMockContext.getResources()).thenReturn(mock(Resources.class)); + PipBoundsState pipBoundsState = new PipBoundsState(mMockContext, + mock(PhoneSizeSpecSource.class), mock(PipDisplayLayoutState.class)); + pipBoundsState.setLastPipComponentName(new ComponentName("org.test", "test1")); + + mPipTaskListener = new PipTaskListener(mMockContext, mMockShellTaskOrganizer, + mMockPipTransitionState, mMockPipScheduler, pipBoundsState, + mMockPipBoundsAlgorithm, mMockShellExecutor); + Rational aspectRatio = new Rational(4, 3); + String action1 = "action1"; + mPipTaskListener.setPictureInPictureParams(getPictureInPictureParams( + aspectRatio, action1)); + + pipBoundsState.setLastPipComponentName(new ComponentName("org.test", "test2")); + + assertTrue(mPipTaskListener.getPictureInPictureParams().empty()); + } + private PictureInPictureParams getPictureInPictureParams(Rational aspectRatio, String... actions) { final PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt index 6ecebd76a951..75f6bda4d750 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt @@ -203,7 +203,7 @@ class GroupedTaskInfoTest : ShellTestCase() { assertThat(taskInfoFromParcel.taskInfoList).hasSize(3) // Only compare task ids val taskIdComparator = Correspondence.transforming<TaskInfo, Int>( - { it?.taskId }, "has taskId of" + { it.taskId }, "has taskId of" ) assertThat(taskInfoFromParcel.taskInfoList).comparingElementsUsing(taskIdComparator) .containsExactly(1, 2, 3).inOrder() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt index fd22a84dee5d..2b39262d9f00 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt @@ -53,7 +53,8 @@ class DragZoneFactoryTest { tabletPortrait.copy(windowBounds = Rect(0, 0, 800, 900), isSmallTablet = true) private val foldableLandscape = foldablePortrait.copy(windowBounds = Rect(0, 0, 900, 800), isLandscape = true) - private val splitScreenModeChecker = SplitScreenModeChecker { SplitScreenMode.NONE } + private var splitScreenMode = SplitScreenMode.NONE + private val splitScreenModeChecker = SplitScreenModeChecker { splitScreenMode } private var isDesktopWindowModeSupported = true private val desktopWindowModeChecker = DesktopWindowModeChecker { isDesktopWindowModeSupported } @@ -283,7 +284,7 @@ class DragZoneFactoryTest { } @Test - fun dragZonesForBubble_tablet_desktopModeDisabled() { + fun dragZonesForBubble_desktopModeDisabled() { isDesktopWindowModeSupported = false dragZoneFactory = DragZoneFactory( @@ -298,7 +299,7 @@ class DragZoneFactoryTest { } @Test - fun dragZonesForExpandedView_tablet_desktopModeDisabled() { + fun dragZonesForExpandedView_desktopModeDisabled() { isDesktopWindowModeSupported = false dragZoneFactory = DragZoneFactory( @@ -314,6 +315,38 @@ class DragZoneFactoryTest { assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty() } + @Test + fun dragZonesForBubble_splitScreenModeUnsupported() { + splitScreenMode = SplitScreenMode.UNSUPPORTED + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) + assertThat(dragZones.filterIsInstance<DragZone.Split>()).isEmpty() + } + + @Test + fun dragZonesForExpandedView_splitScreenModeUnsupported() { + splitScreenMode = SplitScreenMode.UNSUPPORTED + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) + val dragZones = + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) + assertThat(dragZones.filterIsInstance<DragZone.Split>()).isEmpty() + } + private inline fun <reified T> verifyInstance(): DragZoneVerifier = { dragZone -> assertThat(dragZone).isInstanceOf(T::class.java) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt index 257bbb5603a7..b07b6c1a3a87 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt @@ -22,6 +22,7 @@ import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.SurfaceControl +import android.window.TaskSnapshot import androidx.test.filters.SmallTest import com.android.window.flags.Flags import com.android.wm.shell.MockToken @@ -33,6 +34,7 @@ import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer import com.google.common.truth.Truth.assertThat import org.junit.After +import org.junit.Assert.fail import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -84,7 +86,29 @@ class DesktopHeaderManageWindowsMenuTest : ShellTestCase() { assertThat(menu.menuViewContainer).isInstanceOf(AdditionalSystemViewContainer::class.java) } - private fun createMenu(task: RunningTaskInfo) = DesktopHeaderManageWindowsMenu( + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun testShow_nullSnapshotDoesNotCauseNPE() { + val task = createFreeformTask() + val snapshotList = listOf(Pair(/* index = */ 1, /* snapshot = */ null)) + // Set as immersive so that menu is created as system view container (simpler of the + // options) + userRepositories.getProfile(DEFAULT_USER_ID).setTaskInFullImmersiveState( + displayId = task.displayId, + taskId = task.taskId, + immersive = true + ) + try { + menu = createMenu(task, snapshotList) + } catch (e: NullPointerException) { + fail("Null snapshot should not have thrown null pointer exception") + } + } + + private fun createMenu( + task: RunningTaskInfo, + snapshotList: List<Pair<Int, TaskSnapshot?>> = emptyList() + ) = DesktopHeaderManageWindowsMenu( callerTaskInfo = task, x = 0, y = 0, @@ -94,7 +118,7 @@ class DesktopHeaderManageWindowsMenuTest : ShellTestCase() { desktopUserRepositories = userRepositories, surfaceControlBuilderSupplier = { SurfaceControl.Builder() }, surfaceControlTransactionSupplier = { SurfaceControl.Transaction() }, - snapshotList = emptyList(), + snapshotList = snapshotList, onIconClickListener = {}, onOutsideClickListener = {}, ) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index c4f70ac2297f..1371b38a579f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -167,6 +167,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private static final boolean DEFAULT_IS_STATUSBAR_VISIBLE = true; private static final boolean DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED = false; private static final boolean DEFAULT_IS_IN_FULL_IMMERSIVE_MODE = false; + private static final boolean DEFAULT_IS_DRAGGING = false; private static final boolean DEFAULT_HAS_GLOBAL_FOCUS = true; private static final boolean DEFAULT_SHOULD_IGNORE_CORNER_RADIUS = false; private static final boolean DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS = false; @@ -415,6 +416,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_STATUSBAR_VISIBLE, DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, + DEFAULT_IS_DRAGGING, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, @@ -616,6 +618,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_STATUSBAR_VISIBLE, DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, + DEFAULT_IS_DRAGGING, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, @@ -650,6 +653,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_STATUSBAR_VISIBLE, DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, + DEFAULT_IS_DRAGGING, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, @@ -728,6 +732,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_STATUSBAR_VISIBLE, DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, /* inFullImmersiveMode */ true, + DEFAULT_IS_DRAGGING, insetsState, DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, @@ -755,6 +760,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_STATUSBAR_VISIBLE, DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, /* inFullImmersiveMode */ true, + DEFAULT_IS_DRAGGING, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, @@ -781,6 +787,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isStatusBarVisible */ false, DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, + DEFAULT_IS_DRAGGING, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, @@ -807,6 +814,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isStatusBarVisible */ true, /* isKeyguardVisibleAndOccluded */ false, DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, + DEFAULT_IS_DRAGGING, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, @@ -832,6 +840,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isStatusBarVisible */ false, DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, + DEFAULT_IS_DRAGGING, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, @@ -857,6 +866,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_STATUSBAR_VISIBLE, /* isKeyguardVisibleAndOccluded */ true, DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, + DEFAULT_IS_DRAGGING, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, @@ -883,6 +893,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isStatusBarVisible */ true, DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, /* inFullImmersiveMode */ true, + DEFAULT_IS_DRAGGING, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, @@ -901,6 +912,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { /* isStatusBarVisible */ false, DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, /* inFullImmersiveMode */ true, + DEFAULT_IS_DRAGGING, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, @@ -911,6 +923,33 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX) + public void updateRelayoutParams_header_fullyImmersive_captionVisDuringDrag() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + mMockSplitScreenController, + DEFAULT_APPLY_START_TRANSACTION_ON_DRAW, + DEFAULT_SHOULD_SET_TASK_POSITIONING_AND_CROP, + /* isStatusBarVisible */ false, + DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, + /* inFullImmersiveMode */ true, + /* isDragging */ true, + new InsetsState(), + DEFAULT_HAS_GLOBAL_FOCUS, + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); + + assertThat(relayoutParams.mIsCaptionVisible).isTrue(); + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) public void updateRelayoutParams_header_fullyImmersive_overKeyguard_captionNotVisible() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); @@ -927,6 +966,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_STATUSBAR_VISIBLE, /* isKeyguardVisibleAndOccluded */ true, /* inFullImmersiveMode */ true, + DEFAULT_IS_DRAGGING, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, @@ -1588,6 +1628,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { DEFAULT_IS_STATUSBAR_VISIBLE, DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, + DEFAULT_IS_DRAGGING, new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index a2927fa3527b..9a2e2fad50be 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -60,6 +60,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.os.Handler; +import android.os.LocaleList; import android.testing.AndroidTestingRunner; import android.util.DisplayMetrics; import android.view.AttachedSurfaceControl; @@ -97,6 +98,7 @@ import org.mockito.Mockito; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.function.Supplier; /** @@ -475,6 +477,50 @@ public class WindowDecorationTests extends ShellTestCase { } @Test + public void testReinflateViewsOnLocaleListChange() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setVisible(true) + .setDisplayId(Display.DEFAULT_DISPLAY) + .build(); + taskInfo.configuration.setLocales(new LocaleList(Locale.FRANCE, Locale.US)); + final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo)); + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain()); + clearInvocations(windowDecor); + + final ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder() + .setVisible(true) + .setDisplayId(Display.DEFAULT_DISPLAY) + .build(); + taskInfo2.configuration.setLocales(new LocaleList(Locale.US, Locale.FRANCE)); + windowDecor.relayout(taskInfo2, true /* hasGlobalFocus */, Region.obtain()); + // WindowDecoration#releaseViews should be called since the locale list has changed. + verify(windowDecor, times(1)).releaseViews(any()); + } + + @Test + public void testViewNotReinflatedWhenLocaleListNotChanged() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setVisible(true) + .setDisplayId(Display.DEFAULT_DISPLAY) + .build(); + taskInfo.configuration.setLocales(new LocaleList(Locale.FRANCE, Locale.US)); + final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo)); + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain()); + clearInvocations(windowDecor); + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain()); + // WindowDecoration#releaseViews should not be called since nothing has changed. + verify(windowDecor, never()).releaseViews(any()); + } + + @Test public void testLayoutResultCalculation_fullWidthCaption() { final Display defaultDisplay = mock(Display.class); doReturn(defaultDisplay).when(mMockDisplayController) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt index c61e0eb3b5af..c8ccac35d4c4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt @@ -23,6 +23,7 @@ import android.content.pm.ActivityInfo import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.graphics.drawable.Drawable +import android.os.LocaleList import android.os.UserHandle import android.testing.AndroidTestingRunner import android.testing.TestableContext @@ -39,6 +40,7 @@ import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.sysui.UserChangeListener import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader.AppResources import com.google.common.truth.Truth.assertThat +import java.util.Locale import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test @@ -116,8 +118,10 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() { @Test fun testGetName_cached_returnsFromCache() { val task = createTaskInfo(context.userId) + task.configuration.setLocales(LocaleList(Locale.US)) loader.onWindowDecorCreated(task) loader.taskToResourceCache[task.taskId] = AppResources("App Name", mock(), mock()) + loader.localeListOnCache[task.taskId] = LocaleList(Locale.US) loader.getName(task) @@ -130,6 +134,19 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() { } @Test + fun testGetName_cached_localesChanged_loadsResourceAndCaches() { + val task = createTaskInfo(context.userId) + loader.onWindowDecorCreated(task) + loader.taskToResourceCache[task.taskId] = AppResources("App Name", mock(), mock()) + loader.localeListOnCache[task.taskId] = LocaleList(Locale.US, Locale.FRANCE) + task.configuration.setLocales(LocaleList(Locale.FRANCE, Locale.US)) + doReturn("App Name but in French").whenever(mockPackageManager).getApplicationLabel(any()) + + assertThat(loader.getName(task)).isEqualTo("App Name but in French") + assertThat(loader.taskToResourceCache[task.taskId]?.appName).isEqualTo("App Name but in French") + } + + @Test fun testGetHeaderIcon_notCached_loadsResourceAndCaches() { val task = createTaskInfo(context.userId) loader.onWindowDecorCreated(task) diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index a18c5f5f92f6..8ecd6ba9b253 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -6520,41 +6520,79 @@ base::expected<StringPiece16, NullOrIOError> StringPoolRef::string16() const { } bool ResTable::getResourceFlags(uint32_t resID, uint32_t* outFlags) const { - if (mError != NO_ERROR) { - return false; - } + if (mError != NO_ERROR) { + return false; + } - const ssize_t p = getResourcePackageIndex(resID); - const int t = Res_GETTYPE(resID); - const int e = Res_GETENTRY(resID); + const ssize_t p = getResourcePackageIndex(resID); + const int t = Res_GETTYPE(resID); + const int e = Res_GETENTRY(resID); - if (p < 0) { - if (Res_GETPACKAGE(resID)+1 == 0) { - ALOGW("No package identifier when getting flags for resource number 0x%08x", resID); - } else { - ALOGW("No known package when getting flags for resource number 0x%08x", resID); - } - return false; - } - if (t < 0) { - ALOGW("No type identifier when getting flags for resource number 0x%08x", resID); - return false; + if (p < 0) { + if (Res_GETPACKAGE(resID)+1 == 0) { + ALOGW("No package identifier when getting flags for resource number 0x%08x", resID); + } else { + ALOGW("No known package when getting flags for resource number 0x%08x", resID); } + return false; + } + if (t < 0) { + ALOGW("No type identifier when getting flags for resource number 0x%08x", resID); + return false; + } - const PackageGroup* const grp = mPackageGroups[p]; - if (grp == NULL) { - ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID); - return false; - } + const PackageGroup* const grp = mPackageGroups[p]; + if (grp == NULL) { + ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID); + return false; + } - Entry entry; - status_t err = getEntry(grp, t, e, NULL, &entry); - if (err != NO_ERROR) { - return false; + Entry entry; + status_t err = getEntry(grp, t, e, NULL, &entry); + if (err != NO_ERROR) { + return false; + } + + *outFlags = entry.specFlags; + return true; +} + +bool ResTable::getResourceEntryFlags(uint32_t resID, uint32_t* outFlags) const { + if (mError != NO_ERROR) { + return false; + } + + const ssize_t p = getResourcePackageIndex(resID); + const int t = Res_GETTYPE(resID); + const int e = Res_GETENTRY(resID); + + if (p < 0) { + if (Res_GETPACKAGE(resID)+1 == 0) { + ALOGW("No package identifier when getting flags for resource number 0x%08x", resID); + } else { + ALOGW("No known package when getting flags for resource number 0x%08x", resID); } + return false; + } + if (t < 0) { + ALOGW("No type identifier when getting flags for resource number 0x%08x", resID); + return false; + } - *outFlags = entry.specFlags; - return true; + const PackageGroup* const grp = mPackageGroups[p]; + if (grp == NULL) { + ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID); + return false; + } + + Entry entry; + status_t err = getEntry(grp, t, e, NULL, &entry); + if (err != NO_ERROR) { + return false; + } + + *outFlags = entry.entry->flags(); + return true; } bool ResTable::isPackageDynamic(uint8_t packageID) const { diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index 30594dcfa939..63b28da075cd 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -1265,6 +1265,9 @@ struct ResTable_config // Varies in length from 3 to 8 chars. Zero-filled value. char localeNumberingSystem[8]; + // Mark all padding explicitly so it's clear how much we can expand it. + char endPadding[3]; + void copyFromDeviceNoSwap(const ResTable_config& o) { const auto o_size = dtohl(o.size); if (o_size >= sizeof(ResTable_config)) [[likely]] { @@ -1422,6 +1425,13 @@ struct ResTable_config void swapHtoD_slow(); }; +// Fix the struct size for backward compatibility +static_assert(sizeof(ResTable_config) == 64); + +// Make sure there's no unaccounted padding in the structure. +static_assert(offsetof(ResTable_config, endPadding) + + sizeof(ResTable_config::endPadding) == sizeof(ResTable_config)); + /** * A specification of the resources defined by a particular type. * @@ -1583,6 +1593,8 @@ union ResTable_entry // If set, this is a compact entry with data type and value directly // encoded in the this entry, see ResTable_entry::compact FLAG_COMPACT = 0x0008, + // If set, this entry relies on read write android feature flags + FLAG_USES_FEATURE_FLAGS = 0x0010, }; struct Full { @@ -1612,6 +1624,7 @@ union ResTable_entry uint16_t flags() const { return dtohs(full.flags); }; bool is_compact() const { return flags() & FLAG_COMPACT; } bool is_complex() const { return flags() & FLAG_COMPLEX; } + bool uses_feature_flags() const { return flags() & FLAG_USES_FEATURE_FLAGS; } size_t size() const { return is_compact() ? sizeof(ResTable_entry) : dtohs(this->full.size); @@ -2029,6 +2042,8 @@ public: bool getResourceFlags(uint32_t resID, uint32_t* outFlags) const; + bool getResourceEntryFlags(uint32_t resID, uint32_t* outFlags) const; + /** * Returns whether or not the package for the given resource has been dynamically assigned. * If the resource can't be found, returns 'false'. diff --git a/libs/hostgraphics/HostBufferQueue.cpp b/libs/hostgraphics/HostBufferQueue.cpp index 7e14b88a47fa..ef5406250251 100644 --- a/libs/hostgraphics/HostBufferQueue.cpp +++ b/libs/hostgraphics/HostBufferQueue.cpp @@ -29,6 +29,7 @@ public: } virtual status_t detachBuffer(int slot) { + mBuffer.clear(); return OK; } diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp index a210ddf54b2e..7d227f793817 100644 --- a/libs/hwui/jni/Graphics.cpp +++ b/libs/hwui/jni/Graphics.cpp @@ -722,10 +722,15 @@ void RecyclingClippingPixelAllocator::copyIfNecessary() { auto canvas = SkCanvas(recycledPixels->getSkBitmap()); SkRect destination = SkRect::Make(recycledPixels->info().bounds()); - destination.intersect(SkRect::Make(mSkiaBitmap->info().bounds())); - canvas.drawImageRect(mSkiaBitmap->asImage(), *mDesiredSubset, destination, - SkSamplingOptions(SkFilterMode::kLinear), nullptr, - SkCanvas::kFast_SrcRectConstraint); + if (destination.intersect(SkRect::Make(mSkiaBitmap->info().bounds()))) { + canvas.drawImageRect(mSkiaBitmap->asImage(), *mDesiredSubset, destination, + SkSamplingOptions(SkFilterMode::kLinear), nullptr, + SkCanvas::kFast_SrcRectConstraint); + } else { + // The canvas would have discarded the draw operation automatically, but + // this case should have been detected before getting to this point. + ALOGE("Copy destination does not intersect image bounds"); + } } else { void* dst = recycledPixels->pixels(); const size_t dstRowBytes = mRecycledBitmap->rowBytes(); diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig index 83b1778fd611..f8eb41826c6f 100644 --- a/location/java/android/location/flags/location.aconfig +++ b/location/java/android/location/flags/location.aconfig @@ -179,3 +179,10 @@ flag { } } +flag { + name: "gnss_location_provider_overlay_2025_devices" + namespace: "location" + description: "Flag for GNSS location provider overlay for 2025 devices" + bug: "398254728" + is_fixed_read_only: true +} diff --git a/media/java/Android.bp b/media/java/Android.bp index 6878f9d61f6d..28b9d3bbc167 100644 --- a/media/java/Android.bp +++ b/media/java/Android.bp @@ -15,6 +15,7 @@ filegroup { ], exclude_srcs: [ ":framework-media-tv-tunerresourcemanager-sources-aidl", + ":framework-media-quality-sources-aidl", ], visibility: [ "//frameworks/base", diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index d082c7384fd1..32af7c6fca68 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -4857,7 +4857,7 @@ public class AudioManager { focusReceiver = addClientIdToFocusReceiverLocked(clientFakeId); } - return handleExternalAudioPolicyWaitIfNeeded(clientFakeId, focusReceiver); + return handleExternalAudioPolicyWaitIfNeeded(clientFakeId, focusReceiver, afr); } /** @@ -5070,7 +5070,7 @@ public class AudioManager { focusReceiver = addClientIdToFocusReceiverLocked(clientId); } - return handleExternalAudioPolicyWaitIfNeeded(clientId, focusReceiver); + return handleExternalAudioPolicyWaitIfNeeded(clientId, focusReceiver, afr); } @GuardedBy("mFocusRequestsLock") @@ -5086,11 +5086,20 @@ public class AudioManager { } private @FocusRequestResult int handleExternalAudioPolicyWaitIfNeeded(String clientId, - BlockingFocusResultReceiver focusReceiver) { + BlockingFocusResultReceiver focusReceiver, @NonNull AudioFocusRequest afr) { focusReceiver.waitForResult(EXT_FOCUS_POLICY_TIMEOUT_MS); - if (DEBUG && !focusReceiver.receivedResult()) { - Log.e(TAG, "handleExternalAudioPolicyWaitIfNeeded" - + " response from ext policy timed out, denying request"); + if (!focusReceiver.receivedResult()) { + if (DEBUG) { + Log.e(TAG, "handleExternalAudioPolicyWaitIfNeeded" + + " response from ext policy timed out, denying request"); + } + try { + // To prevent from orphan focus holder, cleanup + abandonAudioFocus(afr.getOnAudioFocusChangeListener()); + } catch (Exception e) { + Log.e(TAG, "handleExternalAudioPolicyWaitIfNeeded failed to abandon audio" + +" focus after time out, error: " + e.getMessage()); + } } synchronized (mFocusRequestsLock) { diff --git a/media/java/android/media/IMediaRouter2.aidl b/media/java/android/media/IMediaRouter2.aidl index 85bc8efe2750..e9590d50d719 100644 --- a/media/java/android/media/IMediaRouter2.aidl +++ b/media/java/android/media/IMediaRouter2.aidl @@ -18,6 +18,7 @@ package android.media; import android.media.MediaRoute2Info; import android.media.RoutingSessionInfo; +import android.media.SuggestedDeviceInfo; import android.os.Bundle; import android.os.UserHandle; @@ -37,4 +38,6 @@ oneway interface IMediaRouter2 { */ void requestCreateSessionByManager(long uniqueRequestId, in RoutingSessionInfo oldSession, in MediaRoute2Info route); + void notifyDeviceSuggestionsUpdated(String suggestingPackageName, + in List<SuggestedDeviceInfo> suggestions); } diff --git a/media/java/android/media/IMediaRouter2Manager.aidl b/media/java/android/media/IMediaRouter2Manager.aidl index 21908b2ca2e0..1c399d6958bb 100644 --- a/media/java/android/media/IMediaRouter2Manager.aidl +++ b/media/java/android/media/IMediaRouter2Manager.aidl @@ -21,6 +21,7 @@ import android.media.MediaRoute2Info; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.media.SuggestedDeviceInfo; /** * {@hide} @@ -33,6 +34,8 @@ oneway interface IMediaRouter2Manager { in RouteDiscoveryPreference discoveryPreference); void notifyRouteListingPreferenceChange(String packageName, in @nullable RouteListingPreference routeListingPreference); + void notifyDeviceSuggestionsUpdated(String packageName, String suggestingPackageName, + in @nullable List<SuggestedDeviceInfo> suggestedDeviceInfo); void notifyRoutesUpdated(in List<MediaRoute2Info> routes); void notifyRequestFailed(int requestId, int reason); void invalidateInstance(); diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl index 961962f6a010..60881f4bfc30 100644 --- a/media/java/android/media/IMediaRouterService.aidl +++ b/media/java/android/media/IMediaRouterService.aidl @@ -25,6 +25,7 @@ import android.media.MediaRouterClientState; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.media.SuggestedDeviceInfo; import android.os.Bundle; import android.os.UserHandle; /** @@ -72,6 +73,10 @@ interface IMediaRouterService { in MediaRoute2Info route); void setSessionVolumeWithRouter2(IMediaRouter2 router, String sessionId, int volume); void releaseSessionWithRouter2(IMediaRouter2 router, String sessionId); + void setDeviceSuggestionsWithRouter2(IMediaRouter2 router, + in @nullable List<SuggestedDeviceInfo> suggestedDeviceInfo); + @nullable Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2( + IMediaRouter2 router); // Methods for MediaRouter2Manager List<RoutingSessionInfo> getRemoteSessions(IMediaRouter2Manager manager); @@ -98,4 +103,8 @@ interface IMediaRouterService { String sessionId, int volume); void releaseSessionWithManager(IMediaRouter2Manager manager, int requestId, String sessionId); boolean showMediaOutputSwitcherWithProxyRouter(IMediaRouter2Manager manager); + void setDeviceSuggestionsWithManager(IMediaRouter2Manager manager, + in @nullable List<SuggestedDeviceInfo> suggestedDeviceInfo); + @nullable Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManager( + IMediaRouter2Manager manager); } diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java index 4e86eacea404..f3b21bfdaa3c 100644 --- a/media/java/android/media/MediaCodecInfo.java +++ b/media/java/android/media/MediaCodecInfo.java @@ -3836,6 +3836,151 @@ public final class MediaCodecInfo { maxBlocks, maxBlocksPerSecond, blockSize, blockSize, 1 /* widthAlignment */, 1 /* heightAlignment */); + } else if (GetFlag(() -> android.media.codec.Flags.apvSupport()) + && mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_APV)) { + maxBlocksPerSecond = 11880; + maxBps = 7000000; + + // Sample rate, and Bit rate for APV Codec, + // corresponding to the definitions in + // "10.1.4. Levels and bands" + // found at https://www.ietf.org/archive/id/draft-lim-apv-03.html + for (CodecProfileLevel profileLevel: profileLevels) { + long SR = 0; // luma sample rate + int BR = 0; // bit rate bps + switch (profileLevel.level) { + case CodecProfileLevel.APVLevel1Band0: + SR = 3041280; BR = 7000000; break; + case CodecProfileLevel.APVLevel1Band1: + SR = 3041280; BR = 11000000; break; + case CodecProfileLevel.APVLevel1Band2: + SR = 3041280; BR = 14000000; break; + case CodecProfileLevel.APVLevel1Band3: + SR = 3041280; BR = 21000000; break; + case CodecProfileLevel.APVLevel11Band0: + SR = 6082560; BR = 14000000; break; + case CodecProfileLevel.APVLevel11Band1: + SR = 6082560; BR = 21000000; break; + case CodecProfileLevel.APVLevel11Band2: + SR = 6082560; BR = 28000000; break; + case CodecProfileLevel.APVLevel11Band3: + SR = 6082560; BR = 42000000; break; + case CodecProfileLevel.APVLevel2Band0: + SR = 15667200; BR = 36000000; break; + case CodecProfileLevel.APVLevel2Band1: + SR = 15667200; BR = 53000000; break; + case CodecProfileLevel.APVLevel2Band2: + SR = 15667200; BR = 71000000; break; + case CodecProfileLevel.APVLevel2Band3: + SR = 15667200; BR = 106000000; break; + case CodecProfileLevel.APVLevel21Band0: + SR = 31334400; BR = 71000000; break; + case CodecProfileLevel.APVLevel21Band1: + SR = 31334400; BR = 106000000; break; + case CodecProfileLevel.APVLevel21Band2: + SR = 31334400; BR = 141000000; break; + case CodecProfileLevel.APVLevel21Band3: + SR = 31334400; BR = 212000000; break; + case CodecProfileLevel.APVLevel3Band0: + SR = 66846720; BR = 101000000; break; + case CodecProfileLevel.APVLevel3Band1: + SR = 66846720; BR = 151000000; break; + case CodecProfileLevel.APVLevel3Band2: + SR = 66846720; BR = 201000000; break; + case CodecProfileLevel.APVLevel3Band3: + SR = 66846720; BR = 301000000; break; + case CodecProfileLevel.APVLevel31Band0: + SR = 133693440; BR = 201000000; break; + case CodecProfileLevel.APVLevel31Band1: + SR = 133693440; BR = 301000000; break; + case CodecProfileLevel.APVLevel31Band2: + SR = 133693440; BR = 401000000; break; + case CodecProfileLevel.APVLevel31Band3: + SR = 133693440; BR = 602000000; break; + case CodecProfileLevel.APVLevel4Band0: + SR = 265420800; BR = 401000000; break; + case CodecProfileLevel.APVLevel4Band1: + SR = 265420800; BR = 602000000; break; + case CodecProfileLevel.APVLevel4Band2: + SR = 265420800; BR = 780000000; break; + case CodecProfileLevel.APVLevel4Band3: + SR = 265420800; BR = 1170000000; break; + case CodecProfileLevel.APVLevel41Band0: + SR = 530841600; BR = 780000000; break; + case CodecProfileLevel.APVLevel41Band1: + SR = 530841600; BR = 1170000000; break; + case CodecProfileLevel.APVLevel41Band2: + SR = 530841600; BR = 1560000000; break; + case CodecProfileLevel.APVLevel41Band3: + // Current API allows bitrates only up to Max Integer + // Hence we are limiting internal limits to Integer.MAX_VALUE + // even when actual Level/Band limits are higher + SR = 530841600; BR = Integer.MAX_VALUE; break; + case CodecProfileLevel.APVLevel5Band0: + SR = 1061683200; BR = 1560000000; break; + case CodecProfileLevel.APVLevel5Band1: + SR = 1061683200; BR = Integer.MAX_VALUE; break; + case CodecProfileLevel.APVLevel5Band2: + SR = 1061683200; BR = Integer.MAX_VALUE; break; + case CodecProfileLevel.APVLevel5Band3: + SR = 1061683200; BR = Integer.MAX_VALUE; break; + case CodecProfileLevel.APVLevel51Band0: + case CodecProfileLevel.APVLevel51Band1: + case CodecProfileLevel.APVLevel51Band2: + case CodecProfileLevel.APVLevel51Band3: + SR = 2123366400; BR = Integer.MAX_VALUE; break; + case CodecProfileLevel.APVLevel6Band0: + case CodecProfileLevel.APVLevel6Band1: + case CodecProfileLevel.APVLevel6Band2: + case CodecProfileLevel.APVLevel6Band3: + SR = 4777574400L; BR = Integer.MAX_VALUE; break; + case CodecProfileLevel.APVLevel61Band0: + case CodecProfileLevel.APVLevel61Band1: + case CodecProfileLevel.APVLevel61Band2: + case CodecProfileLevel.APVLevel61Band3: + SR = 8493465600L; BR = Integer.MAX_VALUE; break; + case CodecProfileLevel.APVLevel7Band0: + case CodecProfileLevel.APVLevel7Band1: + case CodecProfileLevel.APVLevel7Band2: + case CodecProfileLevel.APVLevel7Band3: + SR = 16986931200L; BR = Integer.MAX_VALUE; break; + case CodecProfileLevel.APVLevel71Band0: + case CodecProfileLevel.APVLevel71Band1: + case CodecProfileLevel.APVLevel71Band2: + case CodecProfileLevel.APVLevel71Band3: + SR = 33973862400L; BR = Integer.MAX_VALUE; break; + default: + Log.w(TAG, "Unrecognized level " + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.APVProfile422_10: + case CodecProfileLevel.APVProfile422_10HDR10: + case CodecProfileLevel.APVProfile422_10HDR10Plus: + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + errors &= ~ERROR_NONE_SUPPORTED; + maxBlocksPerSecond = Math.max(SR, maxBlocksPerSecond); + maxBps = Math.max(BR, maxBps); + } + + final int blockSize = 16; + maxBlocks = Integer.MAX_VALUE; + maxBlocksPerSecond = Utils.divUp(maxBlocksPerSecond, blockSize * blockSize); + maxBlocks = (int) Math.min((long) maxBlocks, maxBlocksPerSecond); + // Max frame size in APV is 2^24 + int maxLengthInBlocks = Utils.divUp((int) Math.pow(2, 24), blockSize); + maxLengthInBlocks = Math.min(maxLengthInBlocks, maxBlocks); + applyMacroBlockLimits( + maxLengthInBlocks, maxLengthInBlocks, + maxBlocks, maxBlocksPerSecond, + blockSize, blockSize, + 2 /* widthAlignment */, 1 /* heightAlignment */); } else { Log.w(TAG, "Unsupported mime " + mime); // using minimal bitrate here. should be overriden by diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 3af36a404c30..db305effac3f 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -22,6 +22,7 @@ import static com.android.media.flags.Flags.FLAG_ENABLE_GET_TRANSFERABLE_ROUTES; import static com.android.media.flags.Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL; import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2; import static com.android.media.flags.Flags.FLAG_ENABLE_SCREEN_OFF_SCANNING; +import static com.android.media.flags.Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API; import android.Manifest; import android.annotation.CallbackExecutor; @@ -159,6 +160,8 @@ public final class MediaRouter2 { new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList<ControllerCallbackRecord> mControllerCallbackRecords = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList<DeviceSuggestionsCallbackRecord> + mDeviceSuggestionsCallbackRecords = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList<ControllerCreationRequest> mControllerCreationRequests = new CopyOnWriteArrayList<>(); @@ -198,6 +201,10 @@ public final class MediaRouter2 { @Nullable private RouteListingPreference mRouteListingPreference; + @GuardedBy("mLock") + @Nullable + private Map<String, List<SuggestedDeviceInfo>> mSuggestedDeviceInfo = new HashMap<>(); + /** * Stores an auxiliary copy of {@link #mFilteredRoutes} at the time of the last route callback * dispatch. This is only used to determine what callback a route should be assigned to (added, @@ -760,6 +767,27 @@ public final class MediaRouter2 { } /** + * Registers the given callback to be invoked when the {@link SuggestedDeviceInfo} of the target + * router changes. + * + * <p>Calls using a previously registered callback will overwrite the previous executor. + * + * @hide + */ + public void registerDeviceSuggestionsCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull DeviceSuggestionsCallback deviceSuggestionsCallback) { + Objects.requireNonNull(executor, "executor must not be null"); + Objects.requireNonNull(deviceSuggestionsCallback, "callback must not be null"); + + DeviceSuggestionsCallbackRecord record = + new DeviceSuggestionsCallbackRecord(executor, deviceSuggestionsCallback); + + mDeviceSuggestionsCallbackRecords.remove(record); + mDeviceSuggestionsCallbackRecords.add(record); + } + + /** * Unregisters the given callback to not receive {@link RouteListingPreference} change events. * * @see #registerRouteListingPreferenceUpdatedCallback(Executor, Consumer) @@ -779,6 +807,21 @@ public final class MediaRouter2 { } /** + * Unregisters the given callback to not receive {@link SuggestedDeviceInfo} change events. + * + * @see #registerDeviceSuggestionsCallback(Executor, DeviceSuggestionsCallback) + * @hide + */ + public void unregisterDeviceSuggestionsCallback(@NonNull DeviceSuggestionsCallback callback) { + Objects.requireNonNull(callback, "callback must not be null"); + + if (!mDeviceSuggestionsCallbackRecords.remove( + new DeviceSuggestionsCallbackRecord(/* executor */ null, callback))) { + Log.w(TAG, "unregisterDeviceSuggestionsCallback: Ignoring an unknown" + " callback"); + } + } + + /** * Shows the system output switcher dialog. * * <p>Should only be called when the context of MediaRouter2 is in the foreground and visible on @@ -832,6 +875,36 @@ public final class MediaRouter2 { } /** + * Sets the suggested devices. + * + * <p>Use this method to inform the system UI that this device is suggested in the Output + * Switcher and media controls. + * + * <p>You should pass null to this method to clear a previously set suggestion without setting a + * new one. + * + * @param suggestedDeviceInfo The {@link SuggestedDeviceInfo} the router suggests should be + * provided to the user. + * @hide + */ + @FlaggedApi(FLAG_ENABLE_SUGGESTED_DEVICE_API) + public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + mImpl.setDeviceSuggestions(suggestedDeviceInfo); + } + + /** + * Gets the current suggested devices. + * + * @return the suggested devices, keyed by the package name providing each suggestion list. + * @hide + */ + @FlaggedApi(FLAG_ENABLE_SUGGESTED_DEVICE_API) + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() { + return mImpl.getDeviceSuggestions(); + } + + /** * Returns the current {@link RouteListingPreference} of the target router. * * <p>If this instance was created using {@code #getInstance(Context, String)}, then it returns @@ -1518,6 +1591,17 @@ public final class MediaRouter2 { } } + private void notifyDeviceSuggestionsUpdated( + @NonNull String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> deviceSuggestions) { + for (DeviceSuggestionsCallbackRecord record : mDeviceSuggestionsCallbackRecords) { + record.mExecutor.execute( + () -> + record.mDeviceSuggestionsCallback.onSuggestionUpdated( + suggestingPackageName, deviceSuggestions)); + } + } + private void notifyTransfer(RoutingController oldController, RoutingController newController) { for (TransferCallbackRecord record : mTransferCallbackRecords) { record.mExecutor.execute( @@ -1568,6 +1652,25 @@ public final class MediaRouter2 { .build(); } + /** + * Callback for receiving events about device suggestions + * + * @hide + */ + public interface DeviceSuggestionsCallback { + + /** + * Called when suggestions are updated. Whenever you register a callback, this will be + * invoked with the current suggestions. + * + * @param suggestingPackageName the package that provided the suggestions. + * @param suggestedDeviceInfo the suggestions provided by the package. + */ + void onSuggestionUpdated( + @NonNull String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo); + } + /** Callback for receiving events about media route discovery. */ public abstract static class RouteCallback { /** @@ -2326,6 +2429,35 @@ public final class MediaRouter2 { } } + private static final class DeviceSuggestionsCallbackRecord { + public final Executor mExecutor; + public final DeviceSuggestionsCallback mDeviceSuggestionsCallback; + + /* package */ DeviceSuggestionsCallbackRecord( + @NonNull Executor executor, + @NonNull DeviceSuggestionsCallback deviceSuggestionsCallback) { + mExecutor = executor; + mDeviceSuggestionsCallback = deviceSuggestionsCallback; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof DeviceSuggestionsCallbackRecord)) { + return false; + } + return mDeviceSuggestionsCallback + == ((DeviceSuggestionsCallbackRecord) obj).mDeviceSuggestionsCallback; + } + + @Override + public int hashCode() { + return mDeviceSuggestionsCallback.hashCode(); + } + } + static final class TransferCallbackRecord { public final Executor mExecutor; public final TransferCallback mTransferCallback; @@ -2446,6 +2578,17 @@ public final class MediaRouter2 { } @Override + public void notifyDeviceSuggestionsUpdated( + String suggestingPackageName, List<SuggestedDeviceInfo> suggestions) { + mHandler.sendMessage( + obtainMessage( + MediaRouter2::notifyDeviceSuggestionsUpdated, + MediaRouter2.this, + suggestingPackageName, + suggestions)); + } + + @Override public void requestCreateSessionByManager( long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route) { mHandler.sendMessage( @@ -2487,6 +2630,11 @@ public final class MediaRouter2 { void setRouteListingPreference(@Nullable RouteListingPreference preference); + void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo); + + @Nullable + Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions(); + boolean showSystemOutputSwitcher(); List<MediaRoute2Info> getAllRoutes(); @@ -2687,6 +2835,29 @@ public final class MediaRouter2 { } @Override + public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + synchronized (mLock) { + try { + mMediaRouterService.setDeviceSuggestionsWithManager( + mClient, suggestedDeviceInfo); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); + } + } + } + + @Override + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() { + synchronized (mLock) { + try { + return mMediaRouterService.getDeviceSuggestionsWithManager(mClient); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + } + + @Override public boolean showSystemOutputSwitcher() { try { return mMediaRouterService.showMediaOutputSwitcherWithProxyRouter(mClient); @@ -3296,6 +3467,23 @@ public final class MediaRouter2 { notifyRouteListingPreferenceUpdated(routeListingPreference); } + private void onDeviceSuggestionsChangeHandler( + @NonNull String packageName, + @NonNull String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + if (!TextUtils.equals(getClientPackageName(), packageName)) { + return; + } + synchronized (mLock) { + if (Objects.equals( + mSuggestedDeviceInfo.get(suggestingPackageName), suggestedDeviceInfo)) { + return; + } + mSuggestedDeviceInfo.put(suggestingPackageName, suggestedDeviceInfo); + } + notifyDeviceSuggestionsUpdated(suggestingPackageName, suggestedDeviceInfo); + } + private void onRequestFailedOnHandler(int requestId, int reason) { MediaRouter2Manager.TransferRequest matchingRequest = null; for (MediaRouter2Manager.TransferRequest request : mTransferRequests) { @@ -3390,6 +3578,20 @@ public final class MediaRouter2 { } @Override + public void notifyDeviceSuggestionsUpdated( + String packageName, + String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> deviceSuggestions) { + mHandler.sendMessage( + obtainMessage( + ProxyMediaRouter2Impl::onDeviceSuggestionsChangeHandler, + ProxyMediaRouter2Impl.this, + packageName, + suggestingPackageName, + deviceSuggestions)); + } + + @Override public void notifyRoutesUpdated(List<MediaRoute2Info> routes) { mHandler.sendMessage( obtainMessage( @@ -3553,6 +3755,30 @@ public final class MediaRouter2 { } @Override + public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> deviceSuggestions) { + synchronized (mLock) { + try { + registerRouterStubIfNeededLocked(); + mMediaRouterService.setDeviceSuggestionsWithRouter2(mStub, deviceSuggestions); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); + } + } + } + + @Override + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() { + synchronized (mLock) { + try { + return mMediaRouterService.getDeviceSuggestionsWithRouter2(mStub); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + } + + @Override public boolean showSystemOutputSwitcher() { synchronized (mLock) { try { diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 3f18eef2f9aa..bf88709eec33 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -1138,6 +1138,14 @@ public final class MediaRouter2Manager { } @Override + public void notifyDeviceSuggestionsUpdated( + String packageName, + String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + // MediaRouter2Manager doesn't support device suggestions + } + + @Override public void notifyRoutesUpdated(List<MediaRoute2Info> routes) { mHandler.sendMessage( obtainMessage( diff --git a/media/java/android/media/SuggestedDeviceInfo.aidl b/media/java/android/media/SuggestedDeviceInfo.aidl new file mode 100644 index 000000000000..eab642572ed2 --- /dev/null +++ b/media/java/android/media/SuggestedDeviceInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +parcelable SuggestedDeviceInfo; diff --git a/media/java/android/media/SuggestedDeviceInfo.java b/media/java/android/media/SuggestedDeviceInfo.java new file mode 100644 index 000000000000..2aa139fcca17 --- /dev/null +++ b/media/java/android/media/SuggestedDeviceInfo.java @@ -0,0 +1,235 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import static com.android.media.flags.Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.Objects; + +/** + * Allows applications to suggest routes to the system UI (for example, in the System UI Output + * Switcher). + * + * @see MediaRouter2#setSuggestedDevice + * @hide + */ +@FlaggedApi(FLAG_ENABLE_SUGGESTED_DEVICE_API) +public final class SuggestedDeviceInfo implements Parcelable { + @NonNull + public static final Creator<SuggestedDeviceInfo> CREATOR = + new Creator<>() { + @Override + public SuggestedDeviceInfo createFromParcel(Parcel in) { + return new SuggestedDeviceInfo(in); + } + + @Override + public SuggestedDeviceInfo[] newArray(int size) { + return new SuggestedDeviceInfo[size]; + } + }; + + @NonNull private final String mDeviceDisplayName; + + @NonNull private final String mRouteId; + + private final int mType; + + @NonNull private final Bundle mExtras; + + private SuggestedDeviceInfo(Builder builder) { + mDeviceDisplayName = builder.mDeviceDisplayName; + mRouteId = builder.mRouteId; + mType = builder.mType; + mExtras = builder.mExtras; + } + + private SuggestedDeviceInfo(Parcel in) { + mDeviceDisplayName = in.readString(); + mRouteId = in.readString(); + mType = in.readInt(); + mExtras = in.readBundle(); + } + + /** + * Returns the name to be displayed to the user. + * + * @return The device display name. + */ + @NonNull + public String getDeviceDisplayName() { + return mDeviceDisplayName; + } + + /** + * Returns the route ID associated with the suggestion. + * + * @return The route ID. + */ + @NonNull + public String getRouteId() { + return mRouteId; + } + + /** + * Returns the device type associated with the suggestion. + * + * @return The device type. + */ + public int getType() { + return mType; + } + + /** + * Returns the extras associated with the suggestion. + * + * @return The extras. + */ + @Nullable + public Bundle getExtras() { + return mExtras; + } + + // SuggestedDeviceInfo Parcelable implementation. + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(mDeviceDisplayName); + dest.writeString(mRouteId); + dest.writeInt(mType); + dest.writeBundle(mExtras); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof SuggestedDeviceInfo)) { + return false; + } + return Objects.equals(mDeviceDisplayName, ((SuggestedDeviceInfo) obj).mDeviceDisplayName) + && Objects.equals(mRouteId, ((SuggestedDeviceInfo) obj).mRouteId) + && mType == ((SuggestedDeviceInfo) obj).mType; + } + + @Override + public int hashCode() { + return Objects.hash(mDeviceDisplayName, mRouteId, mType); + } + + @Override + public String toString() { + return mDeviceDisplayName + " | " + mRouteId + " | " + mType; + } + + /** Builder for {@link SuggestedDeviceInfo}. */ + public static final class Builder { + @NonNull private String mDeviceDisplayName; + + @NonNull private String mRouteId; + + @NonNull private Integer mType; + + private Bundle mExtras = Bundle.EMPTY; + + /** + * Creates a new SuggestedDeviceInfo. The device display name, route ID, and type must be + * set. The extras cannot be null, but default to an empty {@link Bundle}. + * + * @throws IllegalArgumentException if the builder has a mandatory unset field. + */ + public SuggestedDeviceInfo build() { + if (mDeviceDisplayName == null) { + throw new IllegalArgumentException("Device display name cannot be null"); + } + + if (mRouteId == null) { + throw new IllegalArgumentException("Route ID cannot be null."); + } + + if (mType == null) { + throw new IllegalArgumentException("Device type cannot be null."); + } + + if (mExtras == null) { + throw new IllegalArgumentException("Extras cannot be null."); + } + + return new SuggestedDeviceInfo(this); + } + + /** + * Sets the {@link #getDeviceDisplayName() device display name}. + * + * @throws IllegalArgumentException if the name is null or empty. + */ + public Builder setDeviceDisplayName(@NonNull String deviceDisplayName) { + if (TextUtils.isEmpty(deviceDisplayName)) { + throw new IllegalArgumentException("Device display name cannot be null"); + } + mDeviceDisplayName = deviceDisplayName; + return this; + } + + /** + * Sets the {@link #getRouteId() route id}. + * + * @throws IllegalArgumentException if the route id is null or empty. + */ + public Builder setRouteId(@NonNull String routeId) { + if (TextUtils.isEmpty(routeId)) { + throw new IllegalArgumentException("Device display name cannot be null"); + } + mRouteId = routeId; + return this; + } + + /** Sets the {@link #getType() device type}. */ + public Builder setType(int type) { + mType = type; + return this; + } + + /** + * Sets the {@link #getExtras() extras}. + * + * <p>The default value is an empty {@link Bundle}. + * + * <p>Do not mutate the given {@link Bundle} after passing it to this method. You can use + * {@link Bundle#deepCopy()} to keep a mutable copy. + * + * @throws NullPointerException if the extras are null. + */ + public Builder setExtras(@NonNull Bundle extras) { + mExtras = Objects.requireNonNull(extras, "extras must not be null"); + return this; + } + } +} diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index 0deed3982d9b..e39a0aa8717e 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -62,6 +62,14 @@ flag { } flag { + name: "enable_suggested_device_api" + is_exported: true + namespace: "media_better_together" + description: "Enables the API allowing proxy routers to suggest routes." + bug: "393216553" +} + +flag { name: "enable_full_scan_with_media_content_control" namespace: "media_better_together" description: "Allows holders of the MEDIA_CONTENT_CONTROL permission to scan for routes while not in the foreground." diff --git a/media/java/android/media/quality/Android.bp b/media/java/android/media/quality/Android.bp new file mode 100644 index 000000000000..080d5266ccb7 --- /dev/null +++ b/media/java/android/media/quality/Android.bp @@ -0,0 +1,39 @@ +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"], +} + +filegroup { + name: "framework-media-quality-sources-aidl", + srcs: [ + "aidl/android/media/quality/*.aidl", + ], + path: "aidl", +} + +aidl_interface { + name: "media_quality_aidl_interface", + unstable: true, + local_include_dir: "aidl", + backend: { + java: { + enabled: true, + }, + cpp: { + enabled: false, + }, + ndk: { + enabled: false, + }, + rust: { + enabled: false, + }, + }, + srcs: [ + ":framework-media-quality-sources-aidl", + ], +} diff --git a/media/java/android/media/quality/MediaQualityContract.java b/media/java/android/media/quality/MediaQualityContract.java index e4de3e4420fe..fccdba8e727f 100644 --- a/media/java/android/media/quality/MediaQualityContract.java +++ b/media/java/android/media/quality/MediaQualityContract.java @@ -82,7 +82,7 @@ public class MediaQualityContract { String PARAMETER_NAME = "_name"; String PARAMETER_PACKAGE = "_package"; String PARAMETER_INPUT_ID = "_input_id"; - + String VENDOR_PARAMETERS = "_vendor_parameters"; } /** diff --git a/media/java/android/media/quality/MediaQualityManager.java b/media/java/android/media/quality/MediaQualityManager.java index 0d6d32a22dae..bfd01380a2ee 100644 --- a/media/java/android/media/quality/MediaQualityManager.java +++ b/media/java/android/media/quality/MediaQualityManager.java @@ -274,9 +274,9 @@ public final class MediaQualityManager { @NonNull String name, @Nullable ProfileQueryParams options) { try { - Bundle optionsBundle = options == null - ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle(); - return mService.getPictureProfile(type, name, optionsBundle, mUserHandle); + boolean includeParams = options == null || options.mParametersIncluded; + return mService.getPictureProfile( + type, name, includeParams, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -299,10 +299,9 @@ public final class MediaQualityManager { public List<PictureProfile> getPictureProfilesByPackage( @NonNull String packageName, @Nullable ProfileQueryParams options) { try { - Bundle optionsBundle = options == null - ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle(); + boolean includeParams = options == null || options.mParametersIncluded; return mService.getPictureProfilesByPackage( - packageName, optionsBundle, mUserHandle); + packageName, includeParams, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -321,9 +320,8 @@ public final class MediaQualityManager { @NonNull public List<PictureProfile> getAvailablePictureProfiles(@Nullable ProfileQueryParams options) { try { - Bundle optionsBundle = options == null - ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle(); - return mService.getAvailablePictureProfiles(optionsBundle, mUserHandle); + boolean includeParams = options == null || options.mParametersIncluded; + return mService.getAvailablePictureProfiles(includeParams, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -344,7 +342,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) public boolean setDefaultPictureProfile(@Nullable String pictureProfileId) { try { - return mService.setDefaultPictureProfile(pictureProfileId, mUserHandle); + return mService.setDefaultPictureProfile(pictureProfileId, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -361,7 +359,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) public List<String> getPictureProfilePackageNames() { try { - return mService.getPictureProfilePackageNames(mUserHandle); + return mService.getPictureProfilePackageNames(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -373,7 +371,7 @@ public final class MediaQualityManager { */ public List<PictureProfileHandle> getPictureProfileHandle(String[] id) { try { - return mService.getPictureProfileHandle(id, mUserHandle); + return mService.getPictureProfileHandle(id, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -385,7 +383,7 @@ public final class MediaQualityManager { */ public List<SoundProfileHandle> getSoundProfileHandle(String[] id) { try { - return mService.getSoundProfileHandle(id, mUserHandle); + return mService.getSoundProfileHandle(id, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -401,7 +399,7 @@ public final class MediaQualityManager { */ public void createPictureProfile(@NonNull PictureProfile pp) { try { - mService.createPictureProfile(pp, mUserHandle); + mService.createPictureProfile(pp, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -416,7 +414,7 @@ public final class MediaQualityManager { */ public void updatePictureProfile(@NonNull String profileId, @NonNull PictureProfile pp) { try { - mService.updatePictureProfile(profileId, pp, mUserHandle); + mService.updatePictureProfile(profileId, pp, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -430,7 +428,7 @@ public final class MediaQualityManager { */ public void removePictureProfile(@NonNull String profileId) { try { - mService.removePictureProfile(profileId, mUserHandle); + mService.removePictureProfile(profileId, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -484,9 +482,8 @@ public final class MediaQualityManager { @NonNull String name, @Nullable ProfileQueryParams options) { try { - Bundle optionsBundle = options == null - ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle(); - return mService.getSoundProfile(type, name, optionsBundle, mUserHandle); + boolean includeParams = options == null || options.mParametersIncluded; + return mService.getSoundProfile(type, name, includeParams, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -510,9 +507,9 @@ public final class MediaQualityManager { public List<SoundProfile> getSoundProfilesByPackage( @NonNull String packageName, @Nullable ProfileQueryParams options) { try { - Bundle optionsBundle = options == null - ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle(); - return mService.getSoundProfilesByPackage(packageName, optionsBundle, mUserHandle); + boolean includeParams = options == null || options.mParametersIncluded; + return mService.getSoundProfilesByPackage( + packageName, includeParams, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -531,9 +528,8 @@ public final class MediaQualityManager { @NonNull public List<SoundProfile> getAvailableSoundProfiles(@Nullable ProfileQueryParams options) { try { - Bundle optionsBundle = options == null - ? ProfileQueryParams.DEFAULT.toBundle() : options.toBundle(); - return mService.getAvailableSoundProfiles(optionsBundle, mUserHandle); + boolean includeParams = options == null || options.mParametersIncluded; + return mService.getAvailableSoundProfiles(includeParams, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -554,7 +550,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE) public boolean setDefaultSoundProfile(@Nullable String soundProfileId) { try { - return mService.setDefaultSoundProfile(soundProfileId, mUserHandle); + return mService.setDefaultSoundProfile(soundProfileId, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -572,7 +568,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE) public List<String> getSoundProfilePackageNames() { try { - return mService.getSoundProfilePackageNames(mUserHandle); + return mService.getSoundProfilePackageNames(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -589,7 +585,7 @@ public final class MediaQualityManager { */ public void createSoundProfile(@NonNull SoundProfile sp) { try { - mService.createSoundProfile(sp, mUserHandle); + mService.createSoundProfile(sp, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -604,7 +600,7 @@ public final class MediaQualityManager { */ public void updateSoundProfile(@NonNull String profileId, @NonNull SoundProfile sp) { try { - mService.updateSoundProfile(profileId, sp, mUserHandle); + mService.updateSoundProfile(profileId, sp, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -618,7 +614,7 @@ public final class MediaQualityManager { */ public void removeSoundProfile(@NonNull String profileId) { try { - mService.removeSoundProfile(profileId, mUserHandle); + mService.removeSoundProfile(profileId, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -636,7 +632,7 @@ public final class MediaQualityManager { @NonNull public List<ParameterCapability> getParameterCapabilities(@NonNull List<String> names) { try { - return mService.getParameterCapabilities(names, mUserHandle); + return mService.getParameterCapabilities(names, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -654,7 +650,7 @@ public final class MediaQualityManager { @NonNull public List<String> getPictureProfileAllowList() { try { - return mService.getPictureProfileAllowList(mUserHandle); + return mService.getPictureProfileAllowList(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -668,7 +664,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) public void setPictureProfileAllowList(@NonNull List<String> packageNames) { try { - mService.setPictureProfileAllowList(packageNames, mUserHandle); + mService.setPictureProfileAllowList(packageNames, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -686,7 +682,7 @@ public final class MediaQualityManager { @NonNull public List<String> getSoundProfileAllowList() { try { - return mService.getSoundProfileAllowList(mUserHandle); + return mService.getSoundProfileAllowList(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -700,7 +696,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE) public void setSoundProfileAllowList(@NonNull List<String> packageNames) { try { - mService.setSoundProfileAllowList(packageNames, mUserHandle); + mService.setSoundProfileAllowList(packageNames, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -712,7 +708,7 @@ public final class MediaQualityManager { */ public boolean isSupported() { try { - return mService.isSupported(mUserHandle); + return mService.isSupported(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -730,7 +726,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) public void setAutoPictureQualityEnabled(boolean enabled) { try { - mService.setAutoPictureQualityEnabled(enabled, mUserHandle); + mService.setAutoPictureQualityEnabled(enabled, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -741,7 +737,7 @@ public final class MediaQualityManager { */ public boolean isAutoPictureQualityEnabled() { try { - return mService.isAutoPictureQualityEnabled(mUserHandle); + return mService.isAutoPictureQualityEnabled(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -758,7 +754,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) public void setSuperResolutionEnabled(boolean enabled) { try { - mService.setSuperResolutionEnabled(enabled, mUserHandle); + mService.setSuperResolutionEnabled(enabled, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -769,7 +765,7 @@ public final class MediaQualityManager { */ public boolean isSuperResolutionEnabled() { try { - return mService.isSuperResolutionEnabled(mUserHandle); + return mService.isSuperResolutionEnabled(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -787,7 +783,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE) public void setAutoSoundQualityEnabled(boolean enabled) { try { - mService.setAutoSoundQualityEnabled(enabled, mUserHandle); + mService.setAutoSoundQualityEnabled(enabled, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -798,7 +794,7 @@ public final class MediaQualityManager { */ public boolean isAutoSoundQualityEnabled() { try { - return mService.isAutoSoundQualityEnabled(mUserHandle); + return mService.isAutoSoundQualityEnabled(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -847,7 +843,7 @@ public final class MediaQualityManager { @NonNull AmbientBacklightSettings settings) { Preconditions.checkNotNull(settings); try { - mService.setAmbientBacklightSettings(settings, mUserHandle); + mService.setAmbientBacklightSettings(settings, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -858,7 +854,7 @@ public final class MediaQualityManager { */ public boolean isAmbientBacklightEnabled() { try { - return mService.isAmbientBacklightEnabled(mUserHandle); + return mService.isAmbientBacklightEnabled(mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -872,7 +868,7 @@ public final class MediaQualityManager { @RequiresPermission(android.Manifest.permission.READ_COLOR_ZONES) public void setAmbientBacklightEnabled(boolean enabled) { try { - mService.setAmbientBacklightEnabled(enabled, mUserHandle); + mService.setAmbientBacklightEnabled(enabled, mUserHandle.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/media/java/android/media/quality/SoundProfileHandle.java b/media/java/android/media/quality/SoundProfileHandle.java deleted file mode 100644 index edb546efdaf3..000000000000 --- a/media/java/android/media/quality/SoundProfileHandle.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.media.quality; - -import android.annotation.NonNull; -import android.os.Parcel; -import android.os.Parcelable; - -/** - * A type-safe handle to a sound profile. - * - * @hide - */ -public final class SoundProfileHandle implements Parcelable { - public static final @NonNull SoundProfileHandle NONE = new SoundProfileHandle(-1000); - - private final long mId; - - /** @hide */ - public SoundProfileHandle(long id) { - mId = id; - } - - /** @hide */ - public long getId() { - return mId; - } - - /** @hide */ - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeLong(mId); - } - - /** @hide */ - @Override - public int describeContents() { - return 0; - } - - /** @hide */ - public static final @NonNull Creator<SoundProfileHandle> CREATOR = - new Creator<SoundProfileHandle>() { - @Override - public SoundProfileHandle createFromParcel(Parcel in) { - return new SoundProfileHandle(in); - } - - @Override - public SoundProfileHandle[] newArray(int size) { - return new SoundProfileHandle[size]; - } - }; - - private SoundProfileHandle(@NonNull Parcel in) { - mId = in.readLong(); - } -} diff --git a/media/java/android/media/quality/ActiveProcessingPicture.aidl b/media/java/android/media/quality/aidl/android/media/quality/ActiveProcessingPicture.aidl index 2851306f6e4d..2851306f6e4d 100644 --- a/media/java/android/media/quality/ActiveProcessingPicture.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/ActiveProcessingPicture.aidl diff --git a/media/java/android/media/quality/AmbientBacklightEvent.aidl b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightEvent.aidl index 174cd461e846..174cd461e846 100644 --- a/media/java/android/media/quality/AmbientBacklightEvent.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightEvent.aidl diff --git a/media/java/android/media/quality/AmbientBacklightMetadata.aidl b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightMetadata.aidl index b95a474fbf90..b95a474fbf90 100644 --- a/media/java/android/media/quality/AmbientBacklightMetadata.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightMetadata.aidl diff --git a/media/java/android/media/quality/AmbientBacklightSettings.aidl b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightSettings.aidl index e2cdd03194cd..e2cdd03194cd 100644 --- a/media/java/android/media/quality/AmbientBacklightSettings.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightSettings.aidl diff --git a/media/java/android/media/quality/IAmbientBacklightCallback.aidl b/media/java/android/media/quality/aidl/android/media/quality/IAmbientBacklightCallback.aidl index 159f5b7b5e71..159f5b7b5e71 100644 --- a/media/java/android/media/quality/IAmbientBacklightCallback.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/IAmbientBacklightCallback.aidl diff --git a/media/java/android/media/quality/IMediaQualityManager.aidl b/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl index 6e9fa1dcf93d..0191ea786de0 100644 --- a/media/java/android/media/quality/IMediaQualityManager.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl @@ -25,57 +25,57 @@ import android.media.quality.PictureProfileHandle; import android.media.quality.PictureProfile; import android.media.quality.SoundProfileHandle; import android.media.quality.SoundProfile; -import android.os.Bundle; -import android.os.UserHandle; /** * Interface for Media Quality Manager * @hide */ interface IMediaQualityManager { - PictureProfile createPictureProfile(in PictureProfile pp, in UserHandle user); - void updatePictureProfile(in String id, in PictureProfile pp, in UserHandle user); - void removePictureProfile(in String id, in UserHandle user); - boolean setDefaultPictureProfile(in String id, in UserHandle user); + // TODO: use UserHandle + PictureProfile createPictureProfile(in PictureProfile pp, int userId); + void updatePictureProfile(in String id, in PictureProfile pp, int userId); + void removePictureProfile(in String id, int userId); + boolean setDefaultPictureProfile(in String id, int userId); + // TODO: use Bundle for includeParams PictureProfile getPictureProfile( - in int type, in String name, in Bundle options, in UserHandle user); + in int type, in String name, in boolean includeParams, int userId); List<PictureProfile> getPictureProfilesByPackage( - in String packageName, in Bundle options, in UserHandle user); - List<PictureProfile> getAvailablePictureProfiles(in Bundle options, in UserHandle user); - List<String> getPictureProfilePackageNames(in UserHandle user); - List<String> getPictureProfileAllowList(in UserHandle user); - void setPictureProfileAllowList(in List<String> packages, in UserHandle user); - List<PictureProfileHandle> getPictureProfileHandle(in String[] id, in UserHandle user); + in String packageName, in boolean includeParams, int userId); + List<PictureProfile> getAvailablePictureProfiles(in boolean includeParams, int userId); + List<String> getPictureProfilePackageNames(int userId); + List<String> getPictureProfileAllowList(int userId); + void setPictureProfileAllowList(in List<String> packages, int userId); + List<PictureProfileHandle> getPictureProfileHandle(in String[] id, int userId); - SoundProfile createSoundProfile(in SoundProfile pp, in UserHandle user); - void updateSoundProfile(in String id, in SoundProfile pp, in UserHandle user); - void removeSoundProfile(in String id, in UserHandle user); - boolean setDefaultSoundProfile(in String id, in UserHandle user); + SoundProfile createSoundProfile(in SoundProfile pp, int userId); + void updateSoundProfile(in String id, in SoundProfile pp, int userId); + void removeSoundProfile(in String id, int userId); + boolean setDefaultSoundProfile(in String id, int userId); SoundProfile getSoundProfile( - in int type, in String name, in Bundle options, in UserHandle user); + in int type, in String name, in boolean includeParams, int userId); List<SoundProfile> getSoundProfilesByPackage( - in String packageName, in Bundle options, in UserHandle user); - List<SoundProfile> getAvailableSoundProfiles(in Bundle options, in UserHandle user); - List<String> getSoundProfilePackageNames(in UserHandle user); - List<String> getSoundProfileAllowList(in UserHandle user); - void setSoundProfileAllowList(in List<String> packages, in UserHandle user); - List<SoundProfileHandle> getSoundProfileHandle(in String[] id, in UserHandle user); + in String packageName, in boolean includeParams, int userId); + List<SoundProfile> getAvailableSoundProfiles(in boolean includeParams, int userId); + List<String> getSoundProfilePackageNames(int userId); + List<String> getSoundProfileAllowList(int userId); + void setSoundProfileAllowList(in List<String> packages, int userId); + List<SoundProfileHandle> getSoundProfileHandle(in String[] id, int userId); void registerPictureProfileCallback(in IPictureProfileCallback cb); void registerSoundProfileCallback(in ISoundProfileCallback cb); void registerAmbientBacklightCallback(in IAmbientBacklightCallback cb); - List<ParameterCapability> getParameterCapabilities(in List<String> names, in UserHandle user); + List<ParameterCapability> getParameterCapabilities(in List<String> names, int userId); - boolean isSupported(in UserHandle user); - void setAutoPictureQualityEnabled(in boolean enabled, in UserHandle user); - boolean isAutoPictureQualityEnabled(in UserHandle user); - void setSuperResolutionEnabled(in boolean enabled, in UserHandle user); - boolean isSuperResolutionEnabled(in UserHandle user); - void setAutoSoundQualityEnabled(in boolean enabled, in UserHandle user); - boolean isAutoSoundQualityEnabled(in UserHandle user); + boolean isSupported(int userId); + void setAutoPictureQualityEnabled(in boolean enabled, int userId); + boolean isAutoPictureQualityEnabled(int userId); + void setSuperResolutionEnabled(in boolean enabled, int userId); + boolean isSuperResolutionEnabled(int userId); + void setAutoSoundQualityEnabled(in boolean enabled, int userId); + boolean isAutoSoundQualityEnabled(int userId); - void setAmbientBacklightSettings(in AmbientBacklightSettings settings, in UserHandle user); - void setAmbientBacklightEnabled(in boolean enabled, in UserHandle user); - boolean isAmbientBacklightEnabled(in UserHandle user); + void setAmbientBacklightSettings(in AmbientBacklightSettings settings, int userId); + void setAmbientBacklightEnabled(in boolean enabled, int userId); + boolean isAmbientBacklightEnabled(int userId); } diff --git a/media/java/android/media/quality/IPictureProfileCallback.aidl b/media/java/android/media/quality/aidl/android/media/quality/IPictureProfileCallback.aidl index eed77f695416..eed77f695416 100644 --- a/media/java/android/media/quality/IPictureProfileCallback.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/IPictureProfileCallback.aidl diff --git a/media/java/android/media/quality/ISoundProfileCallback.aidl b/media/java/android/media/quality/aidl/android/media/quality/ISoundProfileCallback.aidl index 3871fb212259..3871fb212259 100644 --- a/media/java/android/media/quality/ISoundProfileCallback.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/ISoundProfileCallback.aidl diff --git a/media/java/android/media/quality/ParameterCapability.aidl b/media/java/android/media/quality/aidl/android/media/quality/ParameterCapability.aidl index eb2ac97916f3..eb2ac97916f3 100644 --- a/media/java/android/media/quality/ParameterCapability.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/ParameterCapability.aidl diff --git a/media/java/android/media/quality/PictureProfile.aidl b/media/java/android/media/quality/aidl/android/media/quality/PictureProfile.aidl index 41d018b12f33..41d018b12f33 100644 --- a/media/java/android/media/quality/PictureProfile.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/PictureProfile.aidl diff --git a/media/java/android/media/quality/PictureProfileHandle.aidl b/media/java/android/media/quality/aidl/android/media/quality/PictureProfileHandle.aidl index 5d14631dbb73..5d14631dbb73 100644 --- a/media/java/android/media/quality/PictureProfileHandle.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/PictureProfileHandle.aidl diff --git a/media/java/android/media/quality/SoundProfile.aidl b/media/java/android/media/quality/aidl/android/media/quality/SoundProfile.aidl index e79fcaac97be..e79fcaac97be 100644 --- a/media/java/android/media/quality/SoundProfile.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/SoundProfile.aidl diff --git a/media/java/android/media/quality/SoundProfileHandle.aidl b/media/java/android/media/quality/aidl/android/media/quality/SoundProfileHandle.aidl index 6b8161c8cc43..ea26b19d84d7 100644 --- a/media/java/android/media/quality/SoundProfileHandle.aidl +++ b/media/java/android/media/quality/aidl/android/media/quality/SoundProfileHandle.aidl @@ -16,4 +16,7 @@ package android.media.quality; -parcelable SoundProfileHandle; +// TODO: add SoundProfileHandle.java +parcelable SoundProfileHandle { + long id; +} diff --git a/media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl b/media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl index cdf6e23f4b47..40848fe6f875 100644 --- a/media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl +++ b/media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl @@ -21,5 +21,5 @@ package android.media.tv.extension.scan; */ interface IHDPlusInfo { // Specifying a HDPlusInfo and start a network scan. - int setHDPlusInfo(String isBlindScanContinue, String isHDMode); + int setHDPlusInfo(boolean isBlindScanContinue, boolean isHDMode); } diff --git a/media/java/android/media/tv/extension/scan/IScanListener.aidl b/media/java/android/media/tv/extension/scan/IScanListener.aidl index 2c4807f97c58..79810a7b035d 100644 --- a/media/java/android/media/tv/extension/scan/IScanListener.aidl +++ b/media/java/android/media/tv/extension/scan/IScanListener.aidl @@ -27,7 +27,7 @@ oneway interface IScanListener { // notify the scan progress. void onScanProgress(String scanProgress, in Bundle scanProgressInfo); // notify the scan completion. - void onScanCompleted(int scanResult); + void onScanCompleted(int scanResult, in Bundle optionScanInfo); // notify that the temporaily held channel list is stored. void onStoreCompleted(int storeResult); } diff --git a/media/java/android/media/tv/extension/scan/IScanSession.aidl b/media/java/android/media/tv/extension/scan/IScanSession.aidl index d42eca1342b5..f53b09661ba6 100644 --- a/media/java/android/media/tv/extension/scan/IScanSession.aidl +++ b/media/java/android/media/tv/extension/scan/IScanSession.aidl @@ -24,7 +24,7 @@ import android.os.Bundle; interface IScanSession { // Start a service scan. int startScan(int broadcastType, String countryCode, String operator, in int[] frequency, - String scanType, String languageCode); + String scanType, String languageCode, in Bundle optionalScanParams); // Reset the scan information held in TIS. int resetScan(); // Cancel scan. diff --git a/media/java/android/media/tv/extension/servicedb/IServiceListEdit.aidl b/media/java/android/media/tv/extension/servicedb/IServiceListEdit.aidl index 1b1577ffc015..0e9ac5f04e5d 100644 --- a/media/java/android/media/tv/extension/servicedb/IServiceListEdit.aidl +++ b/media/java/android/media/tv/extension/servicedb/IServiceListEdit.aidl @@ -78,4 +78,11 @@ interface IServiceListEdit { int addPredefinedChannelList(String serviceListId, in Bundle[] predefinedListBundle); // Add predefined satellite info of Hotbird 13E in scan two satellite scene EU region. int addPredefinedSatInfo(String serviceListId, in Bundle predefinedSatInfoBundle); + + // Get the logo URI for a specific service - DVB-I only. + String getServiceLogoUri(int serviceRecordId); + // Get the installed service list information for a specific channel list id - DVB-I only. + Bundle getInstalledServiceListInfo(String channelListId); + // Get all installed service list information - DVB-I only. + Bundle[] getAllInstalledServiceListInfo(); } diff --git a/media/java/android/media/tv/extension/servicedb/IServiceListImportListener.aidl b/media/java/android/media/tv/extension/servicedb/IServiceListImportListener.aidl index abd8320df11d..fced45b11d3b 100644 --- a/media/java/android/media/tv/extension/servicedb/IServiceListImportListener.aidl +++ b/media/java/android/media/tv/extension/servicedb/IServiceListImportListener.aidl @@ -16,10 +16,12 @@ package android.media.tv.extension.servicedb; +import android.os.Bundle; + /** * @hide */ interface IServiceListImportListener { void onImported(int importResult); - void onPreloaded(int preloadResult); + void onPreloaded(int preloadResult, in Bundle serviceListInfo); }
\ No newline at end of file diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp index 1e6a7b7f2810..45b746d254e1 100644 --- a/native/android/performance_hint.cpp +++ b/native/android/performance_hint.cpp @@ -971,7 +971,7 @@ void FMQWrapper::writeBuffer<HalChannelMessageContents::workDuration>(hal::WorkD .timeStampNanos = (i == count - 1) ? now : message.timeStampNanos, .data = HalChannelMessageContents::make<HalChannelMessageContents::workDuration, hal::WorkDurationFixedV1>({ - .durationNanos = message.cpuDurationNanos, + .durationNanos = message.durationNanos, .workPeriodStartTimestampNanos = message.workPeriodStartTimestampNanos, .cpuDurationNanos = message.cpuDurationNanos, .gpuDurationNanos = message.gpuDurationNanos, diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java index e818a603c5b4..bf739620bc99 100644 --- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java +++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java @@ -138,8 +138,12 @@ public class IllustrationPreference extends Preference implements GroupSectionDi ImageView backgroundViewTablet = (ImageView) holder.findViewById(R.id.background_view_tablet); - backgroundView.setVisibility(mIsTablet ? View.GONE : View.VISIBLE); - backgroundViewTablet.setVisibility(mIsTablet ? View.VISIBLE : View.GONE); + if (backgroundView != null) { + backgroundView.setVisibility(mIsTablet ? View.GONE : View.VISIBLE); + } + if (backgroundViewTablet != null) { + backgroundViewTablet.setVisibility(mIsTablet ? View.VISIBLE : View.GONE); + } if (mIsTablet) { backgroundView = backgroundViewTablet; } @@ -183,7 +187,9 @@ public class IllustrationPreference extends Preference implements GroupSectionDi if (mLottieDynamicColor) { LottieColorUtils.applyDynamicColors(getContext(), illustrationView); } - LottieColorUtils.applyMaterialColor(getContext(), illustrationView); + if (SettingsThemeHelper.isExpressiveTheme(getContext())) { + LottieColorUtils.applyMaterialColor(getContext(), illustrationView); + } if (mOnBindListener != null) { mOnBindListener.onBind(illustrationView); @@ -443,6 +449,10 @@ public class IllustrationPreference extends Preference implements GroupSectionDi illustrationView.setMaxWidth((int) (restrictedMaxHeight * aspectRatio)); } + public boolean isAnimatable() { + return mIsAnimatable; + } + private void startAnimation(Drawable drawable) { if (!(drawable instanceof Animatable)) { return; diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java index 4421424c0e39..e59cc81d3ba8 100644 --- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java +++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java @@ -157,10 +157,6 @@ public class LottieColorUtils { /** Applies material colors. */ public static void applyMaterialColor(@NonNull Context context, @NonNull LottieAnimationView lottieAnimationView) { - if (!SettingsThemeHelper.isExpressiveTheme(context)) { - return; - } - for (String key : MATERIAL_COLOR_MAP.keySet()) { final int color = context.getColor(MATERIAL_COLOR_MAP.get(key)); lottieAnimationView.addValueCallback( diff --git a/packages/SettingsLib/RestrictedLockUtils/res/values/strings.xml b/packages/SettingsLib/RestrictedLockUtils/res/values/strings.xml index 75809730a514..2ffdc930ccea 100644 --- a/packages/SettingsLib/RestrictedLockUtils/res/values/strings.xml +++ b/packages/SettingsLib/RestrictedLockUtils/res/values/strings.xml @@ -21,8 +21,4 @@ <string name="enabled_by_admin">Enabled by admin</string> <!-- Summary for switch preference to denote it is switched off by an admin [CHAR LIMIT=50] --> <string name="disabled_by_admin">Disabled by admin</string> - <!-- Summary for switch preference to denote it is switched on by Advanced protection [CHAR LIMIT=50] --> - <string name="enabled_by_advanced_protection">Enabled by Advanced Protection</string> - <!-- Summary for switch preference to denote it is switched off by Advanced protection [CHAR LIMIT=50] --> - <string name="disabled_by_advanced_protection">Disabled by Advanced Protection</string> </resources> diff --git a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java index 0f6a2a082e0c..1170f1e7c695 100644 --- a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java +++ b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java @@ -19,6 +19,7 @@ package com.android.settingslib.widget; import android.content.Context; import android.util.AttributeSet; import android.view.View; +import android.view.accessibility.AccessibilityEvent; import android.widget.AdapterView; import android.widget.Spinner; @@ -110,7 +111,6 @@ public class SettingsSpinnerPreference extends Preference notifyChanged(); } - @Override public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); @@ -119,6 +119,18 @@ public class SettingsSpinnerPreference extends Preference spinner.setSelection(mPosition); spinner.setOnItemSelectedListener(mOnSelectedListener); spinner.setLongClickable(false); + spinner.setAccessibilityDelegate( + new View.AccessibilityDelegate() { + @Override + public void sendAccessibilityEvent(View host, int eventType) { + if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) { + // Ignore the INTERRUPT events TYPE_VIEW_SELECTED or Talkback will speak + // for it while fragment updating. + return; + } + super.sendAccessibilityEvent(host, eventType); + } + }); if (mShouldPerformClick) { mShouldPerformClick = false; // To show dropdown view. diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt index a8483308556d..6f37f0cc5799 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt @@ -46,7 +46,7 @@ internal data class BlockedByAdminImpl( ) : BlockedByAdmin { override fun getSummary(checked: Boolean?) = when (checked) { true -> enterpriseRepository.getAdminSummaryString( - advancedProtectionStringId = R.string.enabled_by_advanced_protection, + advancedProtectionStringId = com.android.settingslib.R.string.enabled, updatableStringId = Settings.ENABLED_BY_ADMIN_SWITCH_SUMMARY, resId = R.string.enabled_by_admin, enforcedAdmin = enforcedAdmin, @@ -54,7 +54,7 @@ internal data class BlockedByAdminImpl( ) false -> enterpriseRepository.getAdminSummaryString( - advancedProtectionStringId = R.string.disabled_by_advanced_protection, + advancedProtectionStringId = com.android.settingslib.R.string.disabled, updatableStringId = Settings.DISABLED_BY_ADMIN_SWITCH_SUMMARY, resId = R.string.disabled_by_admin, enforcedAdmin = enforcedAdmin, diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemInteger.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemInteger.kt new file mode 100644 index 000000000000..db7a640be893 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemInteger.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.spaprivileged.settingsprovider + +import android.content.ContentResolver +import android.content.Context +import android.provider.Settings +import com.android.settingslib.spaprivileged.database.contentChangeFlow +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +fun Context.settingsSystemInteger( + name: String, + defaultValue: Int +): ReadWriteProperty<Any?, Int> = SettingsSystemIntegerDelegate(this, name, defaultValue) + +fun Context.settingsSystemIntegerFlow(name: String, defaultValue: Int): Flow<Int> { + val value by settingsSystemInteger(name, defaultValue) + return contentChangeFlow(Settings.System.getUriFor(name)) + .map { value } + .distinctUntilChanged() + .conflate() + .flowOn(Dispatchers.IO) +} + +private class SettingsSystemIntegerDelegate( + context: Context, + private val name: String, + private val defaultValue: Int, +) : ReadWriteProperty<Any?, Int> { + + private val contentResolver: ContentResolver = context.contentResolver + + override fun getValue(thisRef: Any?, property: KProperty<*>): Int = + Settings.System.getInt(contentResolver, name, defaultValue) + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { + Settings.System.putInt(contentResolver, name, value) + } +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/robotests/Android.bp b/packages/SettingsLib/SpaPrivileged/tests/robotests/Android.bp new file mode 100644 index 000000000000..e3faf73a69fb --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/robotests/Android.bp @@ -0,0 +1,59 @@ +// +// Copyright (C) 2025 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +package { + default_applicable_licenses: ["frameworks_base_license"], +} + +android_app { + name: "SpaPrivilegedRoboTestStub", + defaults: [ + "SpaPrivilegedLib-defaults", + ], + platform_apis: true, + certificate: "platform", + privileged: true, +} + +android_robolectric_test { + name: "SpaPrivilegedRoboTests", + srcs: [ + ":SpaPrivilegedLib_srcs", + "src/**/*.java", + "src/**/*.kt", + ], + + defaults: [ + "SpaPrivilegedLib-defaults", + ], + + static_libs: [ + "SpaLibTestUtils", + "androidx.test.ext.junit", + "androidx.test.runner", + ], + + java_resource_dirs: [ + "config", + ], + + instrumentation_for: "SpaPrivilegedRoboTestStub", + + test_options: { + timeout: 36000, + }, + + strict_mode: false, +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/robotests/AndroidManifest.xml b/packages/SettingsLib/SpaPrivileged/tests/robotests/AndroidManifest.xml new file mode 100644 index 000000000000..113852d74f69 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/robotests/AndroidManifest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + coreApp="true" + package="com.android.settingslib.spaprivileged.settingsprovider"> + + <uses-permission android:name="android.permission.WRITE_SETTINGS" /> + + <application/> +</manifest>
\ No newline at end of file diff --git a/packages/SettingsLib/SpaPrivileged/tests/robotests/config/robolectric.properties b/packages/SettingsLib/SpaPrivileged/tests/robotests/config/robolectric.properties new file mode 100644 index 000000000000..95a24bde00f6 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/robotests/config/robolectric.properties @@ -0,0 +1,16 @@ +/* +* Copyright (C) 2025 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +sdk=NEWEST_SDK
\ No newline at end of file diff --git a/packages/SettingsLib/SpaPrivileged/tests/robotests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemIntegerTest.kt b/packages/SettingsLib/SpaPrivileged/tests/robotests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemIntegerTest.kt new file mode 100644 index 000000000000..67e4180a624b --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/robotests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSystemIntegerTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.spaprivileged.settingsprovider + +import android.content.Context +import android.provider.Settings + +import androidx.test.core.app.ApplicationProvider + +import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull +import com.android.settingslib.spa.testutils.toListWithTimeout +import com.google.common.truth.Truth.assertThat + +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking + +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class SettingsSystemIntegerTest { + private val context: Context = ApplicationProvider.getApplicationContext() + + @Before + fun setUp() { + Settings.System.putString(context.contentResolver, TEST_NAME, null) + } + + @Test + fun setIntValue_returnSameValueByDelegate() { + val settingValue = 250 + + Settings.System.putInt(context.contentResolver, TEST_NAME, settingValue) + + val value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + + assertThat(value).isEqualTo(settingValue) + } + + @Test + fun setZero_returnZeroByDelegate() { + val settingValue = 0 + Settings.System.putInt(context.contentResolver, TEST_NAME, settingValue) + + val value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + + assertThat(value).isEqualTo(settingValue) + } + + @Test + fun setValueByDelegate_getValueFromSettings() { + val settingsValue = 5 + var value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + + value = settingsValue + + assertThat(Settings.System.getInt(context.contentResolver, TEST_NAME, TEST_SETTING_DEFAULT_VALUE)).isEqualTo(settingsValue) + } + + @Test + fun setZeroByDelegate_getZeroFromSettings() { + val settingValue = 0 + var value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + + value = settingValue + + assertThat(Settings.System.getInt(context.contentResolver, TEST_NAME, TEST_SETTING_DEFAULT_VALUE)).isEqualTo(settingValue) + } + + @Test + fun setValueByDelegate_returnValueFromsettingsSystemIntegerFlow() = runBlocking<Unit> { + val settingValue = 7 + var value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + value = settingValue + + val flow = context.settingsSystemIntegerFlow(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + + assertThat(flow.firstWithTimeoutOrNull()).isEqualTo(settingValue) + } + + @Test + fun setValueByDelegateTwice_collectAfterValueChanged_onlyKeepLatest() = runBlocking<Unit> { + val firstSettingValue = 5 + val secondSettingValue = 10 + + var value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + value = firstSettingValue + + val flow = context.settingsSystemIntegerFlow(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + value = secondSettingValue + + assertThat(flow.firstWithTimeoutOrNull()).isEqualTo(value) + } + + @Test + fun settingsSystemIntegerFlow_collectBeforeValueChanged_getBoth() = runBlocking<Unit> { + val firstSettingValue = 12 + val secondSettingValue = 17 + val delay_ms = 100L + + var value by context.settingsSystemInteger(TEST_NAME, TEST_SETTING_DEFAULT_VALUE) + value = firstSettingValue + + + val listDeferred = async { + context.settingsSystemIntegerFlow(TEST_NAME, TEST_SETTING_DEFAULT_VALUE).toListWithTimeout() + } + + delay(delay_ms) + value = secondSettingValue + + assertThat(listDeferred.await()) + .containsAtLeast(firstSettingValue, secondSettingValue).inOrder() + } + + private companion object { + const val TEST_NAME = "test_system_integer_delegate" + const val TEST_SETTING_DEFAULT_VALUE = -1 + } +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedModeTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedModeTest.kt index f3245c9085e7..189bf363420c 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedModeTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedModeTest.kt @@ -77,8 +77,8 @@ class RestrictedModeTest { if (RestrictedLockUtilsInternal.isPolicyEnforcedByAdvancedProtection(context, RESTRICTION, userId)) { return when (advancedProtectionStringId) { - R.string.enabled_by_advanced_protection -> ENABLED_BY_ADVANCED_PROTECTION - R.string.disabled_by_advanced_protection -> DISABLED_BY_ADVANCED_PROTECTION + com.android.settingslib.R.string.enabled -> ENABLED + com.android.settingslib.R.string.disabled -> DISABLED else -> "" } } @@ -129,7 +129,7 @@ class RestrictedModeTest { val summary = blockedByAdmin.getSummary(true) - assertThat(summary).isEqualTo(ENABLED_BY_ADVANCED_PROTECTION) + assertThat(summary).isEqualTo(ENABLED) } @RequiresFlagsEnabled(Flags.FLAG_AAPM_API) @@ -148,7 +148,7 @@ class RestrictedModeTest { val summary = blockedByAdmin.getSummary(false) - assertThat(summary).isEqualTo(DISABLED_BY_ADVANCED_PROTECTION) + assertThat(summary).isEqualTo(DISABLED) } @RequiresFlagsEnabled(Flags.FLAG_AAPM_API) @@ -202,7 +202,7 @@ class RestrictedModeTest { const val ENABLED_BY_ADMIN = "Enabled by admin" const val DISABLED_BY_ADMIN = "Disabled by admin" - const val ENABLED_BY_ADVANCED_PROTECTION = "Enabled by advanced protection" - const val DISABLED_BY_ADVANCED_PROTECTION = "Disabled by advanced protection" + const val ENABLED = "Enabled" + const val DISABLED = "Disabled" } } diff --git a/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt index 79085af63c6d..308b285e0cfc 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt @@ -175,13 +175,7 @@ class TogglePermissionAppListPageTest { val summary = getSummary(listModel) - assertThat(summary) - .isEqualTo( - context.getString( - com.android.settingslib.widget.restricted.R.string - .disabled_by_advanced_protection - ) - ) + assertThat(summary).isEqualTo(context.getString(com.android.settingslib.R.string.disabled)) } @RequiresFlagsEnabled(Flags.FLAG_AAPM_API) diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 91ec83690722..03cb1ffbdef1 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1263,6 +1263,8 @@ <!-- [CHAR LIMIT=25] Manage applications, text telling using an application is disabled. --> <string name="disabled">Disabled</string> + <!-- Summary for a settings preference indicating it is enabled [CHAR LIMIT = 30] --> + <string name="enabled">Enabled</string> <!-- Summary of app trusted to install apps [CHAR LIMIT=45] --> <string name="external_source_trusted">Allowed</string> <!-- Summary of app not trusted to install apps [CHAR LIMIT=45] --> diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java index 1044750bae25..9d979019be58 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java @@ -120,7 +120,7 @@ public class RestrictedPreferenceHelper { final TextView summaryView = (TextView) holder.findViewById(android.R.id.summary); if (summaryView != null) { final CharSequence disabledText = getDisabledByAdminSummaryString(); - if (mDisabledByAdmin) { + if (mDisabledByAdmin && disabledText != null) { summaryView.setText(disabledText); } else if (mDisabledByEcm) { summaryView.setText(getEcmTextResId()); @@ -132,10 +132,10 @@ public class RestrictedPreferenceHelper { } } - private String getDisabledByAdminSummaryString() { + private @Nullable String getDisabledByAdminSummaryString() { if (isRestrictionEnforcedByAdvancedProtection()) { - return mContext.getString(com.android.settingslib.widget.restricted - .R.string.disabled_by_advanced_protection); + // Advanced Protection doesn't set the summary string, it keeps the current summary. + return null; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { return mContext.getSystemService(DevicePolicyManager.class).getResources().getString( @@ -321,7 +321,10 @@ public class RestrictedPreferenceHelper { } if (android.security.Flags.aapmApi() && !isEnabled && mDisabledByAdmin) { - mPreference.setSummary(getDisabledByAdminSummaryString()); + String summary = getDisabledByAdminSummaryString(); + if (summary != null) { + mPreference.setSummary(summary); + } } if (!isEnabled && mDisabledByEcm) { diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java index a5fa6a854e97..67c4207cb8be 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java @@ -36,6 +36,7 @@ import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceViewHolder; @@ -141,7 +142,7 @@ public class RestrictedSwitchPreference extends SwitchPreferenceCompat implement final TextView additionalSummaryView = (TextView) holder.findViewById( R.id.additional_summary); if (additionalSummaryView != null) { - if (isDisabledByAdmin()) { + if (isDisabledByAdmin() && switchSummary != null) { additionalSummaryView.setText(switchSummary); additionalSummaryView.setVisibility(View.VISIBLE); } else { @@ -151,7 +152,7 @@ public class RestrictedSwitchPreference extends SwitchPreferenceCompat implement } else { final TextView summaryView = (TextView) holder.findViewById(android.R.id.summary); if (summaryView != null) { - if (isDisabledByAdmin()) { + if (isDisabledByAdmin() && switchSummary != null) { summaryView.setText(switchSummary); summaryView.setVisibility(View.VISIBLE); } @@ -171,14 +172,10 @@ public class RestrictedSwitchPreference extends SwitchPreferenceCompat implement () -> context.getString(resId)); } - private String getRestrictedSwitchSummary() { + private @Nullable String getRestrictedSwitchSummary() { if (mHelper.isRestrictionEnforcedByAdvancedProtection()) { - final int apmResId = isChecked() - ? com.android.settingslib.widget.restricted.R.string - .enabled_by_advanced_protection - : com.android.settingslib.widget.restricted.R.string - .disabled_by_advanced_protection; - return getContext().getString(apmResId); + // Advanced Protection doesn't set the summary string, it keeps the current summary. + return null; } return isChecked() diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java index dbbbd5bf8089..f9769fa61e0d 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/RestrictedPreferenceHelperTest.java @@ -23,6 +23,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.atMostOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -130,7 +131,7 @@ public class RestrictedPreferenceHelperTest { @RequiresFlagsEnabled(android.security.Flags.FLAG_AAPM_API) @Test - public void bindPreference_disabled_byAdvancedProtection_shouldDisplayDisabledSummary() { + public void bindPreference_disabled_byAdvancedProtection_shouldKeepExistingSummary() { final TextView summaryView = mock(TextView.class, RETURNS_DEEP_STUBS); final String userRestriction = UserManager.DISALLOW_UNINSTALL_APPS; final RestrictedLockUtils.EnforcedAdmin enforcedAdmin = new RestrictedLockUtils @@ -143,16 +144,14 @@ public class RestrictedPreferenceHelperTest { .thenReturn(summaryView); when(mDevicePolicyManager.getEnforcingAdmin(UserHandle.myUserId(), userRestriction)) .thenReturn(advancedProtectionEnforcingAdmin); - when(mContext.getString( - com.android.settingslib.widget.restricted.R.string.disabled_by_advanced_protection)) - .thenReturn("advanced_protection"); + summaryView.setText("existing summary"); mHelper.useAdminDisabledSummary(true); mHelper.setDisabledByAdmin(enforcedAdmin); mHelper.onBindViewHolder(mViewHolder); - verify(summaryView).setText("advanced_protection"); - verify(summaryView, never()).setVisibility(View.GONE); + verify(summaryView, atMostOnce()).setText(any()); // To set it to existing summary + verify(summaryView, never()).setVisibility(View.VISIBLE); } @RequiresFlagsEnabled(android.security.Flags.FLAG_AAPM_API) diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java index 3d16d6f1cd56..c387d48b33da 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java @@ -507,4 +507,25 @@ public class MediaDeviceTest { assertThat(mPhoneMediaDevice.getSelectionBehavior()).isEqualTo( SELECTION_BEHAVIOR_TRANSFER); } + + @Test + public void getSelectionBehavior_withRouteListingPreferenceItem_returnPreferenceBehavior() { + mItem = + new RouteListingPreference.Item.Builder(DEVICE_ADDRESS_1) + .setSelectionBehavior(SELECTION_BEHAVIOR_GO_TO_APP) + .build(); + MediaDevice castMediaDevice = new ComplexMediaDevice(mContext, mRouteInfo1, mItem); + + assertThat(castMediaDevice.hasRouteListingPreferenceItem()).isTrue(); + assertThat(castMediaDevice.getSelectionBehavior()).isEqualTo(SELECTION_BEHAVIOR_GO_TO_APP); + } + + @Test + public void getSelectionBehavior_withoutRouteListingPreferenceItem_returnTransfer() { + MediaDevice castMediaDevice = + new ComplexMediaDevice(mContext, mRouteInfo1, /* item= */ null); + + assertThat(castMediaDevice.hasRouteListingPreferenceItem()).isFalse(); + assertThat(castMediaDevice.getSelectionBehavior()).isEqualTo(SELECTION_BEHAVIOR_TRANSFER); + } } diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index dafcc729b8f1..d929b0de391a 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -187,6 +187,9 @@ <!-- Default state of tap to wake --> <bool name="def_double_tap_to_wake">true</bool> + <!-- Default setting for double tap to sleep (Settings.Secure.DOUBLE_TAP_TO_SLEEP) --> + <bool name="def_double_tap_to_sleep">false</bool> + <!-- Default for Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT --> <string name="def_nfc_payment_component"></string> diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index c0105298899b..a2291123e192 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -85,6 +85,7 @@ public class SecureSettings { Settings.Secure.MOUNT_UMS_PROMPT, Settings.Secure.MOUNT_UMS_NOTIFY_ENABLED, Settings.Secure.DOUBLE_TAP_TO_WAKE, + Settings.Secure.DOUBLE_TAP_TO_SLEEP, Settings.Secure.WAKE_GESTURE_ENABLED, Settings.Secure.LONG_PRESS_TIMEOUT, Settings.Secure.KEY_REPEAT_ENABLED, @@ -229,6 +230,7 @@ public class SecureSettings { Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED, Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, Settings.Secure.ACCESSIBILITY_MAGNIFICATION_ALWAYS_ON_ENABLED, + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE, Settings.Secure.ACCESSIBILITY_MAGNIFICATION_JOYSTICK_ENABLED, Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED, Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ENABLED, @@ -266,6 +268,7 @@ public class SecureSettings { Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED, Settings.Secure.HUB_MODE_TUTORIAL_STATE, Settings.Secure.GLANCEABLE_HUB_ENABLED, + Settings.Secure.WHEN_TO_START_GLANCEABLE_HUB, Settings.Secure.STYLUS_BUTTONS_ENABLED, Settings.Secure.STYLUS_HANDWRITING_ENABLED, Settings.Secure.DEFAULT_NOTE_TASK_PROFILE, @@ -293,5 +296,7 @@ public class SecureSettings { Settings.Secure.FINGERPRINT_APP_ENABLED, Settings.Secure.FINGERPRINT_KEYGUARD_ENABLED, Settings.Secure.DUAL_SHADE, + Settings.Secure.BROWSER_CONTENT_FILTERS_ENABLED, + Settings.Secure.SEARCH_CONTENT_FILTERS_ENABLED, }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 0ffdf53f2036..a4325344709a 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -131,6 +131,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.MOUNT_UMS_PROMPT, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.MOUNT_UMS_NOTIFY_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.DOUBLE_TAP_TO_WAKE, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.DOUBLE_TAP_TO_SLEEP, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.WAKE_GESTURE_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.LONG_PRESS_TIMEOUT, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.KEY_REPEAT_ENABLED, BOOLEAN_VALIDATOR); @@ -323,6 +324,10 @@ public class SecureSettingsValidators { Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL)); VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_FOLLOW_TYPING_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_ALWAYS_ON_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE, + new InclusiveIntegerRangeValidator( + Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS, + Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE)); VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_JOYSTICK_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put( Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED, @@ -429,6 +434,8 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.DND_CONFIGS_MIGRATED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.HUB_MODE_TUTORIAL_STATE, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.GLANCEABLE_HUB_ENABLED, new InclusiveIntegerRangeValidator(0, 1)); + VALIDATORS.put(Secure.WHEN_TO_START_GLANCEABLE_HUB, + new InclusiveIntegerRangeValidator(0, 3)); VALIDATORS.put(Secure.STYLUS_BUTTONS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.STYLUS_HANDWRITING_ENABLED, new DiscreteValueValidator(new String[] {"-1", "0", "1"})); @@ -461,5 +468,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.FINGERPRINT_APP_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.FINGERPRINT_KEYGUARD_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.DUAL_SHADE, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.BROWSER_CONTENT_FILTERS_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.SEARCH_CONTENT_FILTERS_ENABLED, BOOLEAN_VALIDATOR); } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index e07832eea65e..57facdaa388c 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -1881,6 +1881,10 @@ class SettingsProtoDumpUtil { SecureSettingsProto.Accessibility .ACCESSIBILITY_MAGNIFICATION_JOYSTICK_ENABLED); dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE, + SecureSettingsProto.Accessibility + .ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE); + dumpSetting(s, p, Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED, SecureSettingsProto.Accessibility .ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 4a225bdbd7e5..65ede9d804d0 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -4080,7 +4080,7 @@ public class SettingsProvider extends ContentProvider { @VisibleForTesting final class UpgradeController { - private static final int SETTINGS_VERSION = 227; + private static final int SETTINGS_VERSION = 228; private final int mUserId; @@ -6319,6 +6319,23 @@ public class SettingsProvider extends ContentProvider { currentVersion = 227; } + // Version 227: Add default value for DOUBLE_TAP_TO_SLEEP. + if (currentVersion == 227) { + final SettingsState secureSettings = getSecureSettingsLocked(userId); + final Setting doubleTapToSleep = secureSettings.getSettingLocked( + Settings.Secure.DOUBLE_TAP_TO_SLEEP); + if (doubleTapToSleep.isNull()) { + secureSettings.insertSettingOverrideableByRestoreLocked( + Settings.Secure.DOUBLE_TAP_TO_SLEEP, + getContext().getResources().getBoolean( + R.bool.def_double_tap_to_sleep) ? "1" : "0", + null /* tag */, + true /* makeDefault */, + SettingsState.SYSTEM_PACKAGE_NAME); + } + currentVersion = 228; + } + // vXXX: Add new settings above this point. if (currentVersion != newVersion) { diff --git a/packages/SystemUI/aconfig/predictive_back.aconfig b/packages/SystemUI/aconfig/predictive_back.aconfig index 89a0d895a17c..dd9a8b19f498 100644 --- a/packages/SystemUI/aconfig/predictive_back.aconfig +++ b/packages/SystemUI/aconfig/predictive_back.aconfig @@ -9,8 +9,11 @@ flag { } flag { - name: "predictive_back_delay_transition" + name: "predictive_back_delay_wm_transition" namespace: "systemui" description: "Slightly delays the back transition start" bug: "301195601" + metadata { + purpose: PURPOSE_BUGFIX + } } diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 9db4346a08b7..9dd49d6b1015 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -292,6 +292,17 @@ flag { } flag { + name: "notification_row_accessibility_expanded" + namespace: "systemui" + description: "Prepare ExpandableNotificationRow for new A11y expansion APIs." + bug: "380027122" + metadata { + purpose: PURPOSE_BUGFIX + } +} + + +flag { name: "scene_container" namespace: "systemui" description: "Enables the scene container framework go/flexiglass." @@ -1390,6 +1401,16 @@ flag { } flag { + name: "media_controls_device_manager_background_execution" + namespace: "systemui" + description: "Sends some instances creation to background thread" + bug: "400200474" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "output_switcher_redesign" namespace: "systemui" description: "Enables visual update for Media Output Switcher" @@ -1832,6 +1853,16 @@ flag { } flag { + name: "disable_shade_visible_with_blur" + namespace: "systemui" + description: "Removes the check for a blur radius when determining shade window visibility" + bug: "356804470" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "notification_row_transparency" namespace: "systemui" description: "Enables transparency on the Notification Shade." @@ -2110,3 +2141,10 @@ flag { description: "Enables moving the launching window on top of the origin window in the Animation library." bug: "390422470" } + +flag { + name: "status_bar_chips_return_animations" + namespace: "systemui" + description: "Enables return animations for status bar chips" + bug: "202516970" +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt index 9a746870c6ff..e07d7b337ba2 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt @@ -16,17 +16,18 @@ package com.android.systemui.animation +import kotlin.text.buildString + class FontVariationUtils { private var mWeight = -1 private var mWidth = -1 private var mOpticalSize = -1 private var mRoundness = -1 - private var isUpdated = false + private var mCurrentFVar = "" /* * generate fontVariationSettings string, used for key in typefaceCache in TextAnimator * the order of axes should align to the order of parameters - * if every axis remains unchanged, return "" */ fun updateFontVariation( weight: Int = -1, @@ -34,15 +35,17 @@ class FontVariationUtils { opticalSize: Int = -1, roundness: Int = -1, ): String { - isUpdated = false + var isUpdated = false if (weight >= 0 && mWeight != weight) { isUpdated = true mWeight = weight } + if (width >= 0 && mWidth != width) { isUpdated = true mWidth = width } + if (opticalSize >= 0 && mOpticalSize != opticalSize) { isUpdated = true mOpticalSize = opticalSize @@ -52,23 +55,32 @@ class FontVariationUtils { isUpdated = true mRoundness = roundness } - var resultString = "" - if (mWeight >= 0) { - resultString += "'${GSFAxes.WEIGHT.tag}' $mWeight" - } - if (mWidth >= 0) { - resultString += - (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.WIDTH.tag}' $mWidth" - } - if (mOpticalSize >= 0) { - resultString += - (if (resultString.isBlank()) "" else ", ") + - "'${GSFAxes.OPTICAL_SIZE.tag}' $mOpticalSize" - } - if (mRoundness >= 0) { - resultString += - (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.ROUND.tag}' $mRoundness" + + if (!isUpdated) { + return mCurrentFVar } - return if (isUpdated) resultString else "" + + return buildString { + if (mWeight >= 0) { + if (!isBlank()) append(", ") + append("'${GSFAxes.WEIGHT.tag}' $mWeight") + } + + if (mWidth >= 0) { + if (!isBlank()) append(", ") + append("'${GSFAxes.WIDTH.tag}' $mWidth") + } + + if (mOpticalSize >= 0) { + if (!isBlank()) append(", ") + append("'${GSFAxes.OPTICAL_SIZE.tag}' $mOpticalSize") + } + + if (mRoundness >= 0) { + if (!isBlank()) append(", ") + append("'${GSFAxes.ROUND.tag}' $mRoundness") + } + } + .also { mCurrentFVar = it } } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt index 96feeedb8793..e734dd26eb15 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt @@ -25,6 +25,7 @@ data class AxisDefinition( ) object GSFAxes { + @JvmStatic val WEIGHT = AxisDefinition( tag = "wght", diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt index 5b073e49192a..4a39cff388a9 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt @@ -39,6 +39,7 @@ interface TypefaceVariantCache { fun getTypefaceForVariant(fvar: String?): Typeface? companion object { + @JvmStatic fun createVariantTypeface(baseTypeface: Typeface, fVar: String?): Typeface { if (fVar.isNullOrEmpty()) { return baseTypeface diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt index d08d859ec0d7..fc4d53af4b53 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt @@ -22,7 +22,6 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.ContentDrawScope @@ -32,7 +31,6 @@ import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.graphics.layer.drawLayer import androidx.compose.ui.layout.LayoutCoordinates -import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.modifier.ModifierLocalModifierNode import androidx.compose.ui.node.DrawModifierNode @@ -50,11 +48,7 @@ import androidx.compose.ui.util.fastForEach * The elements redirected to this container will be drawn above the content of this composable. */ fun Modifier.container(state: ContainerState): Modifier { - return onPlaced { state.lastOffsetInWindow = it.positionInWindow() } - .drawWithContent { - drawContent() - state.drawInOverlay(this) - } + return this then ContainerElement(state) } /** @@ -105,6 +99,30 @@ internal interface LayerRenderer { fun drawInOverlay(drawScope: DrawScope) } +private data class ContainerElement(private val state: ContainerState) : + ModifierNodeElement<ContainerNode>() { + override fun create(): ContainerNode { + return ContainerNode(state) + } + + override fun update(node: ContainerNode) { + node.state = state + } +} + +/** A node implementing [container] that can be delegated to. */ +class ContainerNode(var state: ContainerState) : + Modifier.Node(), LayoutAwareModifierNode, DrawModifierNode { + override fun onPlaced(coordinates: LayoutCoordinates) { + state.lastOffsetInWindow = coordinates.positionInWindow() + } + + override fun ContentDrawScope.draw() { + drawContent() + state.drawInOverlay(this) + } +} + private data class DrawInContainerElement( var state: ContainerState, var enabled: () -> Boolean, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index 061fdd99eb1b..0a711487ccb1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -356,7 +356,8 @@ private fun ContentScope.QuickSettingsScene( modifier = Modifier.padding(horizontal = 16.dp), ) } - else -> CollapsedShadeHeader(viewModel = headerViewModel) + else -> + CollapsedShadeHeader(viewModel = headerViewModel, isSplitShade = false) } Spacer(modifier = Modifier.height(16.dp)) // This view has its own horizontal padding diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt index a0216268308c..8f0fb20cef36 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt @@ -52,6 +52,7 @@ import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.modifiers.thenIf import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer +import com.android.systemui.brightness.ui.compose.ContainerColors import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.rememberViewModel @@ -257,15 +258,18 @@ fun ContentScope.QuickSettingsLayout( modifier = Modifier.padding(horizontal = QuickSettingsShade.Dimensions.Padding), ) - BrightnessSliderContainer( - viewModel = viewModel.brightnessSliderViewModel, - containerColor = OverlayShade.Colors.PanelBackground, - modifier = - Modifier.systemGestureExclusionInShade( - enabled = { layoutState.transitionState is TransitionState.Idle } - ) - .fillMaxWidth(), - ) + Box( + Modifier.systemGestureExclusionInShade( + enabled = { layoutState.transitionState is TransitionState.Idle } + ) + ) { + BrightnessSliderContainer( + viewModel = viewModel.brightnessSliderViewModel, + containerColors = + ContainerColors.singleColor(OverlayShade.Colors.PanelBackground), + modifier = Modifier.fillMaxWidth(), + ) + } Box { GridAnchor() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt index 23baeacd76ec..86c8fc34a63c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt @@ -127,6 +127,7 @@ object ShadeHeader { @Composable fun ContentScope.CollapsedShadeHeader( viewModel: ShadeHeaderViewModel, + isSplitShade: Boolean, modifier: Modifier = Modifier, ) { val cutoutLocation = LocalDisplayCutout.current.location @@ -141,8 +142,6 @@ fun ContentScope.CollapsedShadeHeader( } } - val isShadeLayoutWide = viewModel.isShadeLayoutWide - val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen. @@ -154,7 +153,7 @@ fun ContentScope.CollapsedShadeHeader( horizontalArrangement = Arrangement.spacedBy(5.dp), modifier = Modifier.padding(horizontal = horizontalPadding), ) { - Clock(scale = 1f, onClick = viewModel::onClockClicked) + Clock(onClick = viewModel::onClockClicked) VariableDayDate( longerDateText = viewModel.longerDateText, shorterDateText = viewModel.shorterDateText, @@ -184,11 +183,11 @@ fun ContentScope.CollapsedShadeHeader( Modifier.element(ShadeHeader.Elements.CollapsedContentEnd) .padding(horizontal = horizontalPadding), ) { - if (isShadeLayoutWide) { + if (isSplitShade) { ShadeCarrierGroup(viewModel = viewModel) } SystemIconChip( - onClick = viewModel::onSystemIconChipClicked.takeIf { isShadeLayoutWide } + onClick = viewModel::onSystemIconChipClicked.takeIf { isSplitShade } ) { StatusIcons( viewModel = viewModel, @@ -233,13 +232,11 @@ fun ContentScope.ExpandedShadeHeader( .defaultMinSize(minHeight = ShadeHeader.Dimensions.ExpandedHeight), ) { Box(modifier = Modifier.fillMaxWidth()) { - Box { - Clock( - scale = 2.57f, - onClick = viewModel::onClockClicked, - modifier = Modifier.align(Alignment.CenterStart), - ) - } + Clock( + onClick = viewModel::onClockClicked, + modifier = Modifier.align(Alignment.CenterStart), + scale = 2.57f, + ) Box( modifier = Modifier.element(ShadeHeader.Elements.ShadeCarrierGroup).fillMaxWidth() @@ -291,8 +288,6 @@ fun ContentScope.OverlayShadeHeader( val horizontalPadding = max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding) - val isShadeLayoutWide = viewModel.isShadeLayoutWide - val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen. @@ -301,16 +296,15 @@ fun ContentScope.OverlayShadeHeader( startContent = { Row( verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(5.dp), modifier = Modifier.padding(horizontal = horizontalPadding), ) { val chipHighlight = viewModel.notificationsChipHighlight - if (isShadeLayoutWide) { + if (viewModel.showClock) { Clock( - scale = 1f, onClick = viewModel::onClockClicked, modifier = Modifier.padding(horizontal = 4.dp), ) - Spacer(modifier = Modifier.width(5.dp)) } NotificationsChip( onClick = viewModel::onNotificationIconChipClicked, @@ -437,7 +431,11 @@ private fun CutoutAwareShadeHeader( } @Composable -private fun ContentScope.Clock(scale: Float, onClick: () -> Unit, modifier: Modifier = Modifier) { +private fun ContentScope.Clock( + onClick: () -> Unit, + modifier: Modifier = Modifier, + scale: Float = 1f, +) { val layoutDirection = LocalLayoutDirection.current ElementWithValues(key = ShadeHeader.Elements.Clock, modifier = modifier) { 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 5040490da8f6..885d34fb95c9 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 @@ -56,11 +56,11 @@ import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey @@ -68,6 +68,7 @@ import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.animateContentDpAsState +import com.android.compose.animation.scene.animateContentFloatAsState import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.modifiers.padding @@ -223,9 +224,6 @@ private fun ContentScope.ShadeScene( viewModel = viewModel, headerViewModel = headerViewModel, notificationsPlaceholderViewModel = notificationsPlaceholderViewModel, - createTintedIconManager = createTintedIconManager, - createBatteryMeterViewController = createBatteryMeterViewController, - statusBarIconController = statusBarIconController, mediaCarouselController = mediaCarouselController, mediaHost = qqsMediaHost, modifier = modifier, @@ -253,9 +251,6 @@ private fun ContentScope.SingleShade( viewModel: ShadeSceneContentViewModel, headerViewModel: ShadeHeaderViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, - createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, - createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, - statusBarIconController: StatusBarIconController, mediaCarouselController: MediaCarouselController, mediaHost: MediaHost, modifier: Modifier = Modifier, @@ -340,6 +335,7 @@ private fun ContentScope.SingleShade( content = { CollapsedShadeHeader( viewModel = headerViewModel, + isSplitShade = false, modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader), ) @@ -434,15 +430,13 @@ private fun ContentScope.SplitShade( val footerActionsViewModel = remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) } val tileSquishiness by - animateSceneFloatAsState( + animateContentFloatAsState( value = 1f, key = QuickSettings.SharedValues.TilesSquishiness, canOverflow = false, ) val unfoldTranslationXForStartSide by viewModel.unfoldTranslationX(isOnStartSide = true).collectAsStateWithLifecycle(0f) - val unfoldTranslationXForEndSide by - viewModel.unfoldTranslationX(isOnStartSide = false).collectAsStateWithLifecycle(0f) val notificationStackPadding = dimensionResource(id = R.dimen.notification_side_paddings) val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() @@ -512,6 +506,7 @@ private fun ContentScope.SplitShade( Column(modifier = Modifier.fillMaxSize()) { CollapsedShadeHeader( viewModel = headerViewModel, + isSplitShade = true, modifier = Modifier.then(brightnessMirrorShowingModifier) .padding(horizontal = { unfoldTranslationXForStartSide.roundToInt() }), diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt index 72ee75ad2d47..90bf92ae1dd0 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt @@ -32,12 +32,17 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ApproachLayoutModifierNode +import androidx.compose.ui.layout.ApproachMeasureScope import androidx.compose.ui.layout.LookaheadScope -import androidx.compose.ui.layout.approachLayout -import androidx.compose.ui.layout.layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.node.DelegatingNode +import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.zIndex import com.android.compose.animation.scene.Ancestor import com.android.compose.animation.scene.AnimatedState import com.android.compose.animation.scene.ContentKey @@ -68,8 +73,8 @@ import com.android.compose.gesture.NestedScrollControlState import com.android.compose.gesture.NestedScrollableBound import com.android.compose.gesture.nestedScrollController import com.android.compose.modifiers.thenIf +import com.android.compose.ui.graphics.ContainerNode import com.android.compose.ui.graphics.ContainerState -import com.android.compose.ui.graphics.container import kotlin.math.pow /** A content defined in a [SceneTransitionLayout], i.e. a scene or an overlay. */ @@ -158,24 +163,14 @@ internal sealed class Content( fun Content(modifier: Modifier = Modifier, isInvisible: Boolean = false) { // If this content has a custom factory, provide it to the content so that the factory is // automatically used when calling rememberOverscrollEffect(). + val isElevationPossible = + layoutImpl.state.isElevationPossible(content = key, element = null) Box( - modifier - .thenIf(isInvisible) { InvisibleModifier } - .zIndex(zIndex) - .approachLayout( - isMeasurementApproachInProgress = { layoutImpl.state.isTransitioning() } - ) { measurable, constraints -> - // TODO(b/353679003): Use the ModifierNode API to set this *before* the - // approach - // pass is started. - targetSize = lookaheadSize - val placeable = measurable.measure(constraints) - layout(placeable.width, placeable.height) { placeable.place(0, 0) } - } - .thenIf(layoutImpl.state.isElevationPossible(content = key, element = null)) { - Modifier.container(containerState) - } - .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) } + modifier.then(ContentElement(this, isElevationPossible, isInvisible)).thenIf( + layoutImpl.implicitTestTags + ) { + Modifier.testTag(key.testTag) + } ) { CompositionLocalProvider(LocalOverscrollFactory provides lastFactory) { scope.content() @@ -194,6 +189,72 @@ internal sealed class Content( } } +private data class ContentElement( + private val content: Content, + private val isElevationPossible: Boolean, + private val isInvisible: Boolean, +) : ModifierNodeElement<ContentNode>() { + override fun create(): ContentNode = ContentNode(content, isElevationPossible, isInvisible) + + override fun update(node: ContentNode) { + node.update(content, isElevationPossible, isInvisible) + } +} + +private class ContentNode( + private var content: Content, + private var isElevationPossible: Boolean, + private var isInvisible: Boolean, +) : DelegatingNode(), ApproachLayoutModifierNode { + private var containerDelegate = containerDelegate(isElevationPossible) + + private fun containerDelegate(isElevationPossible: Boolean): ContainerNode? { + return if (isElevationPossible) delegate(ContainerNode(content.containerState)) else null + } + + fun update(content: Content, isElevationPossible: Boolean, isInvisible: Boolean) { + if (content != this.content || isElevationPossible != this.isElevationPossible) { + this.content = content + this.isElevationPossible = isElevationPossible + + containerDelegate?.let { undelegate(it) } + containerDelegate = containerDelegate(isElevationPossible) + } + + this.isInvisible = isInvisible + } + + override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean = false + + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints, + ): MeasureResult { + check(isLookingAhead) + return measurable.measure(constraints).run { + content.targetSize = IntSize(width, height) + layout(width, height) { + if (!isInvisible) { + place(0, 0, zIndex = content.zIndex) + } + } + } + } + + override fun ApproachMeasureScope.approachMeasure( + measurable: Measurable, + constraints: Constraints, + ): MeasureResult { + return measurable.measure(constraints).run { + layout(width, height) { + if (!isInvisible) { + place(0, 0, zIndex = content.zIndex) + } + } + } + } +} + internal class ContentEffects(factory: OverscrollFactory) { val overscrollEffect = factory.createOverscrollEffect() val gestureEffect = GestureEffect(overscrollEffect) @@ -307,8 +368,3 @@ internal class ContentScopeImpl( ) } } - -private val InvisibleModifier = - Modifier.layout { measurable, constraints -> - measurable.measure(constraints).run { layout(width, height) {} } - } diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json index 0fcdfa3e1b53..57f67665242c 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json @@ -60,7 +60,9 @@ 912, 928, 944, - 960 + 960, + 976, + 992 ], "features": [ { @@ -310,6 +312,14 @@ { "x": 64, "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 } ] }, @@ -491,67 +501,75 @@ }, { "width": 170.4, - "height": 39.2 + "height": 28.8 }, { "width": 166.8, - "height": 36 + "height": 15.2 }, { "width": 164, - "height": 31.6 + "height": 6.4 }, { "width": 162.4, - "height": 26.8 + "height": 0.8 }, { "width": 161.2, - "height": 22 + "height": 0 }, { "width": 160.4, - "height": 17.6 + "height": 0 + }, + { + "width": 160, + "height": 0 + }, + { + "width": 160, + "height": 0 }, { "width": 160, - "height": 14 + "height": 0 }, { "width": 160, - "height": 10.8 + "height": 0 }, { "width": 160, - "height": 8 + "height": 0 }, { "width": 160, - "height": 6 + "height": 0 }, { "width": 160, - "height": 4.4 + "height": 0 }, { "width": 160, - "height": 2.8 + "height": 0 }, { "width": 160, - "height": 2 + "height": 0 }, { "width": 160, - "height": 1.2 + "height": 0 }, { "width": 160, - "height": 0.8 + "height": 0 }, { "width": 160, - "height": 0.4 + "height": 0 }, { "width": 160, @@ -627,6 +645,8 @@ 0, 0, 0, + 0, + 0, 0 ] } diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json index 3196334c5314..01bc852cf7f4 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json @@ -59,7 +59,9 @@ 896, 912, 928, - 944 + 944, + 960, + 976 ], "features": [ { @@ -305,6 +307,14 @@ { "x": 64, "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 } ] }, @@ -482,71 +492,79 @@ }, { "width": 171.6, - "height": 39.6 + "height": 32 }, { "width": 167.6, - "height": 36.8 + "height": 18 }, { "width": 164.8, - "height": 32.4 + "height": 8.8 }, { "width": 162.8, - "height": 27.6 + "height": 2.8 }, { "width": 161.6, - "height": 22.8 + "height": 0 }, { "width": 160.8, - "height": 18.4 + "height": 0 }, { "width": 160.4, - "height": 14.4 + "height": 0 }, { "width": 160, - "height": 11.2 + "height": 0 }, { "width": 160, - "height": 8.4 + "height": 0 }, { "width": 160, - "height": 6.4 + "height": 0 }, { "width": 160, - "height": 4.4 + "height": 0 }, { "width": 160, - "height": 3.2 + "height": 0 }, { "width": 160, - "height": 2 + "height": 0 }, { "width": 160, - "height": 1.6 + "height": 0 }, { "width": 160, - "height": 0.8 + "height": 0 }, { "width": 160, - "height": 0.4 + "height": 0 }, { "width": 160, - "height": 0.4 + "height": 0 + }, + { + "width": 160, + "height": 0 + }, + { + "width": 160, + "height": 0 }, { "width": 160, @@ -617,6 +635,8 @@ 0, 0, 0, + 0, + 0, 0 ] } diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json index 4b0306853903..b6e423afc6c4 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json @@ -336,55 +336,55 @@ }, { "width": 188, - "height": 24.8 + "height": 25.6 }, { "width": 188, - "height": 32.8 + "height": 36.4 }, { "width": 188, - "height": 44 + "height": 45.6 }, { "width": 188, - "height": 57.2 + "height": 59.2 }, { "width": 188, - "height": 70.8 + "height": 72.8 }, { "width": 188, - "height": 78 + "height": 79.6 }, { "width": 188, - "height": 91.2 + "height": 92.8 }, { "width": 188, - "height": 103.2 + "height": 104.4 }, { "width": 188, - "height": 114.4 + "height": 115.2 }, { "width": 188, - "height": 124.4 + "height": 125.2 }, { "width": 188, - "height": 134 + "height": 134.8 }, { "width": 188, - "height": 142.8 + "height": 143.2 }, { "width": 188, - "height": 150.8 + "height": 151.2 }, { "width": 188, @@ -392,7 +392,7 @@ }, { "width": 188, - "height": 159.6 + "height": 160 }, { "width": 188, @@ -400,7 +400,7 @@ }, { "width": 188, - "height": 174 + "height": 174.4 }, { "width": 188, @@ -412,7 +412,7 @@ }, { "width": 188, - "height": 187.6 + "height": 188 }, { "width": 188, diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json index 10a9ba7e2760..a82db346ed58 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json @@ -39,7 +39,9 @@ 576, 592, 608, - 624 + 624, + 640, + 656 ], "features": [ { @@ -205,6 +207,14 @@ { "x": 64, "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 } ] }, @@ -302,67 +312,75 @@ }, { "width": 170.8, - "height": 39.6 + "height": 29.6 }, { "width": 166.8, - "height": 36.8 + "height": 12.8 }, { "width": 164, - "height": 32.4 + "height": 2.4 }, { "width": 162, - "height": 27.6 + "height": 0 }, { "width": 160.8, - "height": 22.8 + "height": 0 }, { "width": 160.4, - "height": 18.4 + "height": 0 }, { "width": 160, - "height": 14.4 + "height": 0 }, { "width": 160, - "height": 11.2 + "height": 0 }, { "width": 160, - "height": 8.4 + "height": 0 }, { "width": 160, - "height": 6 + "height": 0 }, { "width": 160, - "height": 4.4 + "height": 0 }, { "width": 160, - "height": 3.2 + "height": 0 }, { "width": 160, - "height": 2 + "height": 0 }, { "width": 160, - "height": 1.2 + "height": 0 }, { "width": 160, - "height": 0.8 + "height": 0 }, { "width": 160, - "height": 0.4 + "height": 0 + }, + { + "width": 160, + "height": 0 + }, + { + "width": 160, + "height": 0 }, { "width": 160, @@ -417,6 +435,8 @@ 0, 0, 0, + 0, + 0, 0 ] } diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json index d8bf48d32d20..6dc5a0e79e81 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json @@ -32,8 +32,7 @@ 464, 480, 496, - 512, - 528 + 512 ], "features": [ { @@ -163,10 +162,6 @@ { "x": 50, "y": 50 - }, - { - "x": 50, - "y": 50 } ] }, @@ -228,63 +223,59 @@ }, { "width": 188, - "height": 42.4 + "height": 44.4 }, { "width": 188, - "height": 100 + "height": 103.6 }, { "width": 188, - "height": 161.6 + "height": 166 }, { "width": 188, - "height": 218 + "height": 222.4 }, { "width": 188, - "height": 265.6 + "height": 270 }, { "width": 188, - "height": 303.6 + "height": 307.2 }, { "width": 188, - "height": 332.4 + "height": 335.6 }, { "width": 188, - "height": 354 + "height": 356.4 }, { "width": 188, - "height": 369.2 + "height": 371.2 }, { "width": 188, - "height": 380 + "height": 381.6 }, { "width": 188, - "height": 387.2 + "height": 388.8 }, { "width": 188, - "height": 392 + "height": 393.2 }, { "width": 188, - "height": 395.2 + "height": 396 }, { "width": 188, - "height": 397.6 - }, - { - "width": 188, - "height": 398.4 + "height": 398 }, { "width": 188, @@ -356,7 +347,6 @@ 1, 1, 1, - 1, 1 ] } diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json index 57bdf3e1ecab..1cd971aa2898 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json @@ -69,9 +69,7 @@ 1056, 1072, 1088, - 1104, - 1120, - 1136 + 1104 ], "features": [ { @@ -353,14 +351,6 @@ { "x": 64, "y": 50 - }, - { - "x": 64, - "y": 50 - }, - { - "x": 64, - "y": 50 } ] }, @@ -466,43 +456,43 @@ }, { "width": 188, - "height": 24 + "height": 24.8 }, { "width": 188, - "height": 29.6 + "height": 30 }, { "width": 188, - "height": 37.2 + "height": 38 }, { "width": 188, - "height": 45.6 + "height": 46 }, { "width": 188, - "height": 53.6 + "height": 54 }, { "width": 188, - "height": 60.4 + "height": 61.2 }, { "width": 188, - "height": 66.4 + "height": 66.8 }, { "width": 188, - "height": 71.2 + "height": 71.6 }, { "width": 188, - "height": 75.2 + "height": 75.6 }, { "width": 188, - "height": 77.6 + "height": 78 }, { "width": 188, @@ -510,7 +500,7 @@ }, { "width": 188, - "height": 80.4 + "height": 80.8 }, { "width": 188, @@ -522,7 +512,7 @@ }, { "width": 188, - "height": 79.2 + "height": 79.6 }, { "width": 187.6, @@ -530,7 +520,7 @@ }, { "width": 186.8, - "height": 76 + "height": 76.4 }, { "width": 186, @@ -578,43 +568,39 @@ }, { "width": 172.4, - "height": 39.2 + "height": 37.6 }, { "width": 170.8, - "height": 38.4 + "height": 38 }, { "width": 169.2, - "height": 34.8 + "height": 30.4 }, { "width": 167.6, - "height": 30 + "height": 25.2 }, { "width": 166, - "height": 25.2 + "height": 20.4 }, { "width": 164, - "height": 20.4 + "height": 16 }, { "width": 162.4, - "height": 16.4 + "height": 12.4 }, { "width": 160.8, - "height": 12.8 - }, - { - "width": 160, - "height": 9.6 + "height": 9.2 }, { "width": 160, - "height": 7.2 + "height": 6.8 }, { "width": 160, @@ -626,7 +612,7 @@ }, { "width": 160, - "height": 2.8 + "height": 2.4 }, { "width": 160, @@ -634,10 +620,6 @@ }, { "width": 160, - "height": 1.2 - }, - { - "width": 160, "height": 0.8 }, { @@ -646,7 +628,7 @@ }, { "width": 160, - "height": 0 + "height": 0.4 }, { "width": 160, @@ -735,8 +717,6 @@ 0.03147719, 0.019312752, 0.011740655, - 0, - 0, 0 ] } diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json index 9aa91c2d5e17..1030455e873f 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json @@ -27,7 +27,9 @@ 384, 400, 416, - 432 + 432, + 448, + 464 ], "features": [ { @@ -145,6 +147,14 @@ { "x": 64, "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 } ] }, @@ -194,67 +204,75 @@ }, { "width": 168.8, - "height": 38.4 + "height": 22.4 }, { "width": 165.2, - "height": 34.8 + "height": 10 }, { "width": 162.8, - "height": 30 + "height": 2.4 }, { "width": 161.2, - "height": 25.2 + "height": 0 }, { "width": 160.4, - "height": 20.4 + "height": 0 }, { "width": 160, - "height": 16.4 + "height": 0 }, { "width": 160, - "height": 12.8 + "height": 0 }, { "width": 160, - "height": 9.6 + "height": 0 }, { "width": 160, - "height": 7.2 + "height": 0 }, { "width": 160, - "height": 5.2 + "height": 0 }, { "width": 160, - "height": 3.6 + "height": 0 }, { "width": 160, - "height": 2.8 + "height": 0 }, { "width": 160, - "height": 1.6 + "height": 0 }, { "width": 160, - "height": 1.2 + "height": 0 }, { "width": 160, - "height": 0.8 + "height": 0 }, { "width": 160, - "height": 0.4 + "height": 0 + }, + { + "width": 160, + "height": 0 + }, + { + "width": 160, + "height": 0 }, { "width": 160, @@ -297,6 +315,8 @@ 0, 0, 0, + 0, + 0, 0 ] } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt index 0bd51cd9822d..44f2353dcb75 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt @@ -111,8 +111,8 @@ private constructor(metadata: FailureMetadata, private val actual: TransitionSta } companion object { - fun transitionStates() = Factory { metadata, actual: TransitionState -> - TransitionStateSubject(metadata, actual) + fun transitionStates() = Factory { metadata, actual: TransitionState? -> + TransitionStateSubject(metadata, actual!!) } } } @@ -181,8 +181,8 @@ private constructor(metadata: FailureMetadata, actual: TransitionState.Transitio companion object { fun sceneTransitions() = - Factory { metadata, actual: TransitionState.Transition.ChangeScene -> - SceneTransitionSubject(metadata, actual) + Factory { metadata, actual: TransitionState.Transition.ChangeScene? -> + SceneTransitionSubject(metadata, actual!!) } } } @@ -202,8 +202,8 @@ private constructor( companion object { fun showOrHideOverlayTransitions() = - Factory { metadata, actual: TransitionState.Transition.ShowOrHideOverlay -> - ShowOrHideOverlayTransitionSubject(metadata, actual) + Factory { metadata, actual: TransitionState.Transition.ShowOrHideOverlay? -> + ShowOrHideOverlayTransitionSubject(metadata, actual!!) } } } @@ -221,8 +221,8 @@ private constructor(metadata: FailureMetadata, actual: TransitionState.Transitio companion object { fun replaceOverlayTransitions() = - Factory { metadata, actual: TransitionState.Transition.ReplaceOverlay -> - ReplaceOverlayTransitionSubject(metadata, actual) + Factory { metadata, actual: TransitionState.Transition.ReplaceOverlay? -> + ReplaceOverlayTransitionSubject(metadata, actual!!) } } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt index ab31038fac8f..368a333fbd78 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt @@ -49,8 +49,8 @@ class DpOffsetSubject(metadata: FailureMetadata, private val actual: DpOffset) : val DefaultTolerance = Dp(.5f) fun dpOffsets() = - Factory<DpOffsetSubject, DpOffset> { metadata, actual -> - DpOffsetSubject(metadata, actual) + Factory { metadata, actual: DpOffset? -> + DpOffsetSubject(metadata, actual!!) } } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index 4bf0ceb51784..6e29e6932629 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -38,8 +38,9 @@ import com.android.systemui.animation.TypefaceVariantCacheImpl import com.android.systemui.customization.R import com.android.systemui.log.core.LogLevel import com.android.systemui.log.core.LogcatOnlyMessageBuffer -import com.android.systemui.log.core.Logger import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.plugins.clocks.ClockLogger +import com.android.systemui.plugins.clocks.ClockLogger.Companion.escapeTime import java.io.PrintWriter import java.util.Calendar import java.util.Locale @@ -67,7 +68,7 @@ constructor( var messageBuffer: MessageBuffer get() = logger.buffer set(value) { - logger = Logger(value, TAG) + logger = ClockLogger(this, value, TAG) } var hasCustomPositionUpdatedAnimation: Boolean = false @@ -185,7 +186,9 @@ constructor( time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis() contentDescription = DateFormat.format(descFormat, time) val formattedText = DateFormat.format(format, time) - logger.d({ "refreshTime: new formattedText=$str1" }) { str1 = formattedText?.toString() } + logger.d({ "refreshTime: new formattedText=${escapeTime(str1)}" }) { + str1 = formattedText?.toString() + } // Setting text actually triggers a layout pass in TextView (because the text view is set to // wrap_content width and TextView always relayouts for this). This avoids needless relayout @@ -195,7 +198,7 @@ constructor( } text = formattedText - logger.d({ "refreshTime: done setting new time text to: $str1" }) { + logger.d({ "refreshTime: done setting new time text to: ${escapeTime(str1)}" }) { str1 = formattedText?.toString() } @@ -225,7 +228,7 @@ constructor( @SuppressLint("DrawAllocation") override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - logger.d("onMeasure") + logger.onMeasure(widthMeasureSpec, heightMeasureSpec) if (!isSingleLineInternal && MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) { // Call straight into TextView.setTextSize to avoid setting lastUnconstrainedTextSize @@ -263,14 +266,14 @@ constructor( canvas.translate(parentWidth / 4f, 0f) } - logger.d({ "onDraw($str1)" }) { str1 = text.toString() } + logger.onDraw("$text") // intentionally doesn't call super.onDraw here or else the text will be rendered twice textAnimator?.draw(canvas) canvas.restore() } override fun invalidate() { - logger.d("invalidate") + logger.invalidate() super.invalidate() } @@ -280,7 +283,7 @@ constructor( lengthBefore: Int, lengthAfter: Int, ) { - logger.d({ "onTextChanged($str1)" }) { str1 = text.toString() } + logger.d({ "onTextChanged(${escapeTime(str1)})" }) { str1 = "$text" } super.onTextChanged(text, start, lengthBefore, lengthAfter) } @@ -370,7 +373,7 @@ constructor( return } - logger.d("animateCharge") + logger.animateCharge() val startAnimPhase2 = Runnable { setTextStyle( weight = if (isDozing()) dozingWeight else lockScreenWeight, @@ -394,7 +397,7 @@ constructor( } fun animateDoze(isDozing: Boolean, animate: Boolean) { - logger.d("animateDoze") + logger.animateDoze(isDozing, animate) setTextStyle( weight = if (isDozing) dozingWeight else lockScreenWeight, color = if (isDozing) dozingColor else lockScreenColor, @@ -484,7 +487,7 @@ constructor( isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12 else -> DOUBLE_LINE_FORMAT_12_HOUR } - logger.d({ "refreshFormat($str1)" }) { str1 = format?.toString() } + logger.d({ "refreshFormat(${escapeTime(str1)})" }) { str1 = format?.toString() } descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12 refreshTime() @@ -634,7 +637,7 @@ constructor( companion object { private val TAG = AnimatableClockView::class.simpleName!! - private val DEFAULT_LOGGER = Logger(LogcatOnlyMessageBuffer(LogLevel.WARNING), TAG) + private val DEFAULT_LOGGER = ClockLogger(null, LogcatOnlyMessageBuffer(LogLevel.DEBUG), TAG) const val ANIMATION_DURATION_FOLD_TO_AOD: Int = 600 private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm" diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt index dd1599e5259d..9857d7f3d69d 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt @@ -17,6 +17,8 @@ package com.android.systemui.shared.clocks import android.graphics.Canvas +import com.android.systemui.plugins.clocks.VPoint +import com.android.systemui.plugins.clocks.VPointF object CanvasUtil { fun Canvas.translate(pt: VPointF) = this.translate(pt.x, pt.y) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt index e9e61a718f08..37acbe261f76 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt @@ -161,15 +161,7 @@ class ComposedDigitalLayerController(private val clockCtx: ClockContext) : } override fun onThemeChanged(theme: ThemeConfig) { - val color = - when { - theme.seedColor != null -> theme.seedColor!! - theme.isDarkTheme -> - clockCtx.resources.getColor(android.R.color.system_accent1_100) - else -> clockCtx.resources.getColor(android.R.color.system_accent2_600) - } - - view.updateColor(color) + view.updateColor(theme.getDefaultColor(clockCtx.context)) } override fun onFontSettingChanged(fontSizePx: Float) { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt index 365567b17ec0..bc4bdf4243cb 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -149,14 +149,7 @@ class DefaultClockController( override fun onThemeChanged(theme: ThemeConfig) { this@DefaultClockFaceController.theme = theme - val color = - when { - theme.seedColor != null -> theme.seedColor!! - theme.isDarkTheme -> - resources.getColor(android.R.color.system_accent1_100) - else -> resources.getColor(android.R.color.system_accent2_600) - } - + val color = theme.getDefaultColor(ctx) if (currentColor == color) { return } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt index f5ccc52c8c6b..941cebfb4014 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt @@ -20,7 +20,8 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.TimeInterpolator import android.animation.ValueAnimator -import com.android.systemui.shared.clocks.VPointF.Companion.times +import com.android.systemui.plugins.clocks.VPointF +import com.android.systemui.plugins.clocks.VPointF.Companion.times class DigitTranslateAnimator(private val updateCallback: (VPointF) -> Unit) { var currentTranslation = VPointF.ZERO diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt index 336c66eed889..9ac9e60f05fd 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt @@ -16,13 +16,13 @@ package com.android.systemui.shared.clocks -import android.graphics.RectF import android.view.View import androidx.annotation.VisibleForTesting import com.android.systemui.plugins.clocks.ClockAnimations import com.android.systemui.plugins.clocks.ClockEvents import com.android.systemui.plugins.clocks.ClockFaceConfig import com.android.systemui.plugins.clocks.ClockFaceEvents +import com.android.systemui.plugins.clocks.VRectF interface SimpleClockLayerController { val view: View @@ -32,5 +32,5 @@ interface SimpleClockLayerController { val config: ClockFaceConfig @VisibleForTesting var fakeTimeMills: Long? - var onViewBoundsChanged: ((RectF) -> Unit)? + var onViewBoundsChanged: ((VRectF) -> Unit)? } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt index 97004ef6f9a9..1d963af3ad22 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt @@ -229,15 +229,7 @@ open class SimpleDigitalHandLayerController( } override fun onThemeChanged(theme: ThemeConfig) { - val color = - when { - theme.seedColor != null -> theme.seedColor!! - theme.isDarkTheme -> - clockCtx.resources.getColor(android.R.color.system_accent1_100) - else -> clockCtx.resources.getColor(android.R.color.system_accent2_600) - } - - view.updateColor(color) + view.updateColor(theme.getDefaultColor(clockCtx.context)) refreshTime() } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt index 1e90a2370786..0740b0e504cb 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt @@ -18,8 +18,9 @@ package com.android.systemui.shared.clocks import android.graphics.Rect import android.view.View -import com.android.systemui.shared.clocks.VPoint.Companion.center -import com.android.systemui.shared.clocks.VPointF.Companion.center +import com.android.systemui.plugins.clocks.VPoint.Companion.center +import com.android.systemui.plugins.clocks.VPointF +import com.android.systemui.plugins.clocks.VPointF.Companion.center object ViewUtils { fun View.computeLayoutDiff(targetRegion: Rect, isLargeClock: Boolean): VPointF { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt index 2dc3e2b7af73..ba32ab083063 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt @@ -17,7 +17,6 @@ package com.android.systemui.shared.clocks.view import android.graphics.Canvas -import android.graphics.RectF import android.icu.text.NumberFormat import android.util.MathUtils.constrainedMap import android.view.View @@ -29,14 +28,15 @@ import com.android.app.animation.Interpolators import com.android.systemui.customization.R import com.android.systemui.plugins.clocks.ClockFontAxisSetting import com.android.systemui.plugins.clocks.ClockLogger +import com.android.systemui.plugins.clocks.VPoint +import com.android.systemui.plugins.clocks.VPointF +import com.android.systemui.plugins.clocks.VPointF.Companion.max +import com.android.systemui.plugins.clocks.VPointF.Companion.times +import com.android.systemui.plugins.clocks.VRectF import com.android.systemui.shared.clocks.CanvasUtil.translate import com.android.systemui.shared.clocks.CanvasUtil.use import com.android.systemui.shared.clocks.ClockContext import com.android.systemui.shared.clocks.DigitTranslateAnimator -import com.android.systemui.shared.clocks.VPoint -import com.android.systemui.shared.clocks.VPointF -import com.android.systemui.shared.clocks.VPointF.Companion.max -import com.android.systemui.shared.clocks.VPointF.Companion.times import com.android.systemui.shared.clocks.ViewUtils.measuredSize import java.util.Locale import kotlin.collections.filterNotNull @@ -101,7 +101,7 @@ class FlexClockView(clockCtx: ClockContext) : ViewGroup(clockCtx.context) { updateLocale(Locale.getDefault()) } - var onViewBoundsChanged: ((RectF) -> Unit)? = null + var onViewBoundsChanged: ((VRectF) -> Unit)? = null private val digitOffsets = mutableMapOf<Int, Float>() protected fun calculateSize( @@ -189,13 +189,7 @@ class FlexClockView(clockCtx: ClockContext) : ViewGroup(clockCtx.context) { fun updateLocation() { val layoutBounds = this.layoutBounds ?: return - val bounds = - RectF( - layoutBounds.centerX() - measuredWidth / 2f, - layoutBounds.centerY() - measuredHeight / 2f, - layoutBounds.centerX() + measuredWidth / 2f, - layoutBounds.centerY() + measuredHeight / 2f, - ) + val bounds = VRectF.fromCenter(layoutBounds.center, this.measuredSize) setFrame( bounds.left.roundToInt(), bounds.top.roundToInt(), @@ -215,16 +209,11 @@ class FlexClockView(clockCtx: ClockContext) : ViewGroup(clockCtx.context) { onAnimateDoze = null } - private val layoutBounds = RectF() + private var layoutBounds = VRectF.ZERO override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { logger.onLayout(changed, left, top, right, bottom) - - layoutBounds.left = left.toFloat() - layoutBounds.top = top.toFloat() - layoutBounds.right = right.toFloat() - layoutBounds.bottom = bottom.toFloat() - + layoutBounds = VRectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat()) updateChildFrames(isLayout = true) } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt index fae17a5321ff..2af25fe339a2 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt @@ -23,7 +23,6 @@ import android.graphics.Paint import android.graphics.PorterDuff import android.graphics.PorterDuffXfermode import android.graphics.Rect -import android.graphics.RectF import android.os.VibrationEffect import android.text.Layout import android.text.TextPaint @@ -45,6 +44,11 @@ import com.android.systemui.plugins.clocks.ClockFontAxisSetting import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.replace import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.toFVar import com.android.systemui.plugins.clocks.ClockLogger +import com.android.systemui.plugins.clocks.VPoint +import com.android.systemui.plugins.clocks.VPointF +import com.android.systemui.plugins.clocks.VPointF.Companion.size +import com.android.systemui.plugins.clocks.VRectF +import com.android.systemui.shared.Flags.ambientAod import com.android.systemui.shared.clocks.CanvasUtil.translate import com.android.systemui.shared.clocks.CanvasUtil.use import com.android.systemui.shared.clocks.ClockContext @@ -52,9 +56,6 @@ import com.android.systemui.shared.clocks.DigitTranslateAnimator import com.android.systemui.shared.clocks.DimensionParser import com.android.systemui.shared.clocks.FLEX_CLOCK_ID import com.android.systemui.shared.clocks.FontTextStyle -import com.android.systemui.shared.clocks.VPoint -import com.android.systemui.shared.clocks.VPointF -import com.android.systemui.shared.clocks.VPointF.Companion.size import com.android.systemui.shared.clocks.ViewUtils.measuredSize import com.android.systemui.shared.clocks.ViewUtils.size import com.android.systemui.shared.clocks.toClockAxisSetting @@ -65,11 +66,11 @@ import kotlin.math.roundToInt private val TAG = SimpleDigitalClockTextView::class.simpleName!! -private fun Paint.getTextBounds(text: CharSequence, result: RectF = RectF()): RectF { - val rect = Rect() - this.getTextBounds(text, 0, text.length, rect) - result.set(rect) - return result +private val tempRect = Rect() + +private fun Paint.getTextBounds(text: CharSequence): VRectF { + this.getTextBounds(text, 0, text.length, tempRect) + return VRectF(tempRect) } enum class VerticalAlignment { @@ -142,7 +143,7 @@ open class SimpleDigitalClockTextView( fidgetFontVariation = buildFidgetVariation(lsFontAxes).toFVar() } - var onViewBoundsChanged: ((RectF) -> Unit)? = null + var onViewBoundsChanged: ((VRectF) -> Unit)? = null private val parser = DimensionParser(clockCtx.context) var maxSingleDigitHeight = -1f var maxSingleDigitWidth = -1f @@ -158,13 +159,13 @@ open class SimpleDigitalClockTextView( private val initThread = Thread.currentThread() // textBounds is the size of text in LS, which only measures current text in lockscreen style - var textBounds = RectF() + var textBounds = VRectF.ZERO // prevTextBounds and targetTextBounds are to deal with dozing animation between LS and AOD // especially for the textView which has different bounds during the animation // prevTextBounds holds the state we are transitioning from - private val prevTextBounds = RectF() + private var prevTextBounds = VRectF.ZERO // targetTextBounds holds the state we are interpolating to - private val targetTextBounds = RectF() + private var targetTextBounds = VRectF.ZERO protected val logger = ClockLogger(this, clockCtx.messageBuffer, this::class.simpleName!!) get() = field ?: ClockLogger.INIT_LOGGER @@ -214,8 +215,8 @@ open class SimpleDigitalClockTextView( lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation) typeface = lockScreenPaint.typeface - lockScreenPaint.getTextBounds(text, textBounds) - targetTextBounds.set(textBounds) + textBounds = lockScreenPaint.getTextBounds(text) + targetTextBounds = textBounds textAnimator.setTextStyle(TextAnimator.Style(fVar = lsFontVariation)) measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) @@ -286,7 +287,7 @@ open class SimpleDigitalClockTextView( canvas.use { digitTranslateAnimator?.apply { canvas.translate(currentTranslation) } canvas.translate(getDrawTranslation(interpBounds)) - if (isLayoutRtl()) canvas.translate(interpBounds.width() - textBounds.width(), 0f) + if (isLayoutRtl()) canvas.translate(interpBounds.width - textBounds.width, 0f) textAnimator.draw(canvas) } } @@ -301,16 +302,12 @@ open class SimpleDigitalClockTextView( super.setAlpha(alpha) } - private val layoutBounds = RectF() + private var layoutBounds = VRectF.ZERO override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) logger.onLayout(changed, left, top, right, bottom) - - layoutBounds.left = left.toFloat() - layoutBounds.top = top.toFloat() - layoutBounds.right = right.toFloat() - layoutBounds.bottom = bottom.toFloat() + layoutBounds = VRectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat()) } override fun invalidate() { @@ -326,11 +323,11 @@ open class SimpleDigitalClockTextView( fun animateDoze(isDozing: Boolean, isAnimated: Boolean) { if (!this::textAnimator.isInitialized) return - logger.animateDoze() + logger.animateDoze(isDozing, isAnimated) textAnimator.setTextStyle( TextAnimator.Style( fVar = if (isDozing) aodFontVariation else lsFontVariation, - color = if (isDozing) AOD_COLOR else lockscreenColor, + color = if (isDozing && !ambientAod()) AOD_COLOR else lockscreenColor, textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize, ), TextAnimator.Animation( @@ -340,6 +337,11 @@ open class SimpleDigitalClockTextView( ), ) updateTextBoundsForTextAnimator() + + if (!isAnimated) { + requestLayout() + (parent as? FlexClockView)?.requestLayout() + } } fun animateCharge() { @@ -407,10 +409,10 @@ open class SimpleDigitalClockTextView( } fun refreshText() { - lockScreenPaint.getTextBounds(text, textBounds) - if (this::textAnimator.isInitialized) { - textAnimator.textInterpolator.targetPaint.getTextBounds(text, targetTextBounds) - } + textBounds = lockScreenPaint.getTextBounds(text) + targetTextBounds = + if (!this::textAnimator.isInitialized) textBounds + else textAnimator.textInterpolator.targetPaint.getTextBounds(text) if (layout == null) { requestLayout() @@ -431,23 +433,23 @@ open class SimpleDigitalClockTextView( } /** Returns the interpolated text bounding rect based on interpolation progress */ - private fun getInterpolatedTextBounds(progress: Float = getInterpolatedProgress()): RectF { + private fun getInterpolatedTextBounds(progress: Float = getInterpolatedProgress()): VRectF { if (progress <= 0f) { return prevTextBounds } else if (!textAnimator.isRunning || progress >= 1f) { return targetTextBounds } - return RectF().apply { - left = lerp(prevTextBounds.left, targetTextBounds.left, progress) - right = lerp(prevTextBounds.right, targetTextBounds.right, progress) - top = lerp(prevTextBounds.top, targetTextBounds.top, progress) - bottom = lerp(prevTextBounds.bottom, targetTextBounds.bottom, progress) - } + return VRectF( + left = lerp(prevTextBounds.left, targetTextBounds.left, progress), + right = lerp(prevTextBounds.right, targetTextBounds.right, progress), + top = lerp(prevTextBounds.top, targetTextBounds.top, progress), + bottom = lerp(prevTextBounds.bottom, targetTextBounds.bottom, progress), + ) } private fun computeMeasuredSize( - interpBounds: RectF, + interpBounds: VRectF, widthMeasureSpec: Int = measuredWidthAndState, heightMeasureSpec: Int = measuredHeightAndState, ): VPointF { @@ -460,11 +462,11 @@ open class SimpleDigitalClockTextView( return VPointF( when { mode.x == EXACTLY -> MeasureSpec.getSize(widthMeasureSpec).toFloat() - else -> interpBounds.width() + 2 * lockScreenPaint.strokeWidth + else -> interpBounds.width + 2 * lockScreenPaint.strokeWidth }, when { mode.y == EXACTLY -> MeasureSpec.getSize(heightMeasureSpec).toFloat() - else -> interpBounds.height() + 2 * lockScreenPaint.strokeWidth + else -> interpBounds.height + 2 * lockScreenPaint.strokeWidth }, ) } @@ -488,44 +490,23 @@ open class SimpleDigitalClockTextView( } /** Set the location of the view to match the interpolated text bounds */ - private fun setInterpolatedLocation(measureSize: VPointF): RectF { - val targetRect = RectF() - targetRect.apply { - when (xAlignment) { - XAlignment.LEFT -> { - left = layoutBounds.left - right = layoutBounds.left + measureSize.x - } - XAlignment.CENTER -> { - left = layoutBounds.centerX() - measureSize.x / 2f - right = layoutBounds.centerX() + measureSize.x / 2f - } - XAlignment.RIGHT -> { - left = layoutBounds.right - measureSize.x - right = layoutBounds.right - } - } - - when (verticalAlignment) { - VerticalAlignment.TOP -> { - top = layoutBounds.top - bottom = layoutBounds.top + measureSize.y - } - VerticalAlignment.CENTER -> { - top = layoutBounds.centerY() - measureSize.y / 2f - bottom = layoutBounds.centerY() + measureSize.y / 2f - } - VerticalAlignment.BOTTOM -> { - top = layoutBounds.bottom - measureSize.y - bottom = layoutBounds.bottom - } - VerticalAlignment.BASELINE -> { - top = layoutBounds.centerY() - measureSize.y / 2f - bottom = layoutBounds.centerY() + measureSize.y / 2f - } - } - } + private fun setInterpolatedLocation(measureSize: VPointF): VRectF { + val pos = + VPointF( + when (xAlignment) { + XAlignment.LEFT -> layoutBounds.left + XAlignment.CENTER -> layoutBounds.center.x - measureSize.x / 2f + XAlignment.RIGHT -> layoutBounds.right - measureSize.x + }, + when (verticalAlignment) { + VerticalAlignment.TOP -> layoutBounds.top + VerticalAlignment.CENTER -> layoutBounds.center.y - measureSize.y / 2f + VerticalAlignment.BOTTOM -> layoutBounds.bottom - measureSize.y + VerticalAlignment.BASELINE -> layoutBounds.center.y - measureSize.y / 2f + }, + ) + val targetRect = VRectF.fromTopLeft(pos, measureSize) setFrame( targetRect.left.roundToInt(), targetRect.top.roundToInt(), @@ -536,7 +517,7 @@ open class SimpleDigitalClockTextView( return targetRect } - private fun getDrawTranslation(interpBounds: RectF): VPointF { + private fun getDrawTranslation(interpBounds: VRectF): VPointF { val sizeDiff = this.measuredSize - interpBounds.size val alignment = VPointF( @@ -585,11 +566,11 @@ open class SimpleDigitalClockTextView( if (fontSizePx > 0) { setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx) lockScreenPaint.textSize = textSize - lockScreenPaint.getTextBounds(text, textBounds) - targetTextBounds.set(textBounds) + textBounds = lockScreenPaint.getTextBounds(text) + targetTextBounds = textBounds } if (!constrainedByHeight) { - val lastUnconstrainedHeight = textBounds.height() + lockScreenPaint.strokeWidth * 2 + val lastUnconstrainedHeight = textBounds.height + lockScreenPaint.strokeWidth * 2 fontSizeAdjustFactor = lastUnconstrainedHeight / lastUnconstrainedTextSize } @@ -607,8 +588,8 @@ open class SimpleDigitalClockTextView( for (i in 0..9) { val rectForCalculate = lockScreenPaint.getTextBounds("$i") - maxSingleDigitHeight = max(maxSingleDigitHeight, rectForCalculate.height()) - maxSingleDigitWidth = max(maxSingleDigitWidth, rectForCalculate.width()) + maxSingleDigitHeight = max(maxSingleDigitHeight, rectForCalculate.height) + maxSingleDigitWidth = max(maxSingleDigitWidth, rectForCalculate.width) } maxSingleDigitWidth += 2 * lockScreenPaint.strokeWidth maxSingleDigitHeight += 2 * lockScreenPaint.strokeWidth @@ -636,8 +617,8 @@ open class SimpleDigitalClockTextView( * and targetPaint will store the state we transition to */ private fun updateTextBoundsForTextAnimator() { - textAnimator.textInterpolator.basePaint.getTextBounds(text, prevTextBounds) - textAnimator.textInterpolator.targetPaint.getTextBounds(text, targetTextBounds) + prevTextBounds = textAnimator.textInterpolator.basePaint.getTextBounds(text) + targetTextBounds = textAnimator.textInterpolator.targetPaint.getTextBounds(text) } /** diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java index e26e19d27417..29647cd082b1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java @@ -16,12 +16,18 @@ package com.android.keyguard; +import static com.android.internal.widget.flags.Flags.FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; +import android.hardware.input.InputManager; +import android.platform.test.annotations.EnableFlags; import android.testing.TestableLooper.RunWithLooper; import android.view.View; import android.view.ViewGroup; @@ -90,6 +96,8 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { @Mock private UserActivityNotifier mUserActivityNotifier; private NumPadKey[] mButtons = new NumPadKey[]{}; + @Mock + private InputManager mInputManager; private KeyguardPinBasedInputViewController mKeyguardPinViewController; @@ -118,12 +126,13 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { new KeyguardKeyboardInteractor(new FakeKeyboardRepository()); FakeFeatureFlags featureFlags = new FakeFeatureFlags(); mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_REVAMPED_BOUNCER_MESSAGES); + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(false); mKeyguardPinViewController = new KeyguardPinBasedInputViewController(mPinBasedInputView, mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback, mKeyguardMessageAreaControllerFactory, mLatencyTracker, mEmergencyButtonController, mFalsingCollector, featureFlags, mSelectedUserInteractor, keyguardKeyboardInteractor, mBouncerHapticPlayer, - mUserActivityNotifier) { + mUserActivityNotifier, mInputManager) { @Override public void onResume(int reason) { super.onResume(reason); @@ -148,4 +157,112 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { mKeyguardPinViewController.resetState(); verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin); } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_addDevice_notKeyboard() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(false); + verify(mPasswordEntry, times(1)).setShowPassword(true); + mKeyguardPinViewController.onViewAttached(); + mKeyguardPinViewController.onInputDeviceAdded(1); + verify(mPasswordEntry, times(1)).setShowPassword(true); + } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_addDevice_keyboard() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true); + verify(mPasswordEntry, times(1)).setShowPassword(true); + mKeyguardPinViewController.onViewAttached(); + mKeyguardPinViewController.onInputDeviceAdded(1); + verify(mPasswordEntry, times(1)).setShowPassword(false); + } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_addDevice_multipleKeyboards() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true); + verify(mPasswordEntry, times(1)).setShowPassword(true); + mKeyguardPinViewController.onViewAttached(); + mKeyguardPinViewController.onInputDeviceAdded(1); + verify(mPasswordEntry, times(1)).setShowPassword(false); + mKeyguardPinViewController.onInputDeviceAdded(1); + verify(mPasswordEntry, times(1)).setShowPassword(false); + } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_removeDevice_notKeyboard() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(false); + verify(mPasswordEntry, times(1)).setShowPassword(true); + mKeyguardPinViewController.onViewAttached(); + mKeyguardPinViewController.onInputDeviceRemoved(1); + verify(mPasswordEntry, times(1)).setShowPassword(true); + } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_removeDevice_keyboard() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true, false); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(0)).setShowPassword(false); + mKeyguardPinViewController.onViewAttached(); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(1)).setShowPassword(false); + mKeyguardPinViewController.onInputDeviceRemoved(1); + verify(mPasswordEntry, times(2)).setShowPassword(true); + verify(mPasswordEntry, times(1)).setShowPassword(false); + } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_removeDevice_multipleKeyboards() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true, true); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(0)).setShowPassword(false); + mKeyguardPinViewController.onViewAttached(); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(1)).setShowPassword(false); + mKeyguardPinViewController.onInputDeviceRemoved(1); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(1)).setShowPassword(false); + } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_updateDevice_notKeyboard() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(false); + verify(mPasswordEntry, times(1)).setShowPassword(true); + mKeyguardPinViewController.onViewAttached(); + mKeyguardPinViewController.onInputDeviceChanged(1); + verify(mPasswordEntry, times(1)).setShowPassword(true); + } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_updateDevice_keyboard() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true, false); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(0)).setShowPassword(false); + mKeyguardPinViewController.onViewAttached(); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(1)).setShowPassword(false); + mKeyguardPinViewController.onInputDeviceChanged(1); + verify(mPasswordEntry, times(2)).setShowPassword(true); + verify(mPasswordEntry, times(1)).setShowPassword(false); + } + + @Test + @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void updateAnimations_updateDevice_multipleKeyboards() { + when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true, true); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(0)).setShowPassword(false); + mKeyguardPinViewController.onViewAttached(); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(1)).setShowPassword(false); + mKeyguardPinViewController.onInputDeviceChanged(1); + verify(mPasswordEntry, times(1)).setShowPassword(true); + verify(mPasswordEntry, times(1)).setShowPassword(false); + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index 142a2868ec14..7fea06ec7f41 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -16,6 +16,7 @@ package com.android.keyguard +import android.hardware.input.InputManager import android.testing.TestableLooper import android.view.View import android.view.ViewGroup @@ -104,6 +105,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { @Mock lateinit var enterButton: View @Mock lateinit var uiEventLogger: UiEventLogger @Mock lateinit var mUserActivityNotifier: UserActivityNotifier + @Mock lateinit var inputManager: InputManager @Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback> @@ -154,6 +156,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { keyguardKeyboardInteractor, kosmos.bouncerHapticPlayer, mUserActivityNotifier, + inputManager, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt index c751a7db51dc..003669da498e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt @@ -16,6 +16,7 @@ package com.android.keyguard +import android.hardware.input.InputManager import android.telephony.TelephonyManager import android.testing.TestableLooper import android.view.LayoutInflater @@ -73,6 +74,7 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { @Mock private lateinit var mUserActivityNotifier: UserActivityNotifier private val updateMonitorCallbackArgumentCaptor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) + @Mock private lateinit var inputManager: InputManager private val kosmos = testKosmos() @@ -107,6 +109,7 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { keyguardKeyboardInteractor, kosmos.bouncerHapticPlayer, mUserActivityNotifier, + inputManager, ) underTest.init() underTest.onViewAttached() diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt index c34682551eda..85cb388ace5c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt @@ -16,6 +16,7 @@ package com.android.keyguard +import android.hardware.input.InputManager import android.telephony.PinResult import android.telephony.TelephonyManager import android.testing.TestableLooper @@ -65,6 +66,7 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() { private lateinit var keyguardMessageAreaController: KeyguardMessageAreaController<BouncerKeyguardMessageArea> @Mock private lateinit var mUserActivityNotifier: UserActivityNotifier + @Mock private lateinit var inputManager: InputManager private val kosmos = testKosmos() @@ -102,6 +104,7 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() { keyguardKeyboardInteractor, kosmos.bouncerHapticPlayer, mUserActivityNotifier, + inputManager, ) underTest.init() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ExpandHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ExpandHelperTest.java index 7fb879c02778..f0c9141a76e6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ExpandHelperTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ExpandHelperTest.java @@ -32,7 +32,7 @@ import androidx.test.filters.SmallTest; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.animation.AnimatorTestRule; import com.android.systemui.flags.FakeFeatureFlagsClassic; -import com.android.systemui.statusbar.NotificationMediaManager; +import com.android.systemui.media.NotificationMediaManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java index 0b13900d826b..7e4704a6179a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java @@ -71,6 +71,7 @@ import com.android.systemui.bluetooth.qsdialog.DeviceItem; import com.android.systemui.bluetooth.qsdialog.DeviceItemType; import com.android.systemui.model.SysUiState; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.qs.shared.QSSettingsPackageRepository; import com.android.systemui.res.R; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.phone.SystemUIDialogManager; @@ -108,6 +109,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { private static final String TEST_LABEL = "label"; private static final int TEST_PRESET_INDEX = 1; private static final String TEST_PRESET_NAME = "test_preset"; + private static final String SETTINGS_PACKAGE_NAME = "com.android.settings"; private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); @Mock @@ -137,6 +139,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { @Mock private HearingDevicesUiEventLogger mUiEventLogger; @Mock + private QSSettingsPackageRepository mQSSettingsPackageRepository; + @Mock private CachedBluetoothDevice mCachedDevice; @Mock private BluetoothDevice mDevice; @@ -164,6 +168,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(List.of(mCachedDevice)); when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager); when(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState); + when(mQSSettingsPackageRepository.getSettingsPackageName()) + .thenReturn(SETTINGS_PACKAGE_NAME); when(mDevice.getBondState()).thenReturn(BOND_BONDED); when(mDevice.isConnected()).thenReturn(true); when(mCachedDevice.getDevice()).thenReturn(mDevice); @@ -195,6 +201,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { anyInt(), any()); assertThat(intentCaptor.getValue().getAction()).isEqualTo( Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS); + assertThat(intentCaptor.getValue().getPackage()).isEqualTo(SETTINGS_PACKAGE_NAME); verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_PAIR, TEST_LAUNCH_SOURCE_ID); } @@ -210,6 +217,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { anyInt(), any()); assertThat(intentCaptor.getValue().getAction()).isEqualTo( HearingDevicesDialogDelegate.ACTION_BLUETOOTH_DEVICE_DETAILS); + assertThat(intentCaptor.getValue().getPackage()).isEqualTo(SETTINGS_PACKAGE_NAME); verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_GEAR_CLICK, TEST_LAUNCH_SOURCE_ID); } @@ -392,7 +400,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { mExecutor, mExecutor, mAudioManager, - mUiEventLogger + mUiEventLogger, + mQSSettingsPackageRepository ); mDialog = mDialogDelegate.createDialog(); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt index 8d3640d8d809..53b364c13063 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt @@ -41,21 +41,9 @@ class FontVariationUtilsTest : SysuiTestCase() { @Test fun testStyleValueUnchange_getBlankStr() { val fontVariationUtils = FontVariationUtils() - fontVariationUtils.updateFontVariation( - weight = 100, - width = 100, - opticalSize = 0, - roundness = 100, - ) - val updatedFvar1 = - fontVariationUtils.updateFontVariation( - weight = 100, - width = 100, - opticalSize = 0, - roundness = 100, - ) - Assert.assertEquals("", updatedFvar1) - val updatedFvar2 = fontVariationUtils.updateFontVariation() - Assert.assertEquals("", updatedFvar2) + Assert.assertEquals("", fontVariationUtils.updateFontVariation()) + val fVar = fontVariationUtils.updateFontVariation(weight = 100) + Assert.assertEquals(fVar, fontVariationUtils.updateFontVariation()) + Assert.assertEquals(fVar, fontVariationUtils.updateFontVariation(weight = 100)) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt index 25a287c4cfff..15a6de896e92 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt @@ -247,20 +247,20 @@ class PinInputViewModelTest : SysuiTestCase() { } private class PinInputSubject -private constructor(metadata: FailureMetadata, private val actual: PinInputViewModel) : +private constructor(metadata: FailureMetadata, private val actual: PinInputViewModel?) : Subject(metadata, actual) { fun matches(mnemonics: String) { val actualMnemonics = - actual.input - .map { entry -> + actual?.input + ?.map { entry -> when (entry) { is Digit -> entry.input.digitToChar() is ClearAll -> 'C' else -> throw IllegalArgumentException() } } - .joinToString(separator = "") + ?.joinToString(separator = "") if (mnemonics != actualMnemonics) { failWithActual( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt index 0f400892f988..56b06de0a9ba 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt @@ -17,14 +17,12 @@ package com.android.systemui.common.ui.view +import android.testing.TestableLooper +import android.view.MotionEvent import android.view.ViewConfiguration import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel -import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Down -import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Move -import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Up import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -33,18 +31,22 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.never +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) class TouchHandlingViewInteractionHandlerTest : SysuiTestCase() { @Mock private lateinit var postDelayed: (Runnable, Long) -> DisposableHandle @Mock private lateinit var onLongPressDetected: (Int, Int) -> Unit @Mock private lateinit var onSingleTapDetected: (Int, Int) -> Unit + @Mock private lateinit var onDoubleTapDetected: () -> Unit private lateinit var underTest: TouchHandlingViewInteractionHandler @@ -61,14 +63,17 @@ class TouchHandlingViewInteractionHandlerTest : SysuiTestCase() { underTest = TouchHandlingViewInteractionHandler( + context = context, postDelayed = postDelayed, isAttachedToWindow = { isAttachedToWindow }, onLongPressDetected = onLongPressDetected, onSingleTapDetected = onSingleTapDetected, + onDoubleTapDetected = onDoubleTapDetected, longPressDuration = { ViewConfiguration.getLongPressTimeout().toLong() }, allowedTouchSlop = ViewConfiguration.getTouchSlop(), ) underTest.isLongPressHandlingEnabled = true + underTest.isDoubleTapHandlingEnabled = true } @Test @@ -76,63 +81,250 @@ class TouchHandlingViewInteractionHandlerTest : SysuiTestCase() { val downX = 123 val downY = 456 dispatchTouchEvents( - Down(x = downX, y = downY), - Move(distanceMoved = ViewConfiguration.getTouchSlop() - 0.1f), + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0), + MotionEvent.obtain( + 0L, + 0L, + MotionEvent.ACTION_MOVE, + 123f + ViewConfiguration.getTouchSlop() - 0.1f, + 456f, + 0, + ), ) delayedRunnable?.run() verify(onLongPressDetected).invoke(downX, downY) - verify(onSingleTapDetected, never()).invoke(any(), any()) + verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt()) } @Test fun longPressButFeatureNotEnabled() = runTest { underTest.isLongPressHandlingEnabled = false - dispatchTouchEvents(Down(x = 123, y = 456)) + dispatchTouchEvents(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0)) assertThat(delayedRunnable).isNull() - verify(onLongPressDetected, never()).invoke(any(), any()) - verify(onSingleTapDetected, never()).invoke(any(), any()) + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) + verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt()) } @Test fun longPressButViewNotAttached() = runTest { isAttachedToWindow = false - dispatchTouchEvents(Down(x = 123, y = 456)) + dispatchTouchEvents(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0)) delayedRunnable?.run() - verify(onLongPressDetected, never()).invoke(any(), any()) - verify(onSingleTapDetected, never()).invoke(any(), any()) + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) + verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt()) } @Test fun draggedTooFarToBeConsideredAlongPress() = runTest { dispatchTouchEvents( - Down(x = 123, y = 456), - Move(distanceMoved = ViewConfiguration.getTouchSlop() + 0.1f), + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123F, 456F, 0), + // Drag action within touch slop + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 123f, 456f, 0).apply { + addBatch(0L, 123f + ViewConfiguration.getTouchSlop() + 0.1f, 456f, 0f, 0f, 0) + }, ) assertThat(delayedRunnable).isNull() - verify(onLongPressDetected, never()).invoke(any(), any()) - verify(onSingleTapDetected, never()).invoke(any(), any()) + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) + verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt()) } @Test fun heldDownTooBrieflyToBeConsideredAlongPress() = runTest { dispatchTouchEvents( - Down(x = 123, y = 456), - Up( - distanceMoved = ViewConfiguration.getTouchSlop().toFloat(), - gestureDuration = ViewConfiguration.getLongPressTimeout() - 1L, + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0), + MotionEvent.obtain( + 0L, + ViewConfiguration.getLongPressTimeout() - 1L, + MotionEvent.ACTION_UP, + 123f, + 456F, + 0, ), ) assertThat(delayedRunnable).isNull() - verify(onLongPressDetected, never()).invoke(any(), any()) + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) verify(onSingleTapDetected).invoke(123, 456) } - private fun dispatchTouchEvents(vararg models: MotionEventModel) { - models.forEach { model -> underTest.onTouchEvent(model) } + @Test + fun doubleTap() = runTest { + val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L + dispatchTouchEvents( + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0), + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0), + MotionEvent.obtain( + secondTapTime, + secondTapTime, + MotionEvent.ACTION_DOWN, + 123f, + 456f, + 0, + ), + MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_UP, 123f, 456f, 0), + ) + + verify(onDoubleTapDetected).invoke() + assertThat(delayedRunnable).isNull() + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) + verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt()) + } + + @Test + fun doubleTapButFeatureNotEnabled() = runTest { + underTest.isDoubleTapHandlingEnabled = false + + val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L + dispatchTouchEvents( + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0), + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0), + MotionEvent.obtain( + secondTapTime, + secondTapTime, + MotionEvent.ACTION_DOWN, + 123f, + 456f, + 0, + ), + MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_UP, 123f, 456f, 0), + ) + + verify(onDoubleTapDetected, never()).invoke() + assertThat(delayedRunnable).isNull() + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) + verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt()) + } + + @Test + fun tapIntoLongPress() = runTest { + val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L + dispatchTouchEvents( + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0), + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0), + MotionEvent.obtain( + secondTapTime, + secondTapTime, + MotionEvent.ACTION_DOWN, + 123f, + 456f, + 0, + ), + MotionEvent.obtain( + secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L, + secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L, + MotionEvent.ACTION_MOVE, + 123f + ViewConfiguration.getTouchSlop() - 0.1f, + 456f, + 0, + ), + ) + delayedRunnable?.run() + + verify(onDoubleTapDetected, never()).invoke() + verify(onSingleTapDetected).invoke(anyInt(), anyInt()) + verify(onLongPressDetected).invoke(anyInt(), anyInt()) + } + + @Test + fun tapIntoDownHoldTooBrieflyToBeConsideredLongPress() = runTest { + val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L + dispatchTouchEvents( + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0), + MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 123f, 456f, 0), + MotionEvent.obtain( + secondTapTime, + secondTapTime, + MotionEvent.ACTION_DOWN, + 123f, + 456f, + 0, + ), + MotionEvent.obtain( + secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L, + secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L, + MotionEvent.ACTION_UP, + 123f, + 456f, + 0, + ), + ) + delayedRunnable?.run() + + verify(onDoubleTapDetected, never()).invoke() + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) + verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt()) + } + + @Test + fun tapIntoDrag() = runTest { + val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L + dispatchTouchEvents( + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0), + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0), + MotionEvent.obtain( + secondTapTime, + secondTapTime, + MotionEvent.ACTION_DOWN, + 123f, + 456f, + 0, + ), + // Drag event within touch slop + MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_MOVE, 123f, 456f, 0) + .apply { + addBatch( + secondTapTime, + 123f + ViewConfiguration.getTouchSlop() + 0.1f, + 456f, + 0f, + 0f, + 0, + ) + }, + ) + delayedRunnable?.run() + + verify(onDoubleTapDetected, never()).invoke() + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) + verify(onSingleTapDetected).invoke(anyInt(), anyInt()) + } + + @Test + fun doubleTapOutOfAllowableSlop() = runTest { + val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L + val scaledDoubleTapSlop = ViewConfiguration.get(context).scaledDoubleTapSlop + dispatchTouchEvents( + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0), + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0), + MotionEvent.obtain( + secondTapTime, + secondTapTime, + MotionEvent.ACTION_DOWN, + 123f + scaledDoubleTapSlop + 0.1f, + 456f + scaledDoubleTapSlop + 0.1f, + 0, + ), + MotionEvent.obtain( + secondTapTime, + secondTapTime, + MotionEvent.ACTION_UP, + 123f + scaledDoubleTapSlop + 0.1f, + 456f + scaledDoubleTapSlop + 0.1f, + 0, + ), + ) + + verify(onDoubleTapDetected, never()).invoke() + assertThat(delayedRunnable).isNull() + verify(onLongPressDetected, never()).invoke(anyInt(), anyInt()) + verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt()) + } + + private fun dispatchTouchEvents(vararg events: MotionEvent) { + events.forEach { event -> underTest.onTouchEvent(event) } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt index e0515000b232..454c15667f22 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt @@ -56,6 +56,7 @@ import com.android.systemui.statusbar.phone.dozeScrimController import com.android.systemui.statusbar.phone.screenOffAnimationController import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy @@ -105,7 +106,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { @Test fun nonPowerButtonFPS_vibrateSuccess() = testScope.runTest { - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC) runCurrent() enterDeviceFromFingerprintUnlockLegacy() @@ -116,7 +117,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { @Test fun powerButtonFPS_vibrateSuccess() = testScope.runTest { - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFingerprint(FingerprintSensorType.POWER_BUTTON) kosmos.fakeKeyEventRepository.setPowerButtonDown(false) @@ -133,7 +134,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { @Test fun powerButtonFPS_powerDown_doNotVibrateSuccess() = testScope.runTest { - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFingerprint(FingerprintSensorType.POWER_BUTTON) kosmos.fakeKeyEventRepository.setPowerButtonDown(true) // power button is currently DOWN @@ -150,7 +151,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { @Test fun powerButtonFPS_powerButtonRecentlyPressed_doNotVibrateSuccess() = testScope.runTest { - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFingerprint(FingerprintSensorType.POWER_BUTTON) kosmos.fakeKeyEventRepository.setPowerButtonDown(false) @@ -174,14 +175,14 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { } @Test - fun nonPowerButtonFPS_coExFaceFailure_vibrateError() = + fun nonPowerButtonFPS_coExFaceFailure_doNotVibrateError() = testScope.runTest { val playErrorHaptic by collectLastValue(underTest.playErrorHaptic) enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC) enrollFace() runCurrent() faceFailure() - assertThat(playErrorHaptic).isNotNull() + assertThat(playErrorHaptic).isNull() } @Test @@ -211,7 +212,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { testScope.runTest { kosmos.configureKeyguardBypass(isBypassAvailable = false) underTest = kosmos.deviceEntryHapticsInteractor - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC) runCurrent() configureDeviceEntryFromBiometricSource(isFpUnlock = true) @@ -225,7 +226,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { testScope.runTest { kosmos.configureKeyguardBypass(isBypassAvailable = false) underTest = kosmos.deviceEntryHapticsInteractor - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFingerprint(FingerprintSensorType.POWER_BUTTON) kosmos.fakeKeyEventRepository.setPowerButtonDown(false) @@ -246,18 +247,19 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { enrollFace() kosmos.configureKeyguardBypass(isBypassAvailable = true) underTest = kosmos.deviceEntryHapticsInteractor - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) configureDeviceEntryFromBiometricSource(isFaceUnlock = true) verifyDeviceEntryFromFaceAuth() assertThat(playSuccessHaptic).isNotNull() } + @OptIn(ExperimentalCoroutinesApi::class) @EnableSceneContainer @Test - fun playSuccessHaptic_onFaceAuthSuccess_whenBypassDisabled_sceneContainer() = + fun skipSuccessHaptic_onFaceAuthSuccess_whenBypassDisabled_sceneContainer() = testScope.runTest { underTest = kosmos.deviceEntryHapticsInteractor - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFace() kosmos.configureKeyguardBypass(isBypassAvailable = false) @@ -265,7 +267,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { configureDeviceEntryFromBiometricSource(isFaceUnlock = true, bypassEnabled = false) kosmos.fakeDeviceEntryFaceAuthRepository.isAuthenticated.value = true - assertThat(playSuccessHaptic).isNotNull() + assertThat(playSuccessHaptic).isNull() } @EnableSceneContainer @@ -274,7 +276,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { testScope.runTest { kosmos.configureKeyguardBypass(isBypassAvailable = false) underTest = kosmos.deviceEntryHapticsInteractor - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFingerprint(FingerprintSensorType.POWER_BUTTON) // power button is currently DOWN kosmos.fakeKeyEventRepository.setPowerButtonDown(true) @@ -295,7 +297,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { testScope.runTest { kosmos.configureKeyguardBypass(isBypassAvailable = false) underTest = kosmos.deviceEntryHapticsInteractor - val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + val playSuccessHaptic by collectLastValue(underTest.playSuccessHapticOnDeviceEntry) enrollFingerprint(FingerprintSensorType.POWER_BUTTON) kosmos.fakeKeyEventRepository.setPowerButtonDown(false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt index f37306276848..efc68f3b6884 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt @@ -57,4 +57,17 @@ class KeyEventInteractorTest : SysuiTestCase() { repository.setPowerButtonDown(true) assertThat(isPowerDown).isTrue() } + + @Test + fun testPowerButtonBeingLongPressedInteractor() = + runTest { + val isPowerButtonLongPressed by collectLastValue( + underTest.isPowerButtonLongPressed) + + repository.setPowerButtonLongPressed(false) + assertThat(isPowerButtonLongPressed).isFalse() + + repository.setPowerButtonLongPressed(true) + assertThat(isPowerButtonLongPressed).isTrue() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java index 909acca12551..573216dd680a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java @@ -49,9 +49,9 @@ import androidx.test.filters.SmallTest; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.SystemUIInitializerImpl; import com.android.systemui.SysuiTestCase; +import com.android.systemui.media.NotificationMediaManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.settings.UserTracker; -import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -177,7 +177,8 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { @Test public void schedulesAlarm12hBefore() { long in16Hours = System.currentTimeMillis() + TimeUnit.HOURS.toHours(16); - AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(in16Hours, null); + AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(in16Hours, + null); mProvider.onNextAlarmChanged(alarmClockInfo); long twelveHours = TimeUnit.HOURS.toMillis(KeyguardSliceProvider.ALARM_VISIBILITY_HOURS); @@ -189,7 +190,8 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { @Test public void updatingNextAlarmInvalidatesSlice() { long in16Hours = System.currentTimeMillis() + TimeUnit.HOURS.toHours(8); - AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(in16Hours, null); + AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(in16Hours, + null); mProvider.onNextAlarmChanged(alarmClockInfo); verify(mContentResolver).notifyChange(eq(mProvider.getUri()), eq(null)); @@ -204,7 +206,7 @@ public class KeyguardSliceProviderTest extends SysuiTestCase { @Test public void addZenMode_addedToSlice() { ListBuilder listBuilder = spy(new ListBuilder(getContext(), mProvider.getUri(), - ListBuilder.INFINITY)); + ListBuilder.INFINITY)); mProvider.addZenModeLocked(listBuilder); verify(listBuilder, never()).addRow(any(ListBuilder.RowBuilder.class)); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyEventRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyEventRepositoryTest.kt index c7f1525e2946..9ab5f8948ecc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyEventRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyEventRepositoryTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyevent.data.repository.KeyEventRepositoryImpl import com.android.systemui.statusbar.CommandQueue 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 @@ -34,10 +35,10 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock -import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class KeyEventRepositoryTest : SysuiTestCase() { @@ -62,6 +63,15 @@ class KeyEventRepositoryTest : SysuiTestCase() { } @Test + fun isPowerButtonBeingLongPressed_initialValueFalse() = + testScope.runTest { + val isPowerButtonLongPressed by collectLastValue( + underTest.isPowerButtonLongPressed) + runCurrent() + assertThat(isPowerButtonLongPressed).isFalse() + } + + @Test fun isPowerButtonDown_onChange() = testScope.runTest { val isPowerButtonDown by collectLastValue(underTest.isPowerButtonDown) @@ -77,4 +87,54 @@ class KeyEventRepositoryTest : SysuiTestCase() { ) assertThat(isPowerButtonDown).isFalse() } + + + @Test + fun isPowerButtonBeingLongPressed_onPowerButtonDown() = + testScope.runTest { + val isPowerButtonLongPressed by collectLastValue( + underTest.isPowerButtonLongPressed) + + runCurrent() + + verify(commandQueue).addCallback(commandQueueCallbacks.capture()) + + val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_POWER) + commandQueueCallbacks.value.handleSystemKey(keyEvent) + + assertThat(isPowerButtonLongPressed).isFalse() + } + + @Test + fun isPowerButtonBeingLongPressed_onPowerButtonUp() = + testScope.runTest { + val isPowerButtonLongPressed by collectLastValue( + underTest.isPowerButtonLongPressed) + + runCurrent() + + verify(commandQueue).addCallback(commandQueueCallbacks.capture()) + + val keyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_POWER) + commandQueueCallbacks.value.handleSystemKey(keyEvent) + + assertThat(isPowerButtonLongPressed).isFalse() + } + + @Test + fun isPowerButtonBeingLongPressed_onPowerButtonDown_longPressFlagSet() = + testScope.runTest { + val isPowerButtonBeingLongPressed by collectLastValue( + underTest.isPowerButtonLongPressed) + + runCurrent() + + verify(commandQueue).addCallback(commandQueueCallbacks.capture()) + + val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_POWER) + keyEvent.setFlags(KeyEvent.FLAG_LONG_PRESS) + commandQueueCallbacks.value.handleSystemKey(keyEvent) + + assertThat(isPowerButtonBeingLongPressed).isTrue() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractorTest.kt index 2558d583b001..89a53f5722ac 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractorTest.kt @@ -49,6 +49,7 @@ import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) +@EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) class KeyguardStateCallbackInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() @@ -81,7 +82,6 @@ class KeyguardStateCallbackInteractorTest : SysuiTestCase() { } @Test - @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun test_lockscreenVisibility_notifyDismissSucceeded_ifNotVisible() = testScope.runTest { underTest.addCallback(callback) @@ -109,7 +109,6 @@ class KeyguardStateCallbackInteractorTest : SysuiTestCase() { } @Test - @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun test_lockscreenVisibility_reportsKeyguardShowingChanged() = testScope.runTest { underTest.addCallback(callback) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt index e203a276a2f2..1dddfc1bba9c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt @@ -18,11 +18,17 @@ package com.android.systemui.keyguard.domain.interactor import android.content.Intent +import android.os.PowerManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.provider.Settings import android.view.accessibility.accessibilityManagerWrapper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.logging.uiEventLogger +import com.android.systemui.Flags.FLAG_DOUBLE_TAP_TO_SLEEP import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor @@ -39,6 +45,8 @@ import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository +import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.advanceTimeBy @@ -46,14 +54,19 @@ 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 import org.junit.runner.RunWith +import org.mockito.Mock import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) +@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().apply { @@ -61,17 +74,23 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { this.uiEventLogger = mock<UiEventLoggerFake>() } + @get:Rule val setFlagsRule = SetFlagsRule() + private lateinit var underTest: KeyguardTouchHandlingInteractor private val logger = kosmos.uiEventLogger private val testScope = kosmos.testScope private val keyguardRepository = kosmos.fakeKeyguardRepository private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + private val secureSettingsRepository = kosmos.userAwareSecureSettingsRepository + + @Mock private lateinit var powerManager: PowerManager @Before fun setUp() { MockitoAnnotations.initMocks(this) overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, true) + overrideResource(com.android.internal.R.bool.config_supportDoubleTapSleep, true) whenever(kosmos.accessibilityManagerWrapper.getRecommendedTimeoutMillis(anyInt(), anyInt())) .thenAnswer { it.arguments[0] } @@ -80,13 +99,13 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { @After fun tearDown() { - mContext - .getOrCreateTestableResources() - .removeOverride(R.bool.long_press_keyguard_customize_lockscreen_enabled) + val testableResource = mContext.getOrCreateTestableResources() + testableResource.removeOverride(R.bool.long_press_keyguard_customize_lockscreen_enabled) + testableResource.removeOverride(com.android.internal.R.bool.config_supportDoubleTapSleep) } @Test - fun isEnabled() = + fun isLongPressEnabled() = testScope.runTest { val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled) KeyguardState.values().forEach { keyguardState -> @@ -101,7 +120,7 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { } @Test - fun isEnabled_alwaysFalseWhenQuickSettingsAreVisible() = + fun isLongPressEnabled_alwaysFalseWhenQuickSettingsAreVisible() = testScope.runTest { val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled) KeyguardState.values().forEach { keyguardState -> @@ -112,7 +131,7 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { } @Test - fun isEnabled_alwaysFalseWhenConfigEnabledBooleanIsFalse() = + fun isLongPressEnabled_alwaysFalseWhenConfigEnabledBooleanIsFalse() = testScope.runTest { overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, false) createUnderTest() @@ -294,6 +313,119 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { assertThat(isMenuVisible).isFalse() } + @Test + @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP) + fun isDoubleTapEnabled_flagEnabled_userSettingEnabled_onlyTrueInLockScreenState() { + testScope.runTest { + secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true) + + val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled) + KeyguardState.entries.forEach { keyguardState -> + setUpState(keyguardState = keyguardState) + + if (keyguardState == KeyguardState.LOCKSCREEN) { + assertThat(isEnabled()).isTrue() + } else { + assertThat(isEnabled()).isFalse() + } + } + } + } + + @Test + @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP) + fun isDoubleTapEnabled_flagEnabled_userSettingDisabled_alwaysFalse() { + testScope.runTest { + secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, false) + + val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled) + KeyguardState.entries.forEach { keyguardState -> + setUpState(keyguardState = keyguardState) + + assertThat(isEnabled()).isFalse() + } + } + } + + @Test + @DisableFlags(FLAG_DOUBLE_TAP_TO_SLEEP) + fun isDoubleTapEnabled_flagDisabled_userSettingEnabled_alwaysFalse() { + testScope.runTest { + secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true) + + val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled) + KeyguardState.entries.forEach { keyguardState -> + setUpState(keyguardState = keyguardState) + + assertThat(isEnabled()).isFalse() + } + } + } + + + + @Test + @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP) + fun isDoubleTapEnabled_flagEnabledAndConfigDisabled_alwaysFalse() { + testScope.runTest { + secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true) + overrideResource(com.android.internal.R.bool.config_supportDoubleTapSleep, false) + createUnderTest() + + val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled) + KeyguardState.entries.forEach { keyguardState -> + setUpState(keyguardState = keyguardState) + + assertThat(isEnabled()).isFalse() + } + } + } + + @Test + @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP) + fun isDoubleTapEnabled_quickSettingsVisible_alwaysFalse() { + testScope.runTest { + secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true) + + val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled) + KeyguardState.entries.forEach { keyguardState -> + setUpState(keyguardState = keyguardState, isQuickSettingsVisible = true) + + assertThat(isEnabled()).isFalse() + } + } + } + + @Test + @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP) + fun onDoubleClick_doubleTapEnabled() { + testScope.runTest { + secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true) + val isEnabled by collectLastValue(underTest.isDoubleTapHandlingEnabled) + runCurrent() + + underTest.onDoubleClick() + + assertThat(isEnabled).isTrue() + verify(powerManager).goToSleep(anyLong()) + } + } + + @Test + @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP) + fun onDoubleClick_doubleTapDisabled() { + testScope.runTest { + secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, false) + val isEnabled by collectLastValue(underTest.isDoubleTapHandlingEnabled) + runCurrent() + + underTest.onDoubleClick() + + assertThat(isEnabled).isFalse() + verify(powerManager, never()).goToSleep(anyLong()) + } + } + private suspend fun createUnderTest(isRevampedWppFeatureEnabled: Boolean = true) { // This needs to be re-created for each test outside of kosmos since the flag values are // read during initialization to set up flows. Maybe there is a better way to handle that. @@ -309,6 +441,9 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { accessibilityManager = kosmos.accessibilityManagerWrapper, pulsingGestureListener = kosmos.pulsingGestureListener, faceAuthInteractor = kosmos.deviceEntryFaceAuthInteractor, + secureSettingsRepository = secureSettingsRepository, + powerManager = powerManager, + systemClock = kosmos.fakeSystemClock, ) setUpState() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt index 8533134fd94e..95a6e56717fa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt @@ -21,15 +21,20 @@ import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_NOTIFICATION_SHADE_BLUR import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.flags.BrokenWithSceneContainer +import com.android.systemui.flags.DisableSceneContainer 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.keyguard.ui.transitions.blurConfig import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -43,6 +48,7 @@ class OccludedToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameterizati SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope + private val repository = kosmos.fakeKeyguardTransitionRepository private lateinit var underTest: OccludedToPrimaryBouncerTransitionViewModel companion object { @@ -63,6 +69,25 @@ class OccludedToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameterizati } @Test + @DisableSceneContainer + fun lockscreenAlphaImmediatelyToZero() = + testScope.runTest { + val alpha by collectLastValue(underTest.lockscreenAlpha) + + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) + runCurrent() + assertThat(alpha).isEqualTo(0f) + + repository.sendTransitionStep(step(0.1f, TransitionState.RUNNING)) + runCurrent() + assertThat(alpha).isEqualTo(0f) + + repository.sendTransitionStep(step(1f, TransitionState.FINISHED)) + runCurrent() + assertThat(alpha).isEqualTo(0f) + } + + @Test @BrokenWithSceneContainer(388068805) fun notificationsAreBlurredImmediatelyWhenBouncerIsOpenedAndShadeIsExpanded() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/log/LogWtfHandlerRuleTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/log/LogWtfHandlerRuleTest.kt new file mode 100644 index 000000000000..d5d256e5cd97 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/log/LogWtfHandlerRuleTest.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.log + +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.model.Statement +import org.mockito.kotlin.mock + +@RunWith(AndroidJUnit4::class) +@SmallTest +class LogWtfHandlerRuleTest : SysuiTestCase() { + + val underTest = LogWtfHandlerRule() + + @Test + fun passingTestWithoutWtf_shouldPass() { + val result = runTestCodeWithRule { + Log.e(TAG, "just an error", IndexOutOfBoundsException()) + } + assertThat(result.isSuccess).isTrue() + } + + @Test + fun passingTestWithWtf_shouldFail() { + val result = runTestCodeWithRule { + Log.wtf(TAG, "some terrible failure", IllegalStateException()) + } + assertThat(result.isFailure).isTrue() + val exception = result.exceptionOrNull() + assertThat(exception).isInstanceOf(AssertionError::class.java) + assertThat(exception?.cause).isInstanceOf(Log.TerribleFailure::class.java) + assertThat(exception?.cause?.cause).isInstanceOf(IllegalStateException::class.java) + } + + @Test + fun failingTestWithoutWtf_shouldFail() { + val result = runTestCodeWithRule { + Log.e(TAG, "just an error", IndexOutOfBoundsException()) + throw NullPointerException("some npe") + } + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(NullPointerException::class.java) + } + + @Test + fun failingTestWithWtf_shouldFail() { + val result = runTestCodeWithRule { + Log.wtf(TAG, "some terrible failure", IllegalStateException()) + throw NullPointerException("some npe") + } + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(NullPointerException::class.java) + val suppressedExceptions = result.exceptionOrNull()!!.suppressedExceptions + assertThat(suppressedExceptions).hasSize(1) + val suppressed = suppressedExceptions.first() + assertThat(suppressed).isInstanceOf(AssertionError::class.java) + assertThat(suppressed.cause).isInstanceOf(Log.TerribleFailure::class.java) + assertThat(suppressed.cause?.cause).isInstanceOf(IllegalStateException::class.java) + } + + @Test + fun passingTestWithExemptWtf_shouldPass() { + underTest.addFailureLogExemption { it.tag == TAG_EXPECTED } + val result = runTestCodeWithRule { + Log.wtf(TAG_EXPECTED, "some expected failure", IllegalStateException()) + } + assertThat(result.isSuccess).isTrue() + } + + @Test + fun failingTestWithExemptWtf_shouldFail() { + underTest.addFailureLogExemption { it.tag == TAG_EXPECTED } + val result = runTestCodeWithRule { + Log.wtf(TAG_EXPECTED, "some expected failure", IllegalStateException()) + throw NullPointerException("some npe") + } + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(NullPointerException::class.java) + val suppressedExceptions = result.exceptionOrNull()!!.suppressedExceptions + assertThat(suppressedExceptions).isEmpty() + } + + @Test + fun passingTestWithOneExemptWtfOfTwo_shouldFail() { + underTest.addFailureLogExemption { it.tag == TAG_EXPECTED } + val result = runTestCodeWithRule { + Log.wtf(TAG_EXPECTED, "some expected failure", IllegalStateException()) + Log.wtf(TAG, "some terrible failure", IllegalStateException()) + } + assertThat(result.isFailure).isTrue() + val exception = result.exceptionOrNull() + assertThat(exception).isInstanceOf(AssertionError::class.java) + assertThat(exception?.cause).isInstanceOf(Log.TerribleFailure::class.java) + assertThat(exception?.cause?.cause).isInstanceOf(IllegalStateException::class.java) + } + + @Test + fun failingTestWithOneExemptWtfOfTwo_shouldFail() { + underTest.addFailureLogExemption { it.tag == TAG_EXPECTED } + val result = runTestCodeWithRule { + Log.wtf(TAG_EXPECTED, "some expected failure", IllegalStateException()) + Log.wtf(TAG, "some terrible failure", IllegalStateException()) + throw NullPointerException("some npe") + } + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(NullPointerException::class.java) + val suppressedExceptions = result.exceptionOrNull()!!.suppressedExceptions + assertThat(suppressedExceptions).hasSize(1) + val suppressed = suppressedExceptions.first() + assertThat(suppressed).isInstanceOf(AssertionError::class.java) + assertThat(suppressed.cause).isInstanceOf(Log.TerribleFailure::class.java) + assertThat(suppressed.cause?.cause).isInstanceOf(IllegalStateException::class.java) + } + + private fun runTestCodeWithRule(testCode: () -> Unit): Result<Unit> { + val testCodeStatement = + object : Statement() { + override fun evaluate() { + testCode() + } + } + val wrappedTest = underTest.apply(testCodeStatement, mock()) + return try { + wrappedTest.evaluate() + Result.success(Unit) + } catch (e: Throwable) { + Result.failure(e) + } + } + + companion object { + const val TAG = "LogWtfHandlerRuleTest" + const val TAG_EXPECTED = "EXPECTED" + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/NotificationMediaManagerTest.kt index a7fe586cbfa5..10b00857e887 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/NotificationMediaManagerTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.statusbar +package com.android.systemui.media import android.media.MediaMetadata import android.media.session.MediaController diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java index 2db2199602b8..9c4d93c17d00 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java @@ -48,10 +48,12 @@ import androidx.test.filters.SmallTest; import com.android.media.flags.Flags; import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; +import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.SysuiTestCase; import com.android.systemui.res.R; import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListeningExecutorService; import org.junit.Before; import org.junit.Test; @@ -61,6 +63,7 @@ import org.mockito.Captor; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executor; import java.util.stream.Collectors; @SmallTest @@ -95,6 +98,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase { private List<MediaDevice> mMediaDevices = new ArrayList<>(); private List<MediaItem> mMediaItems = new ArrayList<>(); MediaOutputSeekbar mSpyMediaOutputSeekbar; + Executor mMainExecutor = mContext.getMainExecutor(); + ListeningExecutorService mBackgroundExecutor = ThreadUtils.getBackgroundExecutor(); @Before public void setUp() { @@ -108,6 +113,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase { when(mMediaSwitchingController.getSessionVolumeMax()).thenReturn(TEST_MAX_VOLUME); when(mMediaSwitchingController.getSessionVolume()).thenReturn(TEST_CURRENT_VOLUME); when(mMediaSwitchingController.getSessionName()).thenReturn(TEST_SESSION_NAME); + when(mMediaSwitchingController.getColorSchemeLegacy()).thenReturn( + mock(MediaOutputColorSchemeLegacy.class)); when(mIconCompat.toIcon(mContext)).thenReturn(mIcon); when(mMediaDevice1.getName()).thenReturn(TEST_DEVICE_NAME_1); when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_ID_1); @@ -122,7 +129,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase { mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice1, true)); mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice2, false)); - mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor, + mBackgroundExecutor); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); @@ -148,7 +156,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase { @Test public void onBindViewHolder_bindPairNew_verifyView() { - mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor, + mBackgroundExecutor); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); @@ -173,7 +182,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase { .map((item) -> item.getMediaDevice().get()) .collect(Collectors.toList())); when(mMediaSwitchingController.getSessionName()).thenReturn(TEST_SESSION_NAME); - mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor, + mBackgroundExecutor); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); @@ -195,7 +205,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase { .map((item) -> item.getMediaDevice().get()) .collect(Collectors.toList())); when(mMediaSwitchingController.getSessionName()).thenReturn(null); - mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor, + mBackgroundExecutor); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); @@ -665,7 +676,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase { @Test public void onItemClick_clickPairNew_verifyLaunchBluetoothPairing() { - mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor, + mBackgroundExecutor); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); @@ -683,7 +695,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase { assertThat(mMediaDevice2.getState()).isEqualTo( LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED); when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER); - mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor, + mBackgroundExecutor); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); @@ -701,7 +714,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase { assertThat(mMediaDevice2.getState()).isEqualTo( LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED); when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER); - mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, + mContext.getMainExecutor(), ThreadUtils.getBackgroundExecutor()); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); @@ -723,7 +737,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase { when(mMediaDevice2.getState()).thenReturn( LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED); when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP); - mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor, + mBackgroundExecutor); mMediaOutputAdapter.updateItems(); mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); @@ -778,6 +793,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase { assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2); + assertThat(mViewHolder.mTitleText.getAlpha()) + .isEqualTo(MediaOutputAdapterLegacy.DEVICE_ACTIVE_ALPHA); assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue(); mViewHolder.mContainerLayout.performClick(); @@ -799,6 +816,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase { assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2); + assertThat(mViewHolder.mTitleText.getAlpha()) + .isEqualTo(MediaOutputAdapterLegacy.DEVICE_ACTIVE_ALPHA); assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue(); mViewHolder.mContainerLayout.performClick(); @@ -820,6 +839,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase { assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2); + assertThat(mViewHolder.mTitleText.getAlpha()) + .isEqualTo(MediaOutputAdapterLegacy.DEVICE_ACTIVE_ALPHA); assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue(); mViewHolder.mContainerLayout.performClick(); @@ -841,6 +862,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase { assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2); + assertThat(mViewHolder.mTitleText.getAlpha()) + .isEqualTo(MediaOutputAdapterLegacy.DEVICE_ACTIVE_ALPHA); assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue(); mViewHolder.mContainerLayout.performClick(); @@ -862,6 +885,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase { assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2); + assertThat(mViewHolder.mTitleText.getAlpha()) + .isEqualTo(MediaOutputAdapterLegacy.DEVICE_ACTIVE_ALPHA); assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue(); mViewHolder.mContainerLayout.performClick(); @@ -883,6 +908,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase { assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2); + assertThat(mViewHolder.mTitleText.getAlpha()) + .isEqualTo(MediaOutputAdapterLegacy.DEVICE_DISABLED_ALPHA); assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue(); mViewHolder.mContainerLayout.performClick(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelTest.kt index 3029928f070f..cb13b118fd68 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelTest.kt @@ -25,6 +25,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.app.iUriGrantsManager import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.ui.viewmodel.iconProvider import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest @@ -32,6 +33,8 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.external.TileData +import com.android.systemui.qs.panels.ui.viewmodel.IconProvider +import com.android.systemui.qs.panels.ui.viewmodel.toIconProvider import com.android.systemui.qs.panels.ui.viewmodel.toUiState import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon @@ -80,28 +83,32 @@ class TileRequestDialogViewModelTest : SysuiTestCase() { @Test fun uiState_beforeActivation_hasDefaultIcon_andCorrectData() = kosmos.runTest { - val expectedState = - baseResultLegacyState.apply { icon = defaultIcon }.toUiState(mainResources) + val state = baseResultLegacyState.apply { icon = defaultIcon } + + val expectedState = state.toUiState(mainResources) + val expectedIconProvider = state.toIconProvider() with(underTest.uiState) { expect.that(label).isEqualTo(TEST_LABEL) expect.that(secondaryLabel).isEmpty() - expect.that(state).isEqualTo(expectedState.state) + expect.that(this.state).isEqualTo(expectedState.state) expect.that(handlesLongClick).isFalse() expect.that(handlesSecondaryClick).isFalse() - expect.that(icon).isEqualTo(defaultIcon) expect.that(sideDrawable).isNull() expect.that(accessibilityUiState).isEqualTo(expectedState.accessibilityUiState) } + + expect.that(underTest.iconProvider).isEqualTo(expectedIconProvider) } @Test fun uiState_afterActivation_hasCorrectIcon_andCorrectData() = kosmos.runTest { - val expectedState = - baseResultLegacyState - .apply { icon = QSTileImpl.DrawableIcon(loadedDrawable) } - .toUiState(mainResources) + val state = + baseResultLegacyState.apply { icon = QSTileImpl.DrawableIcon(loadedDrawable) } + + val expectedState = state.toUiState(mainResources) + val expectedIconProvider = state.toIconProvider() underTest.activateIn(testScope) runCurrent() @@ -109,13 +116,13 @@ class TileRequestDialogViewModelTest : SysuiTestCase() { with(underTest.uiState) { expect.that(label).isEqualTo(TEST_LABEL) expect.that(secondaryLabel).isEmpty() - expect.that(state).isEqualTo(expectedState.state) + expect.that(this.state).isEqualTo(expectedState.state) expect.that(handlesLongClick).isFalse() expect.that(handlesSecondaryClick).isFalse() - expect.that(icon).isEqualTo(QSTileImpl.DrawableIcon(loadedDrawable)) expect.that(sideDrawable).isNull() expect.that(accessibilityUiState).isEqualTo(expectedState.accessibilityUiState) } + expect.that(underTest.iconProvider).isEqualTo(expectedIconProvider) } @Test @@ -135,7 +142,7 @@ class TileRequestDialogViewModelTest : SysuiTestCase() { underTest.activateIn(testScope) runCurrent() - assertThat(underTest.uiState.icon).isEqualTo(defaultIcon) + assertThat(underTest.iconProvider).isEqualTo(IconProvider.ConstantIcon(defaultIcon)) } companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt index 68a591dd075f..1d42424bc6ed 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt @@ -16,11 +16,9 @@ package com.android.systemui.qs.panels.ui.viewmodel - import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.qs.FakeQSTile import com.android.systemui.qs.pipeline.data.repository.tileSpecRepository @@ -48,45 +46,43 @@ class DetailsViewModelTest : SysuiTestCase() { } @Test - fun changeTileDetailsViewModel() = with(kosmos) { - testScope.runTest { - val specs = listOf( - spec, - specNoDetails, - ) - tileSpecRepository.setTiles(0, specs) - runCurrent() + fun changeTileDetailsViewModel() = + with(kosmos) { + testScope.runTest { + val specs = listOf(spec, specNoDetails) + tileSpecRepository.setTiles(0, specs) + runCurrent() - val tiles = currentTilesInteractor.currentTiles.value + val tiles = currentTilesInteractor.currentTiles.value - assertThat(currentTilesInteractor.currentTilesSpecs.size).isEqualTo(2) - assertThat(tiles[1].spec).isEqualTo(specNoDetails) - (tiles[1].tile as FakeQSTile).hasDetailsViewModel = false + assertThat(currentTilesInteractor.currentTilesSpecs.size).isEqualTo(2) + assertThat(tiles[1].spec).isEqualTo(specNoDetails) + (tiles[1].tile as FakeQSTile).hasDetailsViewModel = false - assertThat(underTest.activeTileDetails).isNull() + assertThat(underTest.activeTileDetails).isNull() - // Click on the tile who has the `spec`. - assertThat(underTest.onTileClicked(spec)).isTrue() - assertThat(underTest.activeTileDetails).isNotNull() - assertThat(underTest.activeTileDetails?.getTitle()).isEqualTo("internet") + // Click on the tile who has the `spec`. + assertThat(underTest.onTileClicked(spec)).isTrue() + assertThat(underTest.activeTileDetails).isNotNull() + assertThat(underTest.activeTileDetails?.title).isEqualTo("internet") - // Click on a tile who dose not have a valid spec. - assertThat(underTest.onTileClicked(null)).isFalse() - assertThat(underTest.activeTileDetails).isNull() + // Click on a tile who dose not have a valid spec. + assertThat(underTest.onTileClicked(null)).isFalse() + assertThat(underTest.activeTileDetails).isNull() - // Click again on the tile who has the `spec`. - assertThat(underTest.onTileClicked(spec)).isTrue() - assertThat(underTest.activeTileDetails).isNotNull() - assertThat(underTest.activeTileDetails?.getTitle()).isEqualTo("internet") + // Click again on the tile who has the `spec`. + assertThat(underTest.onTileClicked(spec)).isTrue() + assertThat(underTest.activeTileDetails).isNotNull() + assertThat(underTest.activeTileDetails?.title).isEqualTo("internet") - // Click on a tile who dose not have a detailed view. - assertThat(underTest.onTileClicked(specNoDetails)).isFalse() - assertThat(underTest.activeTileDetails).isNull() + // Click on a tile who dose not have a detailed view. + assertThat(underTest.onTileClicked(specNoDetails)).isFalse() + assertThat(underTest.activeTileDetails).isNull() - underTest.closeDetailedView() - assertThat(underTest.activeTileDetails).isNull() + underTest.closeDetailedView() + assertThat(underTest.activeTileDetails).isNull() - assertThat(underTest.onTileClicked(null)).isFalse() + assertThat(underTest.onTileClicked(null)).isFalse() + } } - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/IconProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/IconProviderTest.kt new file mode 100644 index 000000000000..7257a89c214b --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/IconProviderTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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 android.graphics.drawable.TestStubDrawable +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.qs.tileimpl.QSTileImpl +import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon +import com.android.systemui.res.R +import com.google.common.truth.Truth.assertThat +import java.util.function.Supplier +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class IconProviderTest : SysuiTestCase() { + + @Test + fun iconAndSupplier_prefersIcon() { + val state = + QSTile.State().apply { + icon = ResourceIcon.get(R.drawable.android) + iconSupplier = Supplier { QSTileImpl.DrawableIcon(TestStubDrawable()) } + } + val iconProvider = state.toIconProvider() + + assertThat(iconProvider).isEqualTo(IconProvider.ConstantIcon(state.icon)) + } + + @Test + fun iconOnly_hasIcon() { + val state = QSTile.State().apply { icon = ResourceIcon.get(R.drawable.android) } + val iconProvider = state.toIconProvider() + + assertThat(iconProvider).isEqualTo(IconProvider.ConstantIcon(state.icon)) + } + + @Test + fun supplierOnly_hasIcon() { + val state = + QSTile.State().apply { + iconSupplier = Supplier { ResourceIcon.get(R.drawable.android) } + } + val iconProvider = state.toIconProvider() + + assertThat(iconProvider).isEqualTo(IconProvider.IconSupplier(state.iconSupplier)) + } + + @Test + fun noIconOrSupplier_iconNull() { + val state = QSTile.State() + val iconProvider = state.toIconProvider() + + assertThat(iconProvider).isEqualTo(IconProvider.Empty) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiStateTest.kt index 9c8e3225f3a4..b144f0678471 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiStateTest.kt @@ -18,7 +18,6 @@ package com.android.systemui.qs.panels.ui.viewmodel import android.content.res.Resources import android.content.res.mainResources -import android.graphics.drawable.TestStubDrawable import android.service.quicksettings.Tile import android.widget.Button import android.widget.Switch @@ -27,12 +26,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.plugins.qs.QSTile -import com.android.systemui.qs.tileimpl.QSTileImpl -import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon import com.android.systemui.res.R import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat -import java.util.function.Supplier import org.junit.Test import org.junit.runner.RunWith @@ -267,45 +263,6 @@ class TileUiStateTest : SysuiTestCase() { .contains(resources.getString(R.string.tile_unavailable)) } - @Test - fun iconAndSupplier_prefersIcon() { - val state = - QSTile.State().apply { - icon = ResourceIcon.get(R.drawable.android) - iconSupplier = Supplier { QSTileImpl.DrawableIcon(TestStubDrawable()) } - } - val uiState = state.toUiState() - - assertThat(uiState.icon).isEqualTo(state.icon) - } - - @Test - fun iconOnly_hasIcon() { - val state = QSTile.State().apply { icon = ResourceIcon.get(R.drawable.android) } - val uiState = state.toUiState() - - assertThat(uiState.icon).isEqualTo(state.icon) - } - - @Test - fun supplierOnly_hasIcon() { - val state = - QSTile.State().apply { - iconSupplier = Supplier { ResourceIcon.get(R.drawable.android) } - } - val uiState = state.toUiState() - - assertThat(uiState.icon).isEqualTo(state.iconSupplier.get()) - } - - @Test - fun noIconOrSupplier_iconNull() { - val state = QSTile.State() - val uiState = state.toUiState() - - assertThat(uiState.icon).isNull() - } - private fun QSTile.State.toUiState() = toUiState(resources) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt index c3089761effc..5bde7ad27b7a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt @@ -691,11 +691,11 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() { var currentModel: TileDetailsViewModel? = null val setCurrentModel = { model: TileDetailsViewModel? -> currentModel = model } tiles!![0].tile.getDetailsViewModel(setCurrentModel) - assertThat(currentModel?.getTitle()).isEqualTo("a") + assertThat(currentModel?.title).isEqualTo("a") currentModel = null tiles!![1].tile.getDetailsViewModel(setCurrentModel) - assertThat(currentModel?.getTitle()).isEqualTo("b") + assertThat(currentModel?.title).isEqualTo("b") currentModel = null tiles!![2].tile.getDetailsViewModel(setCurrentModel) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt index 00ee1c36590c..1b497a2b36ed 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt @@ -24,6 +24,7 @@ import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager import com.android.systemui.accessibility.hearingaid.HearingDevicesUiEventLogger.Companion.LAUNCH_SOURCE_QS_TILE import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope +import com.android.systemui.qs.shared.QSSettingsPackageRepository import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx @@ -40,6 +41,7 @@ import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @SmallTest @EnabledOnRavenwood @@ -53,14 +55,18 @@ class HearingDevicesTileUserActionInteractorTest : SysuiTestCase() { @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule() @Mock private lateinit var dialogManager: HearingDevicesDialogManager + @Mock private lateinit var settingsPackageRepository: QSSettingsPackageRepository @Before fun setUp() { + whenever(settingsPackageRepository.getSettingsPackageName()) + .thenReturn(SETTINGS_PACKAGE_NAME) underTest = HearingDevicesTileUserActionInteractor( testScope.coroutineContext, inputHandler, dialogManager, + settingsPackageRepository, ) } @@ -91,6 +97,11 @@ class HearingDevicesTileUserActionInteractorTest : SysuiTestCase() { QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { assertThat(it.intent.action).isEqualTo(Settings.ACTION_HEARING_DEVICES_SETTINGS) + assertThat(it.intent.`package`).isEqualTo(SETTINGS_PACKAGE_NAME) } } + + companion object { + private const val SETTINGS_PACKAGE_NAME = "com.android.settings" + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 9adf24f32c0c..1743e056b65c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -863,7 +863,7 @@ class SceneContainerStartableTest : SysuiTestCase() { whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by - collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry) setupBiometricAuth(hasUdfps = true) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) @@ -885,7 +885,7 @@ class SceneContainerStartableTest : SysuiTestCase() { whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by - collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry) setupBiometricAuth(hasUdfps = true) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) @@ -907,7 +907,7 @@ class SceneContainerStartableTest : SysuiTestCase() { whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by - collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry) setupBiometricAuth(hasSfps = true) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) @@ -930,7 +930,7 @@ class SceneContainerStartableTest : SysuiTestCase() { whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by - collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry) setupBiometricAuth(hasSfps = true) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) @@ -1033,7 +1033,7 @@ class SceneContainerStartableTest : SysuiTestCase() { whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by - collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry) setupBiometricAuth(hasSfps = true) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) @@ -1056,7 +1056,7 @@ class SceneContainerStartableTest : SysuiTestCase() { whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by - collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry) setupBiometricAuth(hasSfps = true) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) @@ -1079,7 +1079,7 @@ class SceneContainerStartableTest : SysuiTestCase() { whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by - collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry) setupBiometricAuth(hasSfps = true) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) @@ -1102,7 +1102,7 @@ class SceneContainerStartableTest : SysuiTestCase() { whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by - collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + collectLastValue(deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry) setupBiometricAuth(hasSfps = true) assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) @@ -1160,7 +1160,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) - fun playsFaceErrorHaptics_nonSfps_coEx() = + fun skipsFaceErrorHaptics_nonSfps_coEx() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic) @@ -1172,15 +1172,14 @@ class SceneContainerStartableTest : SysuiTestCase() { underTest.start() updateFaceAuthStatus(isSuccess = false) - assertThat(playErrorHaptic).isNotNull() - assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) - verify(vibratorHelper).vibrateAuthError(anyString()) + assertThat(playErrorHaptic).isNull() + verify(vibratorHelper, never()).vibrateAuthError(anyString()) verify(vibratorHelper, never()).vibrateAuthSuccess(anyString()) } @Test @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) - fun playsMSDLFaceErrorHaptics_nonSfps_coEx() = + fun skipsMSDLFaceErrorHaptics_nonSfps_coEx() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic) @@ -1192,10 +1191,9 @@ class SceneContainerStartableTest : SysuiTestCase() { underTest.start() updateFaceAuthStatus(isSuccess = false) - assertThat(playErrorHaptic).isNotNull() - assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) - assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.FAILURE) - assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties) + assertThat(playErrorHaptic).isNull() + assertThat(msdlPlayer.latestTokenPlayed).isNull() + assertThat(msdlPlayer.latestPropertiesPlayed).isNull() } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index 3407cd50e76f..4a304071ee97 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -41,7 +41,6 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Rect; import android.platform.test.flag.junit.FlagsParameterization; -import android.provider.Settings; import android.testing.TestableLooper.RunWithLooper; import android.view.View; import android.view.WindowManager; @@ -71,7 +70,6 @@ import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; -import com.android.systemui.util.settings.FakeSettings; import com.google.common.util.concurrent.MoreExecutors; @@ -113,7 +111,6 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters; @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener; - private FakeSettings mSecureSettings; private final Executor mMainExecutor = MoreExecutors.directExecutor(); private final Executor mBackgroundExecutor = MoreExecutors.directExecutor(); private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); @@ -135,9 +132,6 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { public void setUp() { MockitoAnnotations.initMocks(this); - mSecureSettings = new FakeSettings(); - mSecureSettings.putInt(Settings.Secure.DISABLE_SECURE_WINDOWS, 0); - // Preferred refresh rate is equal to the first displayMode's refresh rate mPreferredRefreshRate = mContext.getDisplay().getSystemSupportedModes()[0].getRefreshRate(); overrideResource( @@ -171,7 +165,6 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { () -> mSelectedUserInteractor, mUserTracker, mKosmos.getNotificationShadeWindowModel(), - mSecureSettings, mKosmos::getCommunalInteractor, mKosmos.getShadeLayoutParams()); mNotificationShadeWindowController.setScrimsVisibilityListener((visibility) -> {}); @@ -355,19 +348,6 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { } @Test - public void setKeyguardShowingWithSecureWindowsDisabled_disablesSecureFlag() { - mSecureSettings.putInt(Settings.Secure.DISABLE_SECURE_WINDOWS, 1); - mNotificationShadeWindowController.setBouncerShowing(true); - - verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); - assertThat((mLayoutParameters.getValue().flags & FLAG_SECURE) == 0).isTrue(); - assertThat( - (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_PRIVACY) - != 0) - .isTrue(); - } - - @Test public void setKeyguardNotShowing_disablesSecureFlag() { mNotificationShadeWindowController.setBouncerShowing(false); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt index e43c46b36a06..dd0ba00994ce 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.shade.display +import android.platform.test.annotations.EnableFlags import android.view.Display import android.view.Display.TYPE_EXTERNAL import android.view.MotionEvent @@ -31,6 +32,7 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.shade.data.repository.statusBarTouchShadeDisplayPolicy import com.android.systemui.shade.domain.interactor.notificationElement import com.android.systemui.shade.domain.interactor.qsElement +import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test @@ -41,6 +43,7 @@ import org.mockito.kotlin.mock @SmallTest @RunWith(AndroidJUnit4::class) +@EnableFlags(ShadeWindowGoesAround.FLAG_NAME) class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt index 03f546b09faf..24593f4455e5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.shade.domain.interactor import android.content.res.Configuration import android.content.res.mockResources +import android.platform.test.annotations.EnableFlags import android.view.Display import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -27,6 +28,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.scene.ui.view.mockShadeRootView import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository +import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs import com.android.systemui.statusbar.notification.row.notificationRebindingTracker @@ -47,6 +49,7 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @SmallTest +@EnableFlags(ShadeWindowGoesAround.FLAG_NAME) class ShadeDisplaysInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() 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 a832f486ef32..04eb709b8894 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 @@ -94,6 +94,36 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { } @Test + fun showClock_wideLayout_returnsTrue() = + testScope.runTest { + kosmos.enableDualShade(wideLayout = true) + + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade) + assertThat(underTest.showClock).isTrue() + + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade) + assertThat(underTest.showClock).isTrue() + } + + @Test + fun showClock_narrowLayoutOnNotificationsShade_returnsFalse() = + testScope.runTest { + kosmos.enableDualShade(wideLayout = false) + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade) + + assertThat(underTest.showClock).isFalse() + } + + @Test + fun showClock_narrowLayoutOnQuickSettingsShade_returnsTrue() = + testScope.runTest { + kosmos.enableDualShade(wideLayout = false) + setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade) + + assertThat(underTest.showClock).isTrue() + } + + @Test fun onShadeCarrierGroupClicked_launchesNetworkSettings() = testScope.runTest { val activityStarter = kosmos.activityStarter diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt index fad66581682f..0642467a001b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt @@ -19,6 +19,8 @@ package com.android.systemui.shared.clocks import android.content.res.Resources import android.graphics.Color import android.graphics.drawable.Drawable +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.util.TypedValue import android.view.LayoutInflater import android.widget.FrameLayout @@ -29,6 +31,7 @@ import com.android.systemui.customization.R import com.android.systemui.plugins.clocks.ClockId import com.android.systemui.plugins.clocks.ClockSettings import com.android.systemui.plugins.clocks.ThemeConfig +import com.android.systemui.shared.Flags import com.android.systemui.shared.clocks.DefaultClockController.Companion.DOZE_COLOR import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -103,6 +106,26 @@ class DefaultClockProviderTest : SysuiTestCase() { } @Test + @DisableFlags(Flags.FLAG_AMBIENT_AOD) + fun defaultClock_initialize_flagOff() { + val clock = provider.createClock(DEFAULT_CLOCK_ID) + verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA) + verify(mockLargeClockView).setColors(DOZE_COLOR, Color.MAGENTA) + + clock.initialize(true, 0f, 0f, null) + + // This is the default darkTheme color + val expectedColor = context.resources.getColor(android.R.color.system_accent1_100) + verify(mockSmallClockView).setColors(DOZE_COLOR, expectedColor) + verify(mockLargeClockView).setColors(DOZE_COLOR, expectedColor) + verify(mockSmallClockView).onTimeZoneChanged(notNull()) + verify(mockLargeClockView).onTimeZoneChanged(notNull()) + verify(mockSmallClockView).refreshTime() + verify(mockLargeClockView).refreshTime() + } + + @Test + @EnableFlags(Flags.FLAG_AMBIENT_AOD) fun defaultClock_initialize() { val clock = provider.createClock(DEFAULT_CLOCK_ID) verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA) @@ -110,7 +133,7 @@ class DefaultClockProviderTest : SysuiTestCase() { clock.initialize(true, 0f, 0f, null) - val expectedColor = 0 + val expectedColor = Color.MAGENTA verify(mockSmallClockView).setColors(DOZE_COLOR, expectedColor) verify(mockLargeClockView).setColors(DOZE_COLOR, expectedColor) verify(mockSmallClockView).onTimeZoneChanged(notNull()) @@ -165,8 +188,10 @@ class DefaultClockProviderTest : SysuiTestCase() { } @Test - fun defaultClock_events_onThemeChanged_noSeed() { - val expectedColor = 0 + @DisableFlags(Flags.FLAG_AMBIENT_AOD) + fun defaultClock_events_onThemeChanged_noSeed_flagOff() { + // This is the default darkTheme color + val expectedColor = context.resources.getColor(android.R.color.system_accent1_100) val clock = provider.createClock(DEFAULT_CLOCK_ID) verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA) @@ -180,6 +205,22 @@ class DefaultClockProviderTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_AMBIENT_AOD) + fun defaultClock_events_onThemeChanged_noSeedn() { + val expectedColor = Color.TRANSPARENT + val clock = provider.createClock(DEFAULT_CLOCK_ID) + + verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA) + verify(mockLargeClockView).setColors(DOZE_COLOR, Color.MAGENTA) + + clock.smallClock.events.onThemeChanged(ThemeConfig(true, null)) + clock.largeClock.events.onThemeChanged(ThemeConfig(true, null)) + + verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA) + verify(mockLargeClockView).setColors(DOZE_COLOR, Color.MAGENTA) + } + + @Test fun defaultClock_events_onThemeChanged_newSeed() { val initSeedColor = 10 val newSeedColor = 20 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java index 064fd485dab4..b80ff3466007 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java @@ -33,6 +33,7 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.systemui.SysuiTestCase; +import com.android.systemui.log.LogAssertKt; import com.android.systemui.plugins.Plugin; import com.android.systemui.plugins.PluginLifecycleManager; import com.android.systemui.plugins.PluginListener; @@ -138,11 +139,12 @@ public class PluginInstanceTest extends SysuiTestCase { mVersionCheckResult = false; assertFalse(mPluginInstance.hasError()); - mPluginInstanceFactory.create( mContext, mAppInfo, wrongVersionTestPluginComponentName, TestPlugin.class, mPluginListener); - mPluginInstance.onCreate(); + LogAssertKt.assertRunnableLogsWtf(()-> { + mPluginInstance.onCreate(); + }); assertTrue(mPluginInstance.hasError()); assertNull(mPluginInstance.getPlugin()); } @@ -193,8 +195,10 @@ public class PluginInstanceTest extends SysuiTestCase { mPluginInstance.onCreate(); assertFalse(mPluginInstance.hasError()); - Object result = mPluginInstance.getPlugin().methodThrowsError(); - assertNotNull(result); // Wrapper function should return non-null; + LogAssertKt.assertRunnableLogsWtf(()-> { + Object result = mPluginInstance.getPlugin().methodThrowsError(); + assertNotNull(result); // Wrapper function should return non-null; + }); assertTrue(mPluginInstance.hasError()); assertNull(mPluginInstance.getPlugin()); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt new file mode 100644 index 000000000000..e04162bf990a --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar + +import android.platform.test.flag.junit.FlagsParameterization +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.row.NotificationTestHelper +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters + +@SmallTest +@RunWith(ParameterizedAndroidJunit4::class) +class NotificationGroupingUtilTest(flags: FlagsParameterization) : SysuiTestCase() { + + private lateinit var underTest: NotificationGroupingUtil + + private lateinit var testHelper: NotificationTestHelper + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf(NotificationBundleUi.FLAG_NAME) + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + + @Before + fun setup() { + testHelper = NotificationTestHelper(mContext, mDependency) + } + + @Test + fun showsTime() { + val row = testHelper.createRow() + + underTest = NotificationGroupingUtil(row) + assertThat(underTest.showsTime(row)).isTrue() + } +}
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java index c9d910c530ea..01046cd10d87 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java @@ -20,16 +20,29 @@ import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; import static junit.framework.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.ActivityOptions; import android.app.Notification; +import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; import android.os.SystemClock; import android.os.UserHandle; +import android.platform.test.flag.junit.FlagsParameterization; import android.testing.TestableLooper; +import android.util.Pair; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.RemoteViews; -import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; @@ -42,19 +55,37 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.row.NotificationTestHelper; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.policy.RemoteInputUriController; import com.android.systemui.util.kotlin.JavaAdapter; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(ParameterizedAndroidJunit4.class) @TestableLooper.RunWithLooper public class NotificationRemoteInputManagerTest extends SysuiTestCase { + + @Parameters(name = "{0}") + public static List<FlagsParameterization> getParams() { + return FlagsParameterization.allCombinationsOf(NotificationBundleUi.FLAG_NAME); + } + private static final String TEST_PACKAGE_NAME = "test"; private static final int TEST_UID = 0; @@ -69,14 +100,34 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase { @Mock private NotificationClickNotifier mClickNotifier; @Mock private NotificationLockscreenUserManager mLockscreenUserManager; @Mock private PowerInteractor mPowerInteractor; + @Mock + NotificationRemoteInputManager.RemoteInputListener mRemoteInputListener; + private ActionClickLogger mActionClickLogger; + @Captor + ArgumentCaptor<NotificationRemoteInputManager.ClickHandler> mClickHandlerArgumentCaptor; + private Context mSpyContext; + private NotificationTestHelper mTestHelper; private TestableNotificationRemoteInputManager mRemoteInputManager; private NotificationEntry mEntry; + public NotificationRemoteInputManagerTest(FlagsParameterization flags) { + super(); + mSetFlagsRule.setFlagsParameterization(flags); + } + @Before - public void setUp() { + public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + mSpyContext = spy(mContext); + doNothing().when(mSpyContext).startIntentSender( + any(), any(), anyInt(), anyInt(), anyInt(), any()); + + + mTestHelper = new NotificationTestHelper(mSpyContext, mDependency); + mActionClickLogger = spy(new ActionClickLogger(logcatLogBuffer())); + mRemoteInputManager = new TestableNotificationRemoteInputManager(mContext, mock(NotifPipelineFlags.class), mLockscreenUserManager, @@ -87,9 +138,10 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase { mRemoteInputUriController, new RemoteInputControllerLogger(logcatLogBuffer()), mClickNotifier, - new ActionClickLogger(logcatLogBuffer()), + mActionClickLogger, mock(JavaAdapter.class), mock(ShadeInteractor.class)); + mRemoteInputManager.setRemoteInputListener(mRemoteInputListener); mEntry = new NotificationEntryBuilder() .setPkg(TEST_PACKAGE_NAME) .setOpPkg(TEST_PACKAGE_NAME) @@ -133,6 +185,70 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase { assertTrue(mRemoteInputManager.shouldKeepForSmartReplyHistory(mEntry)); } + @Test + public void testActionClick() throws Exception { + RemoteViews.RemoteResponse response = mock(RemoteViews.RemoteResponse.class); + when(response.getLaunchOptions(any())).thenReturn( + Pair.create(mock(Intent.class), mock(ActivityOptions.class))); + ExpandableNotificationRow row = getRowWithReplyAction(); + View actionView = ((LinearLayout) row.getPrivateLayout().getExpandedChild().findViewById( + com.android.internal.R.id.actions)).getChildAt(0); + Notification n = getNotification(row); + CountDownLatch latch = new CountDownLatch(1); + Consumer<NotificationEntry> consumer = notificationEntry -> latch.countDown(); + if (!NotificationBundleUi.isEnabled()) { + mRemoteInputManager.addActionPressListener(consumer); + } + + mRemoteInputManager.getRemoteViewsOnClickHandler().onInteraction( + actionView, + n.actions[0].actionIntent, + response); + + verify(mActionClickLogger).logInitialClick(row.getKey(), 0, n.actions[0].actionIntent); + verify(mClickNotifier).onNotificationActionClick( + eq(row.getKey()), eq(0), eq(n.actions[0]), any(), eq(false)); + verify(mCallback).handleRemoteViewClick(eq(actionView), eq(n.actions[0].actionIntent), + eq(false), eq(0), mClickHandlerArgumentCaptor.capture()); + + mClickHandlerArgumentCaptor.getValue().handleClick(); + verify(mActionClickLogger).logStartingIntentWithDefaultHandler( + row.getKey(), n.actions[0].actionIntent, 0); + + verify(mRemoteInputListener).releaseNotificationIfKeptForRemoteInputHistory(row.getKey()); + if (NotificationBundleUi.isEnabled()) { + verify(row.getEntryAdapter()).onNotificationActionClicked(); + } else { + latch.await(10, TimeUnit.MILLISECONDS); + } + } + + private Notification getNotification(ExpandableNotificationRow row) { + if (NotificationBundleUi.isEnabled()) { + return row.getEntryAdapter().getSbn().getNotification(); + } else { + return row.getEntry().getSbn().getNotification(); + } + } + + private ExpandableNotificationRow getRowWithReplyAction() throws Exception { + PendingIntent pi = PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"), + PendingIntent.FLAG_IMMUTABLE); + Notification n = new Notification.Builder(mSpyContext, "") + .setSmallIcon(com.android.systemui.res.R.drawable.ic_person) + .addAction(new Notification.Action(com.android.systemui.res.R.drawable.ic_person, + "reply", pi)) + .build(); + ExpandableNotificationRow row = mTestHelper.createRow(n); + row.onNotificationUpdated(); + row.getPrivateLayout().setExpandedChild(Notification.Builder.recoverBuilder(mSpyContext, n) + .createBigContentView().apply( + mSpyContext, + row.getPrivateLayout(), + mRemoteInputManager.getRemoteViewsOnClickHandler())); + return row; + } + private class TestableNotificationRemoteInputManager extends NotificationRemoteInputManager { TestableNotificationRemoteInputManager( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index fda4ab005446..485b9febc284 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt @@ -19,43 +19,51 @@ package com.android.systemui.statusbar.chips.call.ui.viewmodel import android.app.PendingIntent import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import android.view.View -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Icon -import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.plugins.activityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.chips.StatusBarChipsReturnAnimations import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlin.test.Test -import kotlinx.coroutines.test.runTest import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidJUnit4::class) -class CallChipViewModelTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val chipBackgroundView = mock<ChipBackgroundContainer>() @@ -71,7 +79,7 @@ class CallChipViewModelTest : SysuiTestCase() { private val mockExpandable: Expandable = mock<Expandable>().apply { whenever(dialogTransitionController(any())).thenReturn(mock()) } - private val underTest by lazy { kosmos.callChipViewModel } + private val Kosmos.underTest by Kosmos.Fixture { callChipViewModel } @Test fun chip_noCall_isHidden() = @@ -88,9 +96,10 @@ class CallChipViewModelTest : SysuiTestCase() { kosmos.runTest { val latest by collectLastValue(underTest.chip) - addOngoingCallState(startTimeMs = 0) + addOngoingCallState(startTimeMs = 0, isAppVisible = false) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse() } @Test @@ -98,9 +107,10 @@ class CallChipViewModelTest : SysuiTestCase() { kosmos.runTest { val latest by collectLastValue(underTest.chip) - addOngoingCallState(startTimeMs = -2) + addOngoingCallState(startTimeMs = -2, isAppVisible = false) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse() } @Test @@ -108,9 +118,82 @@ class CallChipViewModelTest : SysuiTestCase() { kosmos.runTest { val latest by collectLastValue(underTest.chip) - addOngoingCallState(startTimeMs = 345) + addOngoingCallState(startTimeMs = 345, isAppVisible = false) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse() + } + + @Test + @DisableFlags(StatusBarChipsReturnAnimations.FLAG_NAME) + fun chipLegacy_inCallWithVisibleApp_zeroStartTime_isHiddenAsInactive() = + kosmos.runTest { + val latest by collectLastValue(underTest.chip) + + addOngoingCallState(startTimeMs = 0, isAppVisible = true) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java) + } + + @Test + @EnableFlags(StatusBarChipsReturnAnimations.FLAG_NAME) + @EnableChipsModernization + fun chipWithReturnAnimation_inCallWithVisibleApp_zeroStartTime_isHiddenAsIconOnly() = + kosmos.runTest { + val latest by collectLastValue(underTest.chip) + + addOngoingCallState(startTimeMs = 0, isAppVisible = true) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue() + } + + @Test + @DisableFlags(StatusBarChipsReturnAnimations.FLAG_NAME) + fun chipLegacy_inCallWithVisibleApp_negativeStartTime_isHiddenAsInactive() = + kosmos.runTest { + val latest by collectLastValue(underTest.chip) + + addOngoingCallState(startTimeMs = -2, isAppVisible = true) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java) + } + + @Test + @EnableFlags(StatusBarChipsReturnAnimations.FLAG_NAME) + @EnableChipsModernization + fun chipWithReturnAnimation_inCallWithVisibleApp_negativeStartTime_isHiddenAsIconOnly() = + kosmos.runTest { + val latest by collectLastValue(underTest.chip) + + addOngoingCallState(startTimeMs = -2, isAppVisible = true) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue() + } + + @Test + @DisableFlags(StatusBarChipsReturnAnimations.FLAG_NAME) + fun chipLegacy_inCallWithVisibleApp_positiveStartTime_isHiddenAsInactive() = + kosmos.runTest { + val latest by collectLastValue(underTest.chip) + + addOngoingCallState(startTimeMs = 345, isAppVisible = true) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java) + } + + @Test + @EnableFlags(StatusBarChipsReturnAnimations.FLAG_NAME) + @EnableChipsModernization + fun chipWithReturnAnimation_inCallWithVisibleApp_positiveStartTime_isHiddenAsTimer() = + kosmos.runTest { + val latest by collectLastValue(underTest.chip) + + addOngoingCallState(startTimeMs = 345, isAppVisible = true) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue() } @Test @@ -419,5 +502,18 @@ class CallChipViewModelTest : SysuiTestCase() { private const val PROMOTED_BACKGROUND_COLOR = 65 private const val PROMOTED_PRIMARY_TEXT_COLOR = 98 + + @get:Parameters(name = "{0}") + @JvmStatic + val flags: List<FlagsParameterization> + get() = buildList { + addAll( + FlagsParameterization.allCombinationsOf( + StatusBarRootModernization.FLAG_NAME, + StatusBarChipsModernization.FLAG_NAME, + StatusBarChipsReturnAnimations.FLAG_NAME, + ) + ) + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt index 5d1950670777..7f8f5f43e775 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt @@ -27,6 +27,7 @@ import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel @@ -39,6 +40,7 @@ import org.mockito.kotlin.mock @SmallTest @RunWith(AndroidJUnit4::class) +@EnableFlags(StatusBarNotifChips.FLAG_NAME) class SingleNotificationChipInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() val factory = kosmos.singleNotificationChipInteractorFactory diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt index e2d1498270c8..e39fa7099953 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt @@ -137,7 +137,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - addOngoingCallState() + addOngoingCallState(isAppVisible = false) val latest by collectLastValue(underTest.primaryChip) @@ -163,7 +163,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.DoingNothing mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - addOngoingCallState() + addOngoingCallState(isAppVisible = false) val latest by collectLastValue(underTest.primaryChip) @@ -178,7 +178,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { // MediaProjection covers both share-to-app and cast-to-other-device mediaProjectionState.value = MediaProjectionState.NotProjecting - addOngoingCallState(key = notificationKey) + addOngoingCallState(key = notificationKey, isAppVisible = false) val latest by collectLastValue(underTest.primaryChip) @@ -190,7 +190,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { kosmos.runTest { // Start with just the lowest priority chip shown val callNotificationKey = "call" - addOngoingCallState(key = callNotificationKey) + addOngoingCallState(key = callNotificationKey, isAppVisible = false) // And everything else hidden mediaProjectionState.value = MediaProjectionState.NotProjecting screenRecordState.value = ScreenRecordModel.DoingNothing @@ -225,7 +225,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) val callNotificationKey = "call" - addOngoingCallState(key = callNotificationKey) + addOngoingCallState(key = callNotificationKey, isAppVisible = false) val latest by collectLastValue(underTest.primaryChip) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt index 96c4a59f752d..f06244f4f637 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt @@ -62,6 +62,7 @@ import com.android.systemui.statusbar.notification.data.repository.ActiveNotific import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.addNotif import com.android.systemui.statusbar.notification.data.repository.addNotifs +import com.android.systemui.statusbar.notification.data.repository.removeNotif import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.phone.SystemUIDialog @@ -235,7 +236,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { fun primaryChip_screenRecordShowAndCallShow_screenRecordShown() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - addOngoingCallState("call") + addOngoingCallState("call", isAppVisible = false) val latest by collectLastValue(underTest.primaryChip) @@ -248,7 +249,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { kosmos.runTest { val callNotificationKey = "call" screenRecordState.value = ScreenRecordModel.Recording - addOngoingCallState(callNotificationKey) + addOngoingCallState(callNotificationKey, isAppVisible = false) val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -295,7 +296,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @Test fun chipsLegacy_oneChip_notSquished() = kosmos.runTest { - addOngoingCallState() + addOngoingCallState(isAppVisible = false) val latest by collectLastValue(underTest.chipsLegacy) @@ -322,7 +323,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { fun chipsLegacy_twoTimerChips_isSmallPortrait_bothSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - addOngoingCallState(key = "call") + addOngoingCallState(key = "call", isAppVisible = false) val latest by collectLastValue(underTest.chipsLegacy) @@ -349,12 +350,42 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) } + @EnableChipsModernization + @Test + fun chips_threeChips_isSmallPortrait_allSquished() = + kosmos.runTest { + screenRecordState.value = ScreenRecordModel.Recording + addOngoingCallState(key = "call") + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.shortCriticalText = "Some text here" + } + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + ) + ) + + val latest by collectLastValue(underTest.chips) + + // Squished chips are icon only + assertThat(latest!!.active[0]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat(latest!!.active[1]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat(latest!!.active[2]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + } + @DisableChipsModernization @Test fun chipsLegacy_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Starting(millisUntilStarted = 2000) - addOngoingCallState(key = "call") + addOngoingCallState(key = "call", isAppVisible = false) val latest by collectLastValue(underTest.chipsLegacy) @@ -400,7 +431,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .isInstanceOf(OngoingActivityChipModel.Inactive::class.java) // WHEN there's 2 chips - addOngoingCallState(key = "call") + addOngoingCallState(key = "call", isAppVisible = false) // THEN they both become squished assertThat(latest!!.primary) @@ -456,7 +487,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { fun chipsLegacy_twoChips_isLandscape_notSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - addOngoingCallState(key = "call") + addOngoingCallState(key = "call", isAppVisible = false) // WHEN we're in landscape val config = @@ -502,7 +533,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { fun chipsLegacy_twoChips_isLargeScreen_notSquished() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording - addOngoingCallState(key = "call") + addOngoingCallState(key = "call", isAppVisible = false) // WHEN we're on a large screen kosmos.displayStateRepository.setIsLargeScreen(true) @@ -596,7 +627,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.DoingNothing mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - addOngoingCallState(key = "call") + addOngoingCallState(key = "call", isAppVisible = false) val latest by collectLastValue(underTest.primaryChip) @@ -611,7 +642,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.DoingNothing mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - addOngoingCallState(key = "call") + addOngoingCallState(key = "call", isAppVisible = false) val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -650,7 +681,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { mediaProjectionState.value = MediaProjectionState.NotProjecting val callNotificationKey = "call" - addOngoingCallState(key = callNotificationKey) + addOngoingCallState(key = callNotificationKey, isAppVisible = false) val latest by collectLastValue(underTest.primaryChip) @@ -666,7 +697,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // MediaProjection covers both share-to-app and cast-to-other-device mediaProjectionState.value = MediaProjectionState.NotProjecting - addOngoingCallState(key = callNotificationKey) + addOngoingCallState(key = callNotificationKey, isAppVisible = false) val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) @@ -851,7 +882,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @EnableChipsModernization @Test - fun chips_threePromotedNotifs_topTwoActiveThirdInOverflow() = + fun chips_fourPromotedNotifs_topThreeActiveFourthInOverflow() = kosmos.runTest { val latest by collectLastValue(underTest.chips) val unused by collectLastValue(underTest.chipsLegacy) @@ -859,6 +890,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val firstIcon = createStatusBarIconViewOrNull() val secondIcon = createStatusBarIconViewOrNull() val thirdIcon = createStatusBarIconViewOrNull() + val fourthIcon = createStatusBarIconViewOrNull() setNotifs( listOf( activeNotificationModel( @@ -879,20 +911,27 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { promotedContent = PromotedNotificationContentModel.Builder("thirdNotif").build(), ), + activeNotificationModel( + key = "fourthNotif", + statusBarChipIcon = fourthIcon, + promotedContent = + PromotedNotificationContentModel.Builder("fourthNotif").build(), + ), ) ) - assertThat(latest!!.active.size).isEqualTo(2) + assertThat(latest!!.active.size).isEqualTo(3) assertIsNotifChip(latest!!.active[0], context, firstIcon, "firstNotif") assertIsNotifChip(latest!!.active[1], context, secondIcon, "secondNotif") + assertIsNotifChip(latest!!.active[2], context, thirdIcon, "thirdNotif") assertThat(latest!!.overflow.size).isEqualTo(1) - assertIsNotifChip(latest!!.overflow[0], context, thirdIcon, "thirdNotif") + assertIsNotifChip(latest!!.overflow[0], context, fourthIcon, "fourthNotif") assertThat(latest!!.inactive.size).isEqualTo(4) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) } @Test - fun visibleChipKeys_threePromotedNotifs_topTwoInList() = + fun visibleChipKeys_fourPromotedNotifs_topThreeInList() = kosmos.runTest { val latest by collectLastValue(underTest.visibleChipKeys) @@ -916,10 +955,16 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { promotedContent = PromotedNotificationContentModel.Builder("thirdNotif").build(), ), + activeNotificationModel( + key = "fourthNotif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = + PromotedNotificationContentModel.Builder("fourthNotif").build(), + ), ) ) - assertThat(latest).containsExactly("firstNotif", "secondNotif").inOrder() + assertThat(latest).containsExactly("firstNotif", "secondNotif", "thirdNotif").inOrder() } @DisableChipsModernization @@ -930,7 +975,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val unused by collectLastValue(underTest.chips) val callNotificationKey = "call" - addOngoingCallState(callNotificationKey) + addOngoingCallState(callNotificationKey, isAppVisible = false) val firstIcon = createStatusBarIconViewOrNull() activeNotificationListRepository.addNotifs( @@ -957,7 +1002,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @EnableChipsModernization @Test - fun chips_callAndPromotedNotifs_callAndFirstNotifActiveSecondNotifInOverflow() = + fun chips_callAndPromotedNotifs_callAndFirstTwoNotifsActive_thirdNotifInOverflow() = kosmos.runTest { val latest by collectLastValue(underTest.chips) val unused by collectLastValue(underTest.chipsLegacy) @@ -965,6 +1010,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val callNotificationKey = "call" val firstIcon = createStatusBarIconViewOrNull() val secondIcon = createStatusBarIconViewOrNull() + val thirdIcon = createStatusBarIconViewOrNull() addOngoingCallState(key = callNotificationKey) activeNotificationListRepository.addNotifs( listOf( @@ -980,27 +1026,34 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { promotedContent = PromotedNotificationContentModel.Builder("secondNotif").build(), ), + activeNotificationModel( + key = "thirdNotif", + statusBarChipIcon = thirdIcon, + promotedContent = + PromotedNotificationContentModel.Builder("thirdNotif").build(), + ), ) ) - assertThat(latest!!.active.size).isEqualTo(2) + assertThat(latest!!.active.size).isEqualTo(3) assertIsCallChip(latest!!.active[0], callNotificationKey, context) assertIsNotifChip(latest!!.active[1], context, firstIcon, "firstNotif") + assertIsNotifChip(latest!!.active[2], context, secondIcon, "secondNotif") assertThat(latest!!.overflow.size).isEqualTo(1) - assertIsNotifChip(latest!!.overflow[0], context, secondIcon, "secondNotif") + assertIsNotifChip(latest!!.overflow[0], context, thirdIcon, "thirdNotif") assertThat(latest!!.inactive.size).isEqualTo(3) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) } @DisableChipsModernization @Test - fun chipsLegacy_screenRecordAndCallAndPromotedNotifs_notifsNotShown() = + fun chipsLegacy_screenRecordAndCallAndPromotedNotif_notifNotShown() = kosmos.runTest { val callNotificationKey = "call" val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) - addOngoingCallState(callNotificationKey) + addOngoingCallState(callNotificationKey, isAppVisible = false) screenRecordState.value = ScreenRecordModel.Recording activeNotificationListRepository.addNotif( activeNotificationModel( @@ -1016,7 +1069,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { } @Test - fun visibleChipKeys_screenRecordAndCallAndPromotedNotifs_topTwoInList() = + fun visibleChipKeys_screenRecordAndCallAndPromotedNotifs_topThreeInList() = kosmos.runTest { val latest by collectLastValue(underTest.visibleChipKeys) @@ -1025,20 +1078,27 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.Recording activeNotificationListRepository.addNotif( activeNotificationModel( - key = "notif", + key = "notif1", statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), + ) + ) + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif2", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = PromotedNotificationContentModel.Builder("notif2").build(), ) ) assertThat(latest) - .containsExactly(ScreenRecordChipViewModel.KEY, callNotificationKey) + .containsExactly(ScreenRecordChipViewModel.KEY, callNotificationKey, "notif1") .inOrder() } @EnableChipsModernization @Test - fun chips_screenRecordAndCallAndPromotedNotif_notifInOverflow() = + fun chips_screenRecordAndCallAndPromotedNotifs_secondNotifInOverflow() = kosmos.runTest { val latest by collectLastValue(underTest.chips) val unused by collectLastValue(underTest.chipsLegacy) @@ -1055,11 +1115,22 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ) addOngoingCallState(key = callNotificationKey) - assertThat(latest!!.active.size).isEqualTo(2) + // This is the overflow notif + val notifIcon2 = createStatusBarIconViewOrNull() + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif2", + statusBarChipIcon = notifIcon2, + promotedContent = PromotedNotificationContentModel.Builder("notif2").build(), + ) + ) + + assertThat(latest!!.active.size).isEqualTo(3) assertIsScreenRecordChip(latest!!.active[0]) assertIsCallChip(latest!!.active[1], callNotificationKey, context) + assertIsNotifChip(latest!!.active[2], context, notifIcon, "notif") assertThat(latest!!.overflow.size).isEqualTo(1) - assertIsNotifChip(latest!!.overflow[0], context, notifIcon, "notif") + assertIsNotifChip(latest!!.overflow[0], context, notifIcon2, "notif2") assertThat(latest!!.inactive.size).isEqualTo(2) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) } @@ -1089,7 +1160,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertIsNotifChip(latest, context, notifIcon, "notif") // WHEN the higher priority call chip is added - addOngoingCallState(callNotificationKey) + addOngoingCallState(callNotificationKey, isAppVisible = false) // THEN the higher priority call chip is used assertIsCallChip(latest, callNotificationKey, context) @@ -1120,7 +1191,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { screenRecordState.value = ScreenRecordModel.Recording mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - addOngoingCallState(callNotificationKey) + addOngoingCallState(callNotificationKey, isAppVisible = false) val notifIcon = createStatusBarIconViewOrNull() activeNotificationListRepository.addNotif( activeNotificationModel( @@ -1182,7 +1253,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) // WHEN the higher priority call chip is added - addOngoingCallState(callNotificationKey) + addOngoingCallState(callNotificationKey, isAppVisible = false) // THEN the higher priority call chip is used as primary and notif is demoted to // secondary @@ -1234,15 +1305,16 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { @Test fun chips_movesChipsAroundAccordingToPriority() = kosmos.runTest { + systemClock.setCurrentTimeMillis(10_000) val callNotificationKey = "call" // Start with just the lowest priority chip active - val notifIcon = createStatusBarIconViewOrNull() + val notif1Icon = createStatusBarIconViewOrNull() setNotifs( listOf( activeNotificationModel( - key = "notif", - statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + key = "notif1", + statusBarChipIcon = notif1Icon, + promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), ) ) ) @@ -1254,7 +1326,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val unused by collectLastValue(underTest.chipsLegacy) assertThat(latest!!.active.size).isEqualTo(1) - assertIsNotifChip(latest!!.active[0], context, notifIcon, "notif") + assertIsNotifChip(latest!!.active[0], context, notif1Icon, "notif1") assertThat(latest!!.overflow).isEmpty() assertThat(latest!!.inactive.size).isEqualTo(4) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) @@ -1262,10 +1334,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // WHEN the higher priority call chip is added addOngoingCallState(key = callNotificationKey) - // THEN the higher priority call chip and notif are active in that order + // THEN the higher priority call chip and notif1 are active in that order assertThat(latest!!.active.size).isEqualTo(2) assertIsCallChip(latest!!.active[0], callNotificationKey, context) - assertIsNotifChip(latest!!.active[1], context, notifIcon, "notif") + assertIsNotifChip(latest!!.active[1], context, notif1Icon, "notif1") assertThat(latest!!.overflow).isEmpty() assertThat(latest!!.inactive.size).isEqualTo(3) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) @@ -1278,56 +1350,63 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { createTask(taskId = 1), ) - // THEN the higher priority media projection chip and call are active in that order, and - // notif is demoted to overflow - assertThat(latest!!.active.size).isEqualTo(2) + // THEN media projection, then call, then notif1 are active + assertThat(latest!!.active.size).isEqualTo(3) assertIsShareToAppChip(latest!!.active[0]) assertIsCallChip(latest!!.active[1], callNotificationKey, context) - assertThat(latest!!.overflow.size).isEqualTo(1) - assertIsNotifChip(latest!!.overflow[0], context, notifIcon, "notif") + assertIsNotifChip(latest!!.active[2], context, notif1Icon, "notif1") + assertThat(latest!!.overflow).isEmpty() assertThat(latest!!.inactive.size).isEqualTo(2) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) - // WHEN the higher priority screen record chip is added + // WHEN the screen record chip is added, which replaces media projection screenRecordState.value = ScreenRecordModel.Recording + // AND another notification is added + systemClock.advanceTime(2_000) + val notif2Icon = createStatusBarIconViewOrNull() + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif2", + statusBarChipIcon = notif2Icon, + promotedContent = PromotedNotificationContentModel.Builder("notif2").build(), + ) + ) - // THEN the higher priority screen record chip and call are active in that order, and - // media projection and notif are demoted in overflow - assertThat(latest!!.active.size).isEqualTo(2) + // THEN screen record, then call, then notif2 are active + assertThat(latest!!.active.size).isEqualTo(3) assertIsScreenRecordChip(latest!!.active[0]) assertIsCallChip(latest!!.active[1], callNotificationKey, context) + assertIsNotifChip(latest!!.active[2], context, notif2Icon, "notif2") + + // AND notif1 and media projection is demoted in overflow assertThat(latest!!.overflow.size).isEqualTo(2) assertIsShareToAppChip(latest!!.overflow[0]) - assertIsNotifChip(latest!!.overflow[1], context, notifIcon, "notif") + assertIsNotifChip(latest!!.overflow[1], context, notif1Icon, "notif1") assertThat(latest!!.inactive.size).isEqualTo(1) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) - // WHEN screen record and call is dropped + // WHEN screen record and call are dropped screenRecordState.value = ScreenRecordModel.DoingNothing - setNotifs( - listOf( - activeNotificationModel( - key = "notif", - statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), - ) - ) - ) + removeOngoingCallState(callNotificationKey) - // THEN media projection and notif remain - assertThat(latest!!.active.size).isEqualTo(2) + // THEN media projection, notif2, and notif1 remain + assertThat(latest!!.active.size).isEqualTo(3) assertIsShareToAppChip(latest!!.active[0]) - assertIsNotifChip(latest!!.active[1], context, notifIcon, "notif") + assertIsNotifChip(latest!!.active[1], context, notif2Icon, "notif2") + assertIsNotifChip(latest!!.active[2], context, notif1Icon, "notif1") assertThat(latest!!.overflow).isEmpty() assertThat(latest!!.inactive.size).isEqualTo(3) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) // WHEN media projection is dropped mediaProjectionState.value = MediaProjectionState.NotProjecting + // AND notif2 is dropped + systemClock.advanceTime(2_000) + activeNotificationListRepository.removeNotif("notif2") - // THEN only notif is active + // THEN only notif1 is active assertThat(latest!!.active.size).isEqualTo(1) - assertIsNotifChip(latest!!.active[0], context, notifIcon, "notif") + assertIsNotifChip(latest!!.active[0], context, notif1Icon, "notif1") assertThat(latest!!.overflow).isEmpty() assertThat(latest!!.inactive.size).isEqualTo(4) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationCloseButtonTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationCloseButtonTest.kt new file mode 100644 index 000000000000..e4b2e6c9b359 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationCloseButtonTest.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.app.Notification +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.testing.TestableLooper +import android.view.MotionEvent +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.row.NotificationTestHelper +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.stub +import com.android.systemui.res.R +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import org.junit.Before + +private fun getCloseButton(row: ExpandableNotificationRow): View { + val contractedView = row.showingLayout?.contractedChild!! + return contractedView.findViewById(com.android.internal.R.id.close_button) +} + +@SmallTest +@RunWith(AndroidJUnit4::class) +class NotificationCloseButtonTest : SysuiTestCase() { + private lateinit var helper: NotificationTestHelper + + @Before + fun setUp() { + helper = NotificationTestHelper( + mContext, + mDependency, + TestableLooper.get(this) + ) + } + + @Test + @DisableFlags(Flags.FLAG_NOTIFICATION_ADD_X_ON_HOVER_TO_DISMISS) + fun verifyWhenFeatureDisabled() { + // Enable the notification row to dismiss. + helper.dismissibilityProvider.stub { + on { isDismissable(any()) } doReturn true + } + + // By default, the close button should be gone. + val row = createNotificationRow() + val closeButton = getCloseButton(row) + assertThat(closeButton).isNotNull() + assertThat(closeButton.visibility).isEqualTo(View.GONE) + + val hoverEnterEvent = MotionEvent.obtain( + 0/*downTime=*/, + 0/*eventTime=*/, + MotionEvent.ACTION_HOVER_ENTER, + 0f/*x=*/, + 0f/*y=*/, + 0/*metaState*/ + ) + + // The close button should not show if the feature is disabled. + row.onInterceptHoverEvent(hoverEnterEvent) + assertThat(closeButton.visibility).isEqualTo(View.GONE) + } + + @Test + @EnableFlags(Flags.FLAG_NOTIFICATION_ADD_X_ON_HOVER_TO_DISMISS) + fun verifyOnDismissableNotification() { + // Enable the notification row to dismiss. + helper.dismissibilityProvider.stub { + on { isDismissable(any()) } doReturn true + } + + // By default, the close button should be gone. + val row = createNotificationRow() + val closeButton = getCloseButton(row) + assertThat(closeButton).isNotNull() + assertThat(closeButton.visibility).isEqualTo(View.GONE) + + val hoverEnterEvent = MotionEvent.obtain( + 0/*downTime=*/, + 0/*eventTime=*/, + MotionEvent.ACTION_HOVER_ENTER, + 0f/*x=*/, + 0f/*y=*/, + 0/*metaState*/ + ) + + // When the row is hovered, the close button should show. + row.onInterceptHoverEvent(hoverEnterEvent) + assertThat(closeButton.visibility).isEqualTo(View.VISIBLE) + + val hoverExitEvent = MotionEvent.obtain( + 0/*downTime=*/, + 0/*eventTime=*/, + MotionEvent.ACTION_HOVER_EXIT, + 0f/*x=*/, + 0f/*y=*/, + 0/*metaState*/ + ) + + // When hover exits the row, the close button should be gone again. + row.onInterceptHoverEvent(hoverExitEvent) + assertThat(closeButton.visibility).isEqualTo(View.GONE) + } + + @Test + @EnableFlags(Flags.FLAG_NOTIFICATION_ADD_X_ON_HOVER_TO_DISMISS) + fun verifyOnUndismissableNotification() { + // By default, the close button should be gone. + val row = createNotificationRow() + val closeButton = getCloseButton(row) + assertThat(closeButton).isNotNull() + assertThat(closeButton.visibility).isEqualTo(View.GONE) + + val hoverEnterEvent = MotionEvent.obtain( + 0/*downTime=*/, + 0/*eventTime=*/, + MotionEvent.ACTION_HOVER_ENTER, + 0f/*x=*/, + 0f/*y=*/, + 0/*metaState*/ + ) + + // Because the host notification cannot be dismissed, the close button should not show. + row.onInterceptHoverEvent(hoverEnterEvent) + assertThat(closeButton.visibility).isEqualTo(View.GONE) + } + + private fun createNotificationRow(): ExpandableNotificationRow { + val notification = Notification.Builder(context, "channel") + .setContentTitle("title") + .setContentText("text") + .setSmallIcon(R.drawable.ic_person) + .build() + + return helper.createRow(notification) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt index b6889afa4e8a..faafa073be4c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt @@ -29,8 +29,10 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.res.R import com.android.systemui.statusbar.RankingBuilder +import com.android.systemui.statusbar.notification.mockNotificationActivityStarter import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.entryAdapterFactory +import com.android.systemui.statusbar.notification.row.mockNotificationActionClickManager import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING import com.android.systemui.testKosmos @@ -355,16 +357,27 @@ class NotificationEntryAdapterTest : SysuiTestCase() { val notification: Notification = Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build() - val entry = - NotificationEntryBuilder() - .setNotification(notification) - .setImportance(NotificationManager.IMPORTANCE_MIN) - .build() + val entry = NotificationEntryBuilder().setNotification(notification).build() underTest = factory.create(entry) as NotificationEntryAdapter underTest.onNotificationBubbleIconClicked() - verify((factory as? EntryAdapterFactoryImpl)?.getNotificationActivityStarter()) - ?.onNotificationBubbleIconClicked(entry) + verify(kosmos.mockNotificationActivityStarter).onNotificationBubbleIconClicked(entry) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun onNotificationActionClicked() { + val notification: Notification = + Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .addAction(Mockito.mock(Notification.Action::class.java)) + .build() + + val entry = NotificationEntryBuilder().setNotification(notification).build() + + underTest = factory.create(entry) as NotificationEntryAdapter + underTest.onNotificationActionClicked() + verify(kosmos.mockNotificationActionClickManager).onNotificationActionClicked(entry) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinatorTest.kt index 8a9720ea3cb0..732180810880 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinatorTest.kt @@ -34,6 +34,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations +import kotlin.test.assertEquals @SmallTest @RunWith(AndroidJUnit4::class) @@ -87,6 +88,18 @@ class BundleCoordinatorTest : SysuiTestCase() { isFalse() } + @Test + fun testBundler_getBundleIdOrNull_returnBundleId() { + val classifiedEntry = makeEntryOfChannelType(PROMOTIONS_ID) + assertEquals(coordinator.bundler.getBundleIdOrNull(classifiedEntry), PROMOTIONS_ID) + } + + @Test + fun testBundler_getBundleIdOrNull_returnNull() { + val unclassifiedEntry = makeEntryOfChannelType("not system channel") + assertEquals(coordinator.bundler.getBundleIdOrNull(unclassifiedEntry), null) + } + private fun makeEntryOfChannelType( type: String, buildBlock: NotificationEntryBuilder.() -> Unit = {} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index 30983550f0f9..44d88c31c5f1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -50,6 +50,8 @@ import com.android.systemui.statusbar.notification.interruption.NotificationInte import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.DecisionImpl import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.FullScreenIntentDecisionImpl import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider +import com.android.systemui.statusbar.notification.row.mockNotificationActionClickManager +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.phone.NotificationGroupTestHelper import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.FakeExecutor @@ -138,6 +140,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { headsUpViewBinder, visualInterruptionDecisionProvider, remoteInputManager, + kosmos.mockNotificationActionClickManager, launchFullScreenIntentProvider, flags, statusBarNotificationChipsInteractor, @@ -161,8 +164,14 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(notifPipeline).addOnBeforeFinalizeFilterListener(capture()) } onHeadsUpChangedListener = withArgCaptor { verify(headsUpManager).addListener(capture()) } - actionPressListener = withArgCaptor { - verify(remoteInputManager).addActionPressListener(capture()) + actionPressListener = if (NotificationBundleUi.isEnabled) { + withArgCaptor { + verify(kosmos.mockNotificationActionClickManager).addActionClickListener(capture()) + } + } else { + withArgCaptor { + verify(remoteInputManager).addActionPressListener(capture()) + } } given(headsUpManager.allEntries).willAnswer { huns.stream() } given(headsUpManager.isHeadsUpEntry(anyString())).willAnswer { invocation -> @@ -260,7 +269,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { addHUN(entry) actionPressListener.accept(entry) - verify(headsUpManager, times(1)).setUserActionMayIndirectlyRemove(entry) + verify(headsUpManager, times(1)).setUserActionMayIndirectlyRemove(entry.key) whenever(headsUpManager.canRemoveImmediately(anyString())).thenReturn(true) assertFalse(notifLifetimeExtender.maybeExtendLifetime(entry, 0)) 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 a90539413adb..e28e587d2cdc 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 @@ -28,6 +28,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.statusBarStateController import com.android.systemui.shade.shadeTestUtil import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.collection.BundleEntry import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry @@ -280,6 +281,8 @@ class LockScreenMinimalismCoordinatorTest : SysuiTestCase() { val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build() val listEntryList = listOf(group, solo1, solo2) val notificationEntryList = listOf(solo1, solo2, parent, child1, child2) + val bundle = BundleEntry("bundleKey") + val bundleList = listOf(bundle) runCoordinatorTest { // All entries are added (and now unseen) @@ -300,6 +303,11 @@ class LockScreenMinimalismCoordinatorTest : SysuiTestCase() { assertThatTopOngoingKey().isEqualTo(null) assertThatTopUnseenKey().isEqualTo(solo1.key) + // TEST: bundle is not picked + onBeforeTransformGroupsListener.onBeforeTransformGroups(bundleList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + // TEST: if top-ranked unseen is colorized, fall back to #2 ranked unseen solo1.setColorizedFgs(true) onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt index e22acd53e584..8560b66d961f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt @@ -37,6 +37,7 @@ import com.android.systemui.flags.andSceneContainer import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.log.assertLogsWtfs import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.shadeTestUtil @@ -205,8 +206,7 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { fun pinnedHeadsUpStatuses_pinnedByUser_butFlagOff_returnsNotPinned() { val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) entry.row = testHelper.createRow() - underTest.showNotification(entry, isPinnedByUser = true) - + assertLogsWtfs { underTest.showNotification(entry, isPinnedByUser = true) } assertThat(underTest.hasPinnedHeadsUp()).isFalse() assertThat(underTest.pinnedHeadsUpStatus()).isEqualTo(PinnedStatus.NotPinned) } @@ -1071,7 +1071,7 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(underTest.canRemoveImmediately(notifEntry.key)).isFalse() - underTest.setUserActionMayIndirectlyRemove(notifEntry) + underTest.setUserActionMayIndirectlyRemove(notifEntry.key) assertThat(underTest.canRemoveImmediately(notifEntry.key)).isTrue() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index 8e7733bb23ca..e6b2c2541447 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -29,6 +29,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -60,11 +61,11 @@ import com.android.systemui.TestableDependency; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.flags.FakeFeatureFlagsClassic; import com.android.systemui.flags.FeatureFlagsClassic; +import com.android.systemui.media.NotificationMediaManager; import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.media.dialog.MediaOutputDialogManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.res.R; -import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.SmartReplyController; @@ -363,6 +364,7 @@ public class NotificationTestHelper { .setUid(UID) .setInitialPid(2000) .setNotification(summary) + .setUser(USER_HANDLE) .setParent(GroupEntry.ROOT_ENTRY) .build(); GroupEntryBuilder groupEntry = new GroupEntryBuilder() @@ -743,11 +745,12 @@ public class NotificationTestHelper { mock(MetricsLogger.class), mock(PeopleNotificationIdentifier.class), mock(NotificationIconStyleProvider.class), - mock(VisualStabilityCoordinator.class) + mock(VisualStabilityCoordinator.class), + mock(NotificationActionClickManager.class) ).create(entry); row.initialize( - entryAdapter, + spy(entryAdapter), entry, mock(RemoteInputViewSubcomponent.Factory.class), APP_NAME, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt index f6c031f54818..8e3bdc48398f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt @@ -219,7 +219,6 @@ class StackStateAnimatorTest : SysuiTestCase() { ) } - @DisableFlags(Flags.FLAG_PHYSICAL_NOTIFICATION_MOVEMENT) @Test @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME) fun startAnimationForEvents_headsUpFromBottom_startsHeadsUpAppearAnim_flagOn() { @@ -246,7 +245,7 @@ class StackStateAnimatorTest : SysuiTestCase() { } @Test - @DisableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME) + @DisableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME, Flags.FLAG_PHYSICAL_NOTIFICATION_MOVEMENT) fun startAnimationForEvents_startsHeadsUpDisappearAnim_flagOff() { val disappearDuration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR.toLong() val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR) @@ -277,6 +276,7 @@ class StackStateAnimatorTest : SysuiTestCase() { @Test @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME) + @DisableFlags(Flags.FLAG_PHYSICAL_NOTIFICATION_MOVEMENT) fun startAnimationForEvents_startsHeadsUpDisappearAnim_flagOn() { val disappearDuration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR.toLong() val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt index 14e7cdc50227..3b836b774788 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt @@ -16,11 +16,11 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel -import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds @@ -32,7 +32,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) -@RunWithLooper +@EnableSceneContainer class NotificationsPlaceholderViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val underTest by lazy { kosmos.notificationsPlaceholderViewModel } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index e2330f448a1b..1ea41de63e64 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -61,8 +61,8 @@ 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.log.SessionTracker; +import com.android.systemui.media.NotificationMediaManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.KeyguardStateController; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt index bab349aa7a74..f4204af7829b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt @@ -82,6 +82,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { statusBarChipIconView = testIconView, contentIntent = testIntent, promotedContent = testPromotedContent, + isAppVisible = false, ) // Verify model is InCall and has the correct icon, intent, and promoted content. @@ -92,12 +93,12 @@ class OngoingCallInteractorTest : SysuiTestCase() { assertThat(model.intent).isSameInstanceAs(testIntent) assertThat(model.notificationKey).isEqualTo(key) assertThat(model.promotedContent).isSameInstanceAs(testPromotedContent) + assertThat(model.isAppVisible).isFalse() } @Test fun ongoingCallNotification_setsAllFields_withAppVisible() = kosmos.runTest { - kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = true val latest by collectLastValue(underTest.ongoingCallState) // Set up notification with icon view and intent @@ -112,17 +113,19 @@ class OngoingCallInteractorTest : SysuiTestCase() { statusBarChipIconView = testIconView, contentIntent = testIntent, promotedContent = testPromotedContent, + isAppVisible = true, ) - // Verify model is InCallWithVisibleApp and has the correct icon, intent, and promoted - // content. - assertThat(latest).isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java) - val model = latest as OngoingCallModel.InCallWithVisibleApp + // Verify model is InCall with visible app and has the correct icon, intent, and + // promoted content. + assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) + val model = latest as OngoingCallModel.InCall assertThat(model.startTimeMs).isEqualTo(startTimeMs) assertThat(model.notificationIconView).isSameInstanceAs(testIconView) assertThat(model.intent).isSameInstanceAs(testIntent) assertThat(model.notificationKey).isEqualTo(key) assertThat(model.promotedContent).isSameInstanceAs(testPromotedContent) + assertThat(model.isAppVisible).isTrue() } @Test @@ -139,23 +142,23 @@ class OngoingCallInteractorTest : SysuiTestCase() { @Test fun ongoingCallNotification_appVisibleInitially_emitsInCallWithVisibleApp() = kosmos.runTest { - kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = true val latest by collectLastValue(underTest.ongoingCallState) - addOngoingCallState(uid = UID) + addOngoingCallState(uid = UID, isAppVisible = true) - assertThat(latest).isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java) + assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat((latest as OngoingCallModel.InCall).isAppVisible).isTrue() } @Test fun ongoingCallNotification_appNotVisibleInitially_emitsInCall() = kosmos.runTest { - kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false val latest by collectLastValue(underTest.ongoingCallState) - addOngoingCallState(uid = UID) + addOngoingCallState(uid = UID, isAppVisible = false) assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat((latest as OngoingCallModel.InCall).isAppVisible).isFalse() } @Test @@ -164,17 +167,19 @@ class OngoingCallInteractorTest : SysuiTestCase() { val latest by collectLastValue(underTest.ongoingCallState) // Start with notification and app not visible - kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false - addOngoingCallState(uid = UID) + addOngoingCallState(uid = UID, isAppVisible = false) assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat((latest as OngoingCallModel.InCall).isAppVisible).isFalse() // App becomes visible kosmos.activityManagerRepository.fake.setIsAppVisible(UID, true) - assertThat(latest).isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java) + assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat((latest as OngoingCallModel.InCall).isAppVisible).isTrue() // App becomes invisible again kosmos.activityManagerRepository.fake.setIsAppVisible(UID, false) assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat((latest as OngoingCallModel.InCall).isAppVisible).isFalse() } @Test @@ -238,18 +243,17 @@ class OngoingCallInteractorTest : SysuiTestCase() { .ongoingProcessRequiresStatusBarVisible ) - kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false - - addOngoingCallState(uid = UID) + addOngoingCallState(uid = UID, isAppVisible = false) assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat((ongoingCallState as OngoingCallModel.InCall).isAppVisible).isFalse() assertThat(requiresStatusBarVisibleInRepository).isTrue() assertThat(requiresStatusBarVisibleInWindowController).isTrue() kosmos.activityManagerRepository.fake.setIsAppVisible(UID, true) - assertThat(ongoingCallState) - .isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java) + assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat((ongoingCallState as OngoingCallModel.InCall).isAppVisible).isTrue() assertThat(requiresStatusBarVisibleInRepository).isFalse() assertThat(requiresStatusBarVisibleInWindowController).isFalse() } @@ -265,6 +269,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { addOngoingCallState() assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) + assertThat((ongoingCallState as OngoingCallModel.InCall).isAppVisible).isFalse() verify(kosmos.swipeStatusBarAwayGestureHandler, never()) .addOnGestureDetectedCallback(any(), any()) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosTest.kt index c89dc5722c7a..33689931e6ed 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,789 +26,701 @@ import com.android.settingslib.mobile.MobileIconCarrierIdOverrides import com.android.settingslib.mobile.MobileIconCarrierIdOverridesImpl import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS +import com.android.systemui.flags.fake +import com.android.systemui.flags.featureFlagsClassic +import com.android.systemui.kairos.ActivatedKairosFixture +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.KairosTestScope +import com.android.systemui.kairos.MutableState +import com.android.systemui.kairos.kairos +import com.android.systemui.kairos.map +import com.android.systemui.kairos.runKairosTest +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.log.table.logcatTableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.CarrierMergedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType -import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepositoryKairos +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileMappingsProxy import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FIVE_G_OVERRIDE import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FOUR_G import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.THREE_G import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel -import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +@OptIn(ExperimentalKairosApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class MobileIconInteractorKairosTest : SysuiTestCase() { - private val kosmos = testKosmos() + private val kosmos = + testKosmos().apply { + useUnconfinedTestDispatcher() + featureFlagsClassic.fake.apply { setDefault(FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS) } + } - private lateinit var underTest: MobileIconInteractorKairos - private val mobileMappingsProxy = FakeMobileMappingsProxy() - private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy, mock()) + private val Kosmos.tableLogBuffer by Fixture { + logcatTableLogBuffer(this, "MobileIconInteractorKairosTest") + } - private val connectionRepository = - FakeMobileConnectionRepository( - SUB_1_ID, - logcatTableLogBuffer(kosmos, "MobileIconInteractorTest"), - ) + private var Kosmos.overrides: MobileIconCarrierIdOverrides by Fixture { + MobileIconCarrierIdOverridesImpl() + } - private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(testDispatcher) + private val Kosmos.defaultSubscriptionHasDataEnabled by Fixture { MutableState(kairos, true) } - @Before - fun setUp() { - underTest = createInteractor() + private val Kosmos.alwaysShowDataRatIcon by Fixture { MutableState(kairos, false) } - mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = true - connectionRepository.isInService.value = true - } + private val Kosmos.alwaysUseCdmaLevel by Fixture { MutableState(kairos, false) } - @Test - fun gsm_usesGsmLevel() = - testScope.runTest { - connectionRepository.isGsm.value = true - connectionRepository.primaryLevel.value = GSM_LEVEL - connectionRepository.cdmaLevel.value = CDMA_LEVEL + private val Kosmos.isSingleCarrier by Fixture { MutableState(kairos, true) } - var latest: Int? = null - val job = underTest.signalLevelIcon.onEach { latest = it.level }.launchIn(this) + private val Kosmos.mobileIsDefault by Fixture { MutableState(kairos, false) } - assertThat(latest).isEqualTo(GSM_LEVEL) + private val Kosmos.defaultMobileIconMapping by Fixture { + MutableState(kairos, fakeMobileIconsInteractor.TEST_MAPPING) + } - job.cancel() - } + private val Kosmos.defaultMobileIconGroup by Fixture { MutableState(kairos, TelephonyIcons.G) } - @Test - fun gsm_alwaysShowCdmaTrue_stillUsesGsmLevel() = - testScope.runTest { - connectionRepository.isGsm.value = true - connectionRepository.primaryLevel.value = GSM_LEVEL - connectionRepository.cdmaLevel.value = CDMA_LEVEL - mobileIconsInteractor.alwaysUseCdmaLevel.value = true + private val Kosmos.isDefaultConnectionFailed by Fixture { MutableState(kairos, false) } - var latest: Int? = null - val job = underTest.signalLevelIcon.onEach { latest = it.level }.launchIn(this) + private val Kosmos.isForceHidden by Fixture { MutableState(kairos, false) } - assertThat(latest).isEqualTo(GSM_LEVEL) + private val Kosmos.underTest by ActivatedKairosFixture { + MobileIconInteractorKairosImpl( + defaultSubscriptionHasDataEnabled, + alwaysShowDataRatIcon, + alwaysUseCdmaLevel, + isSingleCarrier, + mobileIsDefault, + defaultMobileIconMapping, + defaultMobileIconGroup, + isDefaultConnectionFailed, + isForceHidden, + connectionRepository = connectionRepo, + context = context, + carrierIdOverrides = overrides, + ) + } - job.cancel() + private val Kosmos.connectionRepo by Fixture { + FakeMobileConnectionRepositoryKairos(SUB_1_ID, kairos, tableLogBuffer).apply { + dataEnabled.setValue(true) + isInService.setValue(true) } + } + + private fun runTest(block: suspend KairosTestScope.() -> Unit) = + kosmos.run { runKairosTest { block() } } @Test - fun notGsm_level_default_unknown() = - testScope.runTest { - connectionRepository.isGsm.value = false + fun gsm_usesGsmLevel() = runTest { + connectionRepo.isGsm.setValue(true) + connectionRepo.primaryLevel.setValue(GSM_LEVEL) + connectionRepo.cdmaLevel.setValue(CDMA_LEVEL) - var latest: Int? = null - val job = underTest.signalLevelIcon.onEach { latest = it.level }.launchIn(this) + val latest by underTest.signalLevelIcon.collectLastValue() - assertThat(latest).isEqualTo(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) - job.cancel() - } + assertThat(latest?.level).isEqualTo(GSM_LEVEL) + } @Test - fun notGsm_alwaysShowCdmaTrue_usesCdmaLevel() = - testScope.runTest { - connectionRepository.isGsm.value = false - connectionRepository.primaryLevel.value = GSM_LEVEL - connectionRepository.cdmaLevel.value = CDMA_LEVEL - mobileIconsInteractor.alwaysUseCdmaLevel.value = true + fun gsm_alwaysShowCdmaTrue_stillUsesGsmLevel() = runTest { + connectionRepo.isGsm.setValue(true) + connectionRepo.primaryLevel.setValue(GSM_LEVEL) + connectionRepo.cdmaLevel.setValue(CDMA_LEVEL) + // mobileIconsInteractor.alwaysUseCdmaLevel.setValue(true) + alwaysUseCdmaLevel.setValue(true) - var latest: Int? = null - val job = underTest.signalLevelIcon.onEach { latest = it.level }.launchIn(this) + val latest by underTest.signalLevelIcon.collectLastValue() - assertThat(latest).isEqualTo(CDMA_LEVEL) - - job.cancel() - } + assertThat(latest?.level).isEqualTo(GSM_LEVEL) + } @Test - fun notGsm_alwaysShowCdmaFalse_usesPrimaryLevel() = - testScope.runTest { - connectionRepository.isGsm.value = false - connectionRepository.primaryLevel.value = GSM_LEVEL - connectionRepository.cdmaLevel.value = CDMA_LEVEL - mobileIconsInteractor.alwaysUseCdmaLevel.value = false - - var latest: Int? = null - val job = underTest.signalLevelIcon.onEach { latest = it.level }.launchIn(this) + fun notGsm_level_default_unknown() = runTest { + connectionRepo.isGsm.setValue(false) - assertThat(latest).isEqualTo(GSM_LEVEL) + val latest by underTest.signalLevelIcon.collectLastValue() - job.cancel() - } + assertThat(latest?.level).isEqualTo(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + } @Test - fun numberOfLevels_comesFromRepo_whenApplicable() = - testScope.runTest { - var latest: Int? = null - val job = - underTest.signalLevelIcon - .onEach { latest = (it as? SignalIconModel.Cellular)?.numberOfLevels } - .launchIn(this) - - connectionRepository.numberOfLevels.value = 5 - assertThat(latest).isEqualTo(5) + fun notGsm_alwaysShowCdmaTrue_usesCdmaLevel() = runTest { + connectionRepo.isGsm.setValue(false) + connectionRepo.primaryLevel.setValue(GSM_LEVEL) + connectionRepo.cdmaLevel.setValue(CDMA_LEVEL) + // mobileIconsInteractor.alwaysUseCdmaLevel.setValue(true) + alwaysUseCdmaLevel.setValue(true) - connectionRepository.numberOfLevels.value = 4 - assertThat(latest).isEqualTo(4) + val latest by underTest.signalLevelIcon.collectLastValue() - job.cancel() - } + assertThat(latest?.level).isEqualTo(CDMA_LEVEL) + } @Test - fun inflateSignalStrength_arbitrarilyAddsOneToTheReportedLevel() = - testScope.runTest { - connectionRepository.inflateSignalStrength.value = false - val latest by collectLastValue(underTest.signalLevelIcon) + fun notGsm_alwaysShowCdmaFalse_usesPrimaryLevel() = runTest { + connectionRepo.isGsm.setValue(false) + connectionRepo.primaryLevel.setValue(GSM_LEVEL) + connectionRepo.cdmaLevel.setValue(CDMA_LEVEL) + // mobileIconsInteractor.alwaysUseCdmaLevel.setValue(false) + alwaysUseCdmaLevel.setValue(false) - connectionRepository.primaryLevel.value = 4 - assertThat(latest!!.level).isEqualTo(4) + val latest by underTest.signalLevelIcon.collectLastValue() - connectionRepository.inflateSignalStrength.value = true - connectionRepository.primaryLevel.value = 4 - - // when INFLATE_SIGNAL_STRENGTH is true, we add 1 to the reported signal level - assertThat(latest!!.level).isEqualTo(5) - } + assertThat(latest?.level).isEqualTo(GSM_LEVEL) + } @Test - fun networkSlice_configOn_hasPrioritizedCaps_showsSlice() = - testScope.runTest { - connectionRepository.allowNetworkSliceIndicator.value = true - val latest by collectLastValue(underTest.showSliceAttribution) + fun numberOfLevels_comesFromRepo_whenApplicable() = runTest { + val latest by + underTest.signalLevelIcon + .map { (it as? SignalIconModel.Cellular)?.numberOfLevels } + .collectLastValue() - connectionRepository.hasPrioritizedNetworkCapabilities.value = true + connectionRepo.numberOfLevels.setValue(5) + assertThat(latest).isEqualTo(5) - assertThat(latest).isTrue() - } + connectionRepo.numberOfLevels.setValue(4) + assertThat(latest).isEqualTo(4) + } @Test - fun networkSlice_configOn_noPrioritizedCaps_noSlice() = - testScope.runTest { - connectionRepository.allowNetworkSliceIndicator.value = true - val latest by collectLastValue(underTest.showSliceAttribution) + fun inflateSignalStrength_arbitrarilyAddsOneToTheReportedLevel() = runTest { + connectionRepo.inflateSignalStrength.setValue(false) + val latest by underTest.signalLevelIcon.collectLastValue() - connectionRepository.hasPrioritizedNetworkCapabilities.value = false + connectionRepo.primaryLevel.setValue(4) + assertThat(latest!!.level).isEqualTo(4) - assertThat(latest).isFalse() - } + connectionRepo.inflateSignalStrength.setValue(true) + connectionRepo.primaryLevel.setValue(4) + + // when INFLATE_SIGNAL_STRENGTH is true, we add 1 to the reported signal level + assertThat(latest!!.level).isEqualTo(5) + } @Test - fun networkSlice_configOff_hasPrioritizedCaps_noSlice() = - testScope.runTest { - connectionRepository.allowNetworkSliceIndicator.value = false - val latest by collectLastValue(underTest.showSliceAttribution) + fun networkSlice_configOn_hasPrioritizedCaps_showsSlice() = runTest { + connectionRepo.allowNetworkSliceIndicator.setValue(true) + val latest by underTest.showSliceAttribution.collectLastValue() - connectionRepository.hasPrioritizedNetworkCapabilities.value = true + connectionRepo.hasPrioritizedNetworkCapabilities.setValue(true) - assertThat(latest).isFalse() - } + assertThat(latest).isTrue() + } @Test - fun networkSlice_configOff_noPrioritizedCaps_noSlice() = - testScope.runTest { - connectionRepository.allowNetworkSliceIndicator.value = false - val latest by collectLastValue(underTest.showSliceAttribution) + fun networkSlice_configOn_noPrioritizedCaps_noSlice() = runTest { + connectionRepo.allowNetworkSliceIndicator.setValue(true) + val latest by underTest.showSliceAttribution.collectLastValue() - connectionRepository.hasPrioritizedNetworkCapabilities.value = false + connectionRepo.hasPrioritizedNetworkCapabilities.setValue(false) - assertThat(latest).isFalse() - } + assertThat(latest).isFalse() + } @Test - fun iconGroup_three_g() = - testScope.runTest { - connectionRepository.resolvedNetworkType.value = - DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) - - var latest: NetworkTypeIconModel? = null - val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + fun networkSlice_configOff_hasPrioritizedCaps_noSlice() = runTest { + connectionRepo.allowNetworkSliceIndicator.setValue(false) + val latest by underTest.showSliceAttribution.collectLastValue() - assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)) + connectionRepo.hasPrioritizedNetworkCapabilities.setValue(true) - job.cancel() - } + assertThat(latest).isFalse() + } @Test - fun iconGroup_updates_on_change() = - testScope.runTest { - connectionRepository.resolvedNetworkType.value = - DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) + fun networkSlice_configOff_noPrioritizedCaps_noSlice() = runTest { + connectionRepo.allowNetworkSliceIndicator.setValue(false) + val latest by underTest.showSliceAttribution.collectLastValue() - var latest: NetworkTypeIconModel? = null - val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + connectionRepo.hasPrioritizedNetworkCapabilities.setValue(false) - connectionRepository.resolvedNetworkType.value = - DefaultNetworkType(mobileMappingsProxy.toIconKey(FOUR_G)) - - assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.FOUR_G)) - - job.cancel() - } + assertThat(latest).isFalse() + } @Test - fun iconGroup_5g_override_type() = - testScope.runTest { - connectionRepository.resolvedNetworkType.value = - OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(FIVE_G_OVERRIDE)) - - var latest: NetworkTypeIconModel? = null - val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + fun iconGroup_three_g() = runTest { + connectionRepo.resolvedNetworkType.setValue( + DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) + ) - assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.NR_5G)) + val latest by underTest.networkTypeIconGroup.collectLastValue() - job.cancel() - } + assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)) + } @Test - fun iconGroup_default_if_no_lookup() = - testScope.runTest { - connectionRepository.resolvedNetworkType.value = - DefaultNetworkType(mobileMappingsProxy.toIconKey(NETWORK_TYPE_UNKNOWN)) + fun iconGroup_updates_on_change() = runTest { + connectionRepo.resolvedNetworkType.setValue( + DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) + ) - var latest: NetworkTypeIconModel? = null - val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + val latest by underTest.networkTypeIconGroup.collectLastValue() - assertThat(latest) - .isEqualTo(NetworkTypeIconModel.DefaultIcon(FakeMobileIconsInteractor.DEFAULT_ICON)) + connectionRepo.resolvedNetworkType.setValue( + DefaultNetworkType(mobileMappingsProxy.toIconKey(FOUR_G)) + ) - job.cancel() - } + assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.FOUR_G)) + } @Test - fun iconGroup_carrierMerged_usesOverride() = - testScope.runTest { - connectionRepository.resolvedNetworkType.value = CarrierMergedNetworkType - - var latest: NetworkTypeIconModel? = null - val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + fun iconGroup_5g_override_type() = runTest { + connectionRepo.resolvedNetworkType.setValue( + OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(FIVE_G_OVERRIDE)) + ) - assertThat(latest) - .isEqualTo( - NetworkTypeIconModel.DefaultIcon(CarrierMergedNetworkType.iconGroupOverride) - ) + val latest by underTest.networkTypeIconGroup.collectLastValue() - job.cancel() - } + assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.NR_5G)) + } @Test - fun overrideIcon_usesCarrierIdOverride() = - testScope.runTest { - val overrides = - mock<MobileIconCarrierIdOverrides>().also { - whenever(it.carrierIdEntryExists(anyInt())).thenReturn(true) - whenever(it.getOverrideFor(anyInt(), anyString(), any())).thenReturn(1234) - } + fun iconGroup_default_if_no_lookup() = runTest { + connectionRepo.resolvedNetworkType.setValue( + DefaultNetworkType(mobileMappingsProxy.toIconKey(NETWORK_TYPE_UNKNOWN)) + ) - underTest = createInteractor(overrides) + val latest by underTest.networkTypeIconGroup.collectLastValue() - connectionRepository.resolvedNetworkType.value = - DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) + assertThat(latest) + .isEqualTo(NetworkTypeIconModel.DefaultIcon(FakeMobileIconsInteractor.DEFAULT_ICON)) + } - var latest: NetworkTypeIconModel? = null - val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + @Test + fun iconGroup_carrierMerged_usesOverride() = runTest { + connectionRepo.resolvedNetworkType.setValue(CarrierMergedNetworkType) - assertThat(latest) - .isEqualTo(NetworkTypeIconModel.OverriddenIcon(TelephonyIcons.THREE_G, 1234)) + val latest by underTest.networkTypeIconGroup.collectLastValue() - job.cancel() - } + assertThat(latest) + .isEqualTo(NetworkTypeIconModel.DefaultIcon(CarrierMergedNetworkType.iconGroupOverride)) + } @Test - fun alwaysShowDataRatIcon_matchesParent() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this) + fun overrideIcon_usesCarrierIdOverride() = runTest { + overrides = + mock<MobileIconCarrierIdOverrides> { + on { carrierIdEntryExists(anyInt()) } doReturn true + on { getOverrideFor(anyInt(), anyString(), any()) } doReturn 1234 + } - mobileIconsInteractor.alwaysShowDataRatIcon.value = true - assertThat(latest).isTrue() + connectionRepo.resolvedNetworkType.setValue( + DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) + ) - mobileIconsInteractor.alwaysShowDataRatIcon.value = false - assertThat(latest).isFalse() + val latest by underTest.networkTypeIconGroup.collectLastValue() - job.cancel() - } + assertThat(latest) + .isEqualTo(NetworkTypeIconModel.OverriddenIcon(TelephonyIcons.THREE_G, 1234)) + } @Test - fun dataState_connected() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this) + fun alwaysShowDataRatIcon_matchesParent() = runTest { + val latest by underTest.alwaysShowDataRatIcon.collectLastValue() - connectionRepository.dataConnectionState.value = DataConnectionState.Connected + // mobileIconsInteractor.alwaysShowDataRatIcon.setValue(true) + alwaysShowDataRatIcon.setValue(true) - assertThat(latest).isTrue() + assertThat(latest).isTrue() - job.cancel() - } + // mobileIconsInteractor.alwaysShowDataRatIcon.setValue(false) + alwaysShowDataRatIcon.setValue(false) - @Test - fun dataState_notConnected() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this) + assertThat(latest).isFalse() + } - connectionRepository.dataConnectionState.value = DataConnectionState.Disconnected + @Test + fun dataState_connected() = runTest { + val latest by underTest.isDataConnected.collectLastValue() - assertThat(latest).isFalse() + connectionRepo.dataConnectionState.setValue(DataConnectionState.Connected) - job.cancel() - } + assertThat(latest).isTrue() + } @Test - fun isInService_usesRepositoryValue() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.isInService.onEach { latest = it }.launchIn(this) + fun dataState_notConnected() = runTest { + val latest by underTest.isDataConnected.collectLastValue() - connectionRepository.isInService.value = true + connectionRepo.dataConnectionState.setValue(DataConnectionState.Disconnected) - assertThat(latest).isTrue() + assertThat(latest).isFalse() + } - connectionRepository.isInService.value = false + @Test + fun isInService_usesRepositoryValue() = runTest { + val latest by underTest.isInService.collectLastValue() - assertThat(latest).isFalse() + connectionRepo.isInService.setValue(true) - job.cancel() - } + assertThat(latest).isTrue() - @Test - fun roaming_isGsm_usesConnectionModel() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.isRoaming.onEach { latest = it }.launchIn(this) + connectionRepo.isInService.setValue(false) - connectionRepository.cdmaRoaming.value = true - connectionRepository.isGsm.value = true - connectionRepository.isRoaming.value = false + assertThat(latest).isFalse() + } - assertThat(latest).isFalse() + @Test + fun roaming_isGsm_usesConnectionModel() = runTest { + val latest by underTest.isRoaming.collectLastValue() - connectionRepository.isRoaming.value = true + connectionRepo.cdmaRoaming.setValue(true) + connectionRepo.isGsm.setValue(true) + connectionRepo.isRoaming.setValue(false) - assertThat(latest).isTrue() + assertThat(latest).isFalse() - job.cancel() - } + connectionRepo.isRoaming.setValue(true) - @Test - fun roaming_isCdma_usesCdmaRoamingBit() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.isRoaming.onEach { latest = it }.launchIn(this) + assertThat(latest).isTrue() + } - connectionRepository.cdmaRoaming.value = false - connectionRepository.isGsm.value = false - connectionRepository.isRoaming.value = true + @Test + fun roaming_isCdma_usesCdmaRoamingBit() = runTest { + val latest by underTest.isRoaming.collectLastValue() - assertThat(latest).isFalse() + connectionRepo.cdmaRoaming.setValue(false) + connectionRepo.isGsm.setValue(false) + connectionRepo.isRoaming.setValue(true) - connectionRepository.cdmaRoaming.value = true - connectionRepository.isGsm.value = false - connectionRepository.isRoaming.value = false + assertThat(latest).isFalse() - assertThat(latest).isTrue() + connectionRepo.cdmaRoaming.setValue(true) + connectionRepo.isGsm.setValue(false) + connectionRepo.isRoaming.setValue(false) - job.cancel() - } + assertThat(latest).isTrue() + } @Test - fun roaming_falseWhileCarrierNetworkChangeActive() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.isRoaming.onEach { latest = it }.launchIn(this) + fun roaming_falseWhileCarrierNetworkChangeActive() = runTest { + val latest by underTest.isRoaming.collectLastValue() - connectionRepository.cdmaRoaming.value = true - connectionRepository.isGsm.value = false - connectionRepository.isRoaming.value = true - connectionRepository.carrierNetworkChangeActive.value = true + connectionRepo.cdmaRoaming.setValue(true) + connectionRepo.isGsm.setValue(false) + connectionRepo.isRoaming.setValue(true) + connectionRepo.carrierNetworkChangeActive.setValue(true) - assertThat(latest).isFalse() + assertThat(latest).isFalse() - connectionRepository.cdmaRoaming.value = true - connectionRepository.isGsm.value = true + connectionRepo.cdmaRoaming.setValue(true) + connectionRepo.isGsm.setValue(true) - assertThat(latest).isFalse() - - job.cancel() - } + assertThat(latest).isFalse() + } @Test - fun networkName_usesOperatorAlphaShortWhenNonNullAndRepoIsDefault() = - testScope.runTest { - var latest: NetworkNameModel? = null - val job = underTest.networkName.onEach { latest = it }.launchIn(this) - - val testOperatorName = "operatorAlphaShort" + fun networkName_usesOperatorAlphaShortWhenNonNullAndRepoIsDefault() = runTest { + val latest by underTest.networkName.collectLastValue() - // Default network name, operator name is non-null, uses the operator name - connectionRepository.networkName.value = DEFAULT_NAME_MODEL - connectionRepository.operatorAlphaShort.value = testOperatorName + val testOperatorName = "operatorAlphaShort" - assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived(testOperatorName)) + // Default network name, operator name is non-null, uses the operator name + connectionRepo.networkName.setValue(DEFAULT_NAME_MODEL) + connectionRepo.operatorAlphaShort.setValue(testOperatorName) - // Default network name, operator name is null, uses the default - connectionRepository.operatorAlphaShort.value = null + assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived(testOperatorName)) - assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL) + // Default network name, operator name is null, uses the default + connectionRepo.operatorAlphaShort.setValue(null) - // Derived network name, operator name non-null, uses the derived name - connectionRepository.networkName.value = DERIVED_NAME_MODEL - connectionRepository.operatorAlphaShort.value = testOperatorName + assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL) - assertThat(latest).isEqualTo(DERIVED_NAME_MODEL) + // Derived network name, operator name non-null, uses the derived name + connectionRepo.networkName.setValue(DERIVED_NAME_MODEL) + connectionRepo.operatorAlphaShort.setValue(testOperatorName) - job.cancel() - } + assertThat(latest).isEqualTo(DERIVED_NAME_MODEL) + } @Test - fun networkNameForSubId_usesOperatorAlphaShortWhenNonNullAndRepoIsDefault() = - testScope.runTest { - var latest: String? = null - val job = underTest.carrierName.onEach { latest = it }.launchIn(this) - - val testOperatorName = "operatorAlphaShort" + fun networkNameForSubId_usesOperatorAlphaShortWhenNonNullAndRepoIsDefault() = runTest { + val latest by underTest.carrierName.collectLastValue() - // Default network name, operator name is non-null, uses the operator name - connectionRepository.carrierName.value = DEFAULT_NAME_MODEL - connectionRepository.operatorAlphaShort.value = testOperatorName + val testOperatorName = "operatorAlphaShort" - assertThat(latest).isEqualTo(testOperatorName) + // Default network name, operator name is non-null, uses the operator name + connectionRepo.carrierName.setValue(DEFAULT_NAME_MODEL) + connectionRepo.operatorAlphaShort.setValue(testOperatorName) - // Default network name, operator name is null, uses the default - connectionRepository.operatorAlphaShort.value = null + assertThat(latest).isEqualTo(testOperatorName) - assertThat(latest).isEqualTo(DEFAULT_NAME) + // Default network name, operator name is null, uses the default + connectionRepo.operatorAlphaShort.setValue(null) - // Derived network name, operator name non-null, uses the derived name - connectionRepository.carrierName.value = - NetworkNameModel.SubscriptionDerived(DERIVED_NAME) - connectionRepository.operatorAlphaShort.value = testOperatorName + assertThat(latest).isEqualTo(DEFAULT_NAME) - assertThat(latest).isEqualTo(DERIVED_NAME) + // Derived network name, operator name non-null, uses the derived name + connectionRepo.carrierName.setValue(NetworkNameModel.SubscriptionDerived(DERIVED_NAME)) + connectionRepo.operatorAlphaShort.setValue(testOperatorName) - job.cancel() - } + assertThat(latest).isEqualTo(DERIVED_NAME) + } @Test - fun isSingleCarrier_matchesParent() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this) + fun isSingleCarrier_matchesParent() = runTest { + val latest by underTest.isSingleCarrier.collectLastValue() - mobileIconsInteractor.isSingleCarrier.value = true - assertThat(latest).isTrue() + // mobileIconsInteractor.isSingleCarrier.setValue(true) + isSingleCarrier.setValue(true) + assertThat(latest).isTrue() - mobileIconsInteractor.isSingleCarrier.value = false - assertThat(latest).isFalse() - - job.cancel() - } + // mobileIconsInteractor.isSingleCarrier.setValue(false) + isSingleCarrier.setValue(false) + assertThat(latest).isFalse() + } @Test - fun isForceHidden_matchesParent() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this) - - mobileIconsInteractor.isForceHidden.value = true - assertThat(latest).isTrue() + fun isForceHidden_matchesParent() = runTest { + val latest by underTest.isForceHidden.collectLastValue() - mobileIconsInteractor.isForceHidden.value = false - assertThat(latest).isFalse() + // mobileIconsInteractor.isForceHidden.setValue(true) + isForceHidden.setValue(true) + assertThat(latest).isTrue() - job.cancel() - } + // mobileIconsInteractor.isForceHidden.setValue(false) + isForceHidden.setValue(false) + assertThat(latest).isFalse() + } @Test - fun isAllowedDuringAirplaneMode_matchesRepo() = - testScope.runTest { - val latest by collectLastValue(underTest.isAllowedDuringAirplaneMode) + fun isAllowedDuringAirplaneMode_matchesRepo() = runTest { + val latest by underTest.isAllowedDuringAirplaneMode.collectLastValue() - connectionRepository.isAllowedDuringAirplaneMode.value = true - assertThat(latest).isTrue() + connectionRepo.isAllowedDuringAirplaneMode.setValue(true) + assertThat(latest).isTrue() - connectionRepository.isAllowedDuringAirplaneMode.value = false - assertThat(latest).isFalse() - } + connectionRepo.isAllowedDuringAirplaneMode.setValue(false) + assertThat(latest).isFalse() + } @Test - fun cellBasedIconId_correctLevel_notCutout() = - testScope.runTest { - connectionRepository.isNonTerrestrial.value = false - connectionRepository.isInService.value = true - connectionRepository.primaryLevel.value = 1 - connectionRepository.setDataEnabled(false) - connectionRepository.isNonTerrestrial.value = false + fun cellBasedIconId_correctLevel_notCutout() = runTest { + connectionRepo.isNonTerrestrial.setValue(false) + connectionRepo.isInService.setValue(true) + connectionRepo.primaryLevel.setValue(1) + connectionRepo.dataEnabled.setValue(true) + connectionRepo.isNonTerrestrial.setValue(false) - var latest: SignalIconModel.Cellular? = null - val job = - underTest.signalLevelIcon - .onEach { latest = it as? SignalIconModel.Cellular } - .launchIn(this) + val latest by + underTest.signalLevelIcon.map { it as? SignalIconModel.Cellular }.collectLastValue() - assertThat(latest?.level).isEqualTo(1) - assertThat(latest?.showExclamationMark).isFalse() + assertThat(latest?.level).isEqualTo(1) - job.cancel() - } + // TODO: need to provision MobileIconsInteractorKairos#isDefaultConnectionFailed + + // defaultSubscriptionHasDataEnabled? + assertThat(latest?.showExclamationMark).isEqualTo(false) + } @Test - fun icon_usesLevelFromInteractor() = - testScope.runTest { - connectionRepository.isNonTerrestrial.value = false - connectionRepository.isInService.value = true + fun icon_usesLevelFromInteractor() = runTest { + connectionRepo.isNonTerrestrial.setValue(false) + connectionRepo.isInService.setValue(true) - var latest: SignalIconModel? = null - val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this) + val latest by underTest.signalLevelIcon.collectLastValue() - connectionRepository.primaryLevel.value = 3 - assertThat(latest!!.level).isEqualTo(3) + connectionRepo.primaryLevel.setValue(3) + assertThat(latest!!.level).isEqualTo(3) - connectionRepository.primaryLevel.value = 1 - assertThat(latest!!.level).isEqualTo(1) - - job.cancel() - } + connectionRepo.primaryLevel.setValue(1) + assertThat(latest!!.level).isEqualTo(1) + } @Test - fun cellBasedIcon_usesNumberOfLevelsFromInteractor() = - testScope.runTest { - connectionRepository.isNonTerrestrial.value = false - - var latest: SignalIconModel.Cellular? = null - val job = - underTest.signalLevelIcon - .onEach { latest = it as? SignalIconModel.Cellular } - .launchIn(this) + fun cellBasedIcon_usesNumberOfLevelsFromInteractor() = runTest { + connectionRepo.isNonTerrestrial.setValue(false) - connectionRepository.numberOfLevels.value = 5 - assertThat(latest!!.numberOfLevels).isEqualTo(5) + val latest by + underTest.signalLevelIcon.map { it as? SignalIconModel.Cellular }.collectLastValue() - connectionRepository.numberOfLevels.value = 2 - assertThat(latest!!.numberOfLevels).isEqualTo(2) + connectionRepo.numberOfLevels.setValue(5) + assertThat(latest!!.numberOfLevels).isEqualTo(5) - job.cancel() - } + connectionRepo.numberOfLevels.setValue(2) + assertThat(latest!!.numberOfLevels).isEqualTo(2) + } @Test - fun cellBasedIcon_defaultDataDisabled_showExclamationTrue() = - testScope.runTest { - connectionRepository.isNonTerrestrial.value = false - mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = false - - var latest: SignalIconModel.Cellular? = null - val job = - underTest.signalLevelIcon - .onEach { latest = it as? SignalIconModel.Cellular } - .launchIn(this) + fun cellBasedIcon_defaultDataDisabled_showExclamationTrue() = runTest { + connectionRepo.isNonTerrestrial.setValue(false) + connectionRepo.dataEnabled.setValue(false) + defaultSubscriptionHasDataEnabled.setValue(false) - assertThat(latest!!.showExclamationMark).isTrue() + val latest by underTest.signalLevelIcon.collectLastValue() - job.cancel() - } + assertThat((latest!! as SignalIconModel.Cellular).showExclamationMark).isTrue() + } @Test - fun cellBasedIcon_defaultConnectionFailed_showExclamationTrue() = - testScope.runTest { - connectionRepository.isNonTerrestrial.value = false - mobileIconsInteractor.isDefaultConnectionFailed.value = true + fun cellBasedIcon_defaultConnectionFailed_showExclamationTrue() = runTest { + connectionRepo.isNonTerrestrial.setValue(false) + // mobileIconsInteractor.isDefaultConnectionFailed.setValue(true) + isDefaultConnectionFailed.setValue(true) - var latest: SignalIconModel.Cellular? = null - val job = - underTest.signalLevelIcon - .onEach { latest = it as? SignalIconModel.Cellular } - .launchIn(this) + val latest by underTest.signalLevelIcon.collectLastValue() - assertThat(latest!!.showExclamationMark).isTrue() - - job.cancel() - } + assertThat((latest!! as SignalIconModel.Cellular).showExclamationMark).isTrue() + } @Test - fun cellBasedIcon_enabledAndNotFailed_showExclamationFalse() = - testScope.runTest { - connectionRepository.isNonTerrestrial.value = false - connectionRepository.isInService.value = true - mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = true - mobileIconsInteractor.isDefaultConnectionFailed.value = false - - var latest: SignalIconModel.Cellular? = null - val job = - underTest.signalLevelIcon - .onEach { latest = it as? SignalIconModel.Cellular } - .launchIn(this) + fun cellBasedIcon_enabledAndNotFailed_showExclamationFalse() = runTest { + connectionRepo.isNonTerrestrial.setValue(false) + connectionRepo.isInService.setValue(true) + connectionRepo.dataEnabled.setValue(true) + // mobileIconsInteractor.isDefaultConnectionFailed.setValue(false) + isDefaultConnectionFailed.setValue(false) - assertThat(latest!!.showExclamationMark).isFalse() + val latest by underTest.signalLevelIcon.collectLastValue() - job.cancel() - } + assertThat((latest!! as SignalIconModel.Cellular).showExclamationMark).isFalse() + } @Test - fun cellBasedIcon_usesEmptyState_whenNotInService() = - testScope.runTest { - var latest: SignalIconModel.Cellular? = null - val job = - underTest.signalLevelIcon - .onEach { latest = it as? SignalIconModel.Cellular } - .launchIn(this) + fun cellBasedIcon_usesEmptyState_whenNotInService() = runTest { + val latest by + underTest.signalLevelIcon.map { it as SignalIconModel.Cellular }.collectLastValue() - connectionRepository.isNonTerrestrial.value = false - connectionRepository.isInService.value = false + connectionRepo.isNonTerrestrial.setValue(false) + connectionRepo.isInService.setValue(false) - assertThat(latest?.level).isEqualTo(0) - assertThat(latest?.showExclamationMark).isTrue() + assertThat(latest?.level).isEqualTo(0) + assertThat(latest?.showExclamationMark).isTrue() - // Changing the level doesn't overwrite the disabled state - connectionRepository.primaryLevel.value = 2 - assertThat(latest?.level).isEqualTo(0) - assertThat(latest?.showExclamationMark).isTrue() + // Changing the level doesn't overwrite the disabled state + connectionRepo.primaryLevel.setValue(2) + assertThat(latest?.level).isEqualTo(0) + assertThat(latest?.showExclamationMark).isTrue() - // Once back in service, the regular icon appears - connectionRepository.isInService.value = true - assertThat(latest?.level).isEqualTo(2) - assertThat(latest?.showExclamationMark).isFalse() - - job.cancel() - } + // Once back in service, the regular icon appears + connectionRepo.isInService.setValue(true) + assertThat(latest?.level).isEqualTo(2) + assertThat(latest?.showExclamationMark).isFalse() + } @Test - fun cellBasedIcon_usesCarrierNetworkState_whenInCarrierNetworkChangeMode() = - testScope.runTest { - var latest: SignalIconModel.Cellular? = null - val job = - underTest.signalLevelIcon - .onEach { latest = it as? SignalIconModel.Cellular? } - .launchIn(this) - - connectionRepository.isNonTerrestrial.value = false - connectionRepository.isInService.value = true - connectionRepository.carrierNetworkChangeActive.value = true - connectionRepository.primaryLevel.value = 1 - connectionRepository.cdmaLevel.value = 1 + fun cellBasedIcon_usesCarrierNetworkState_whenInCarrierNetworkChangeMode() = runTest { + val latest by + underTest.signalLevelIcon.map { it as SignalIconModel.Cellular }.collectLastValue() - assertThat(latest!!.level).isEqualTo(1) - assertThat(latest!!.carrierNetworkChange).isTrue() + connectionRepo.isNonTerrestrial.setValue(false) + connectionRepo.isInService.setValue(true) + connectionRepo.carrierNetworkChangeActive.setValue(true) + connectionRepo.primaryLevel.setValue(1) + connectionRepo.cdmaLevel.setValue(1) - // SignalIconModel respects the current level - connectionRepository.primaryLevel.value = 2 + assertThat(latest!!.level).isEqualTo(1) + assertThat(latest!!.carrierNetworkChange).isTrue() - assertThat(latest!!.level).isEqualTo(2) - assertThat(latest!!.carrierNetworkChange).isTrue() + // SignalIconModel respects the current level + connectionRepo.primaryLevel.setValue(2) - job.cancel() - } + assertThat(latest!!.level).isEqualTo(2) + assertThat(latest!!.carrierNetworkChange).isTrue() + } @Test - fun satBasedIcon_isUsedWhenNonTerrestrial() = - testScope.runTest { - val latest by collectLastValue(underTest.signalLevelIcon) + fun satBasedIcon_isUsedWhenNonTerrestrial() = runTest { + val latest by underTest.signalLevelIcon.collectLastValue() - // Start off using cellular - assertThat(latest).isInstanceOf(SignalIconModel.Cellular::class.java) + // Start off using cellular + assertThat(latest).isInstanceOf(SignalIconModel.Cellular::class.java) - connectionRepository.isNonTerrestrial.value = true + connectionRepo.isNonTerrestrial.setValue(true) - assertThat(latest).isInstanceOf(SignalIconModel.Satellite::class.java) - } + assertThat(latest).isInstanceOf(SignalIconModel.Satellite::class.java) + } @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) @Test // See b/346904529 for more context - fun satBasedIcon_doesNotInflateSignalStrength_flagOff() = - testScope.runTest { - val latest by collectLastValue(underTest.signalLevelIcon) + fun satBasedIcon_doesNotInflateSignalStrength_flagOff() = runTest { + val latest by underTest.signalLevelIcon.collectLastValue() - // GIVEN a satellite connection - connectionRepository.isNonTerrestrial.value = true - // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH - connectionRepository.inflateSignalStrength.value = true + // GIVEN a satellite connection + connectionRepo.isNonTerrestrial.setValue(true) + // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH + connectionRepo.inflateSignalStrength.setValue(true) - connectionRepository.primaryLevel.value = 4 - assertThat(latest!!.level).isEqualTo(4) + connectionRepo.primaryLevel.setValue(4) + assertThat(latest!!.level).isEqualTo(4) - connectionRepository.inflateSignalStrength.value = true - connectionRepository.primaryLevel.value = 4 + connectionRepo.inflateSignalStrength.setValue(true) + connectionRepo.primaryLevel.setValue(4) - // Icon level is unaffected - assertThat(latest!!.level).isEqualTo(4) - } + // Icon level is unaffected + assertThat(latest!!.level).isEqualTo(4) + } @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) @Test // See b/346904529 for more context - fun satBasedIcon_doesNotInflateSignalStrength_flagOn() = - testScope.runTest { - val latest by collectLastValue(underTest.signalLevelIcon) + fun satBasedIcon_doesNotInflateSignalStrength_flagOn() = runTest { + val latest by underTest.signalLevelIcon.collectLastValue() - // GIVEN a satellite connection - connectionRepository.isNonTerrestrial.value = true - // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH - connectionRepository.inflateSignalStrength.value = true + // GIVEN a satellite connection + connectionRepo.isNonTerrestrial.setValue(true) + // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH + connectionRepo.inflateSignalStrength.setValue(true) - connectionRepository.satelliteLevel.value = 4 - assertThat(latest!!.level).isEqualTo(4) + connectionRepo.satelliteLevel.setValue(4) + assertThat(latest!!.level).isEqualTo(4) - connectionRepository.inflateSignalStrength.value = true - connectionRepository.primaryLevel.value = 4 + connectionRepo.inflateSignalStrength.setValue(true) + connectionRepo.primaryLevel.setValue(4) - // Icon level is unaffected - assertThat(latest!!.level).isEqualTo(4) - } + // Icon level is unaffected + assertThat(latest!!.level).isEqualTo(4) + } @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) @Test - fun satBasedIcon_usesPrimaryLevel_flagOff() = - testScope.runTest { - val latest by collectLastValue(underTest.signalLevelIcon) + fun satBasedIcon_usesPrimaryLevel_flagOff() = runTest { + val latest by underTest.signalLevelIcon.collectLastValue() - // GIVEN a satellite connection - connectionRepository.isNonTerrestrial.value = true + // GIVEN a satellite connection + connectionRepo.isNonTerrestrial.setValue(true) - // GIVEN primary level is set - connectionRepository.primaryLevel.value = 4 - connectionRepository.satelliteLevel.value = 0 + // GIVEN primary level is set + connectionRepo.primaryLevel.setValue(4) + connectionRepo.satelliteLevel.setValue(0) - // THEN icon uses the primary level because the flag is off - assertThat(latest!!.level).isEqualTo(4) - } + // THEN icon uses the primary level because the flag is off + assertThat(latest!!.level).isEqualTo(4) + } @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) @Test - fun satBasedIcon_usesSatelliteLevel_flagOn() = - testScope.runTest { - val latest by collectLastValue(underTest.signalLevelIcon) + fun satBasedIcon_usesSatelliteLevel_flagOn() = runTest { + val latest by underTest.signalLevelIcon.collectLastValue() - // GIVEN a satellite connection - connectionRepository.isNonTerrestrial.value = true + // GIVEN a satellite connection + connectionRepo.isNonTerrestrial.setValue(true) - // GIVEN satellite level is set - connectionRepository.satelliteLevel.value = 4 - connectionRepository.primaryLevel.value = 0 + // GIVEN satellite level is set + connectionRepo.satelliteLevel.setValue(4) + connectionRepo.primaryLevel.setValue(0) - // THEN icon uses the satellite level because the flag is on - assertThat(latest!!.level).isEqualTo(4) - } + // THEN icon uses the satellite level because the flag is on + assertThat(latest!!.level).isEqualTo(4) + } /** * Context (b/377518113), this test will not be needed after FLAG_CARRIER_ROAMING_NB_IOT_NTN is @@ -816,43 +728,23 @@ class MobileIconInteractorKairosTest : SysuiTestCase() { */ @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) @Test - fun satBasedIcon_reportsLevelZeroWhenOutOfService() = - testScope.runTest { - val latest by collectLastValue(underTest.signalLevelIcon) - - // GIVEN a satellite connection - connectionRepository.isNonTerrestrial.value = true - // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH - connectionRepository.inflateSignalStrength.value = true + fun satBasedIcon_reportsLevelZeroWhenOutOfService() = runTest { + val latest by underTest.signalLevelIcon.collectLastValue() - connectionRepository.primaryLevel.value = 4 - assertThat(latest!!.level).isEqualTo(4) + // GIVEN a satellite connection + connectionRepo.isNonTerrestrial.setValue(true) + // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH + connectionRepo.inflateSignalStrength.setValue(true) - connectionRepository.isInService.value = false - connectionRepository.primaryLevel.value = 4 + connectionRepo.primaryLevel.setValue(4) + assertThat(latest!!.level).isEqualTo(4) - // THEN level reports 0, by policy - assertThat(latest!!.level).isEqualTo(0) - } + connectionRepo.isInService.setValue(false) + connectionRepo.primaryLevel.setValue(4) - private fun createInteractor( - overrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl() - ) = - MobileIconInteractorKairosImpl( - testScope.backgroundScope, - mobileIconsInteractor.activeDataConnectionHasDataEnabled, - mobileIconsInteractor.alwaysShowDataRatIcon, - mobileIconsInteractor.alwaysUseCdmaLevel, - mobileIconsInteractor.isSingleCarrier, - mobileIconsInteractor.mobileIsDefault, - mobileIconsInteractor.defaultMobileIconMapping, - mobileIconsInteractor.defaultMobileIconGroup, - mobileIconsInteractor.isDefaultConnectionFailed, - mobileIconsInteractor.isForceHidden, - connectionRepository, - context, - overrides, - ) + // THEN level reports 0, by policy + assertThat(latest!!.level).isEqualTo(0) + } companion object { private const val GSM_LEVEL = 1 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosTest.kt index a9360d139a3d..4f6439d2d0b2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,186 +28,164 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.flags.Flags import com.android.systemui.flags.fake import com.android.systemui.flags.featureFlagsClassic +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.KairosTestScope +import com.android.systemui.kairos.runKairosTest import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.core.NewStatusBarIcons import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel -import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake -import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository -import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository -import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepositoryLogbufferName +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepositoryKairos +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepositoryKairos import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot import com.android.systemui.statusbar.pipeline.shared.data.repository.connectivityRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.fake -import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository import com.android.systemui.testKosmos -import com.android.systemui.util.CarrierConfigTracker +import com.android.systemui.util.carrierConfigTracker import com.google.common.truth.Truth.assertThat import java.util.UUID import kotlinx.coroutines.test.advanceTimeBy import org.junit.Test import org.junit.runner.RunWith -import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +@OptIn(ExperimentalKairosApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class MobileIconsInteractorKairosTest : SysuiTestCase() { - private val kosmos by lazy { + + private val kosmos = testKosmos().apply { - mobileConnectionsRepositoryLogbufferName = "MobileIconsInteractorTest" - mobileConnectionsRepository.fake.run { - setMobileConnectionRepositoryMap( - mapOf( - SUB_1_ID to FakeMobileConnectionRepository(SUB_1_ID, mock()), - SUB_2_ID to FakeMobileConnectionRepository(SUB_2_ID, mock()), - SUB_3_ID to FakeMobileConnectionRepository(SUB_3_ID, mock()), - SUB_4_ID to FakeMobileConnectionRepository(SUB_4_ID, mock()), - ) - ) - setActiveMobileDataSubscriptionId(SUB_1_ID) - } + useUnconfinedTestDispatcher() + mobileConnectionsRepositoryKairos = + fakeMobileConnectionsRepositoryKairos.apply { + setActiveMobileDataSubscriptionId(SUB_1_ID) + subscriptions.setValue(listOf(SUB_1, SUB_2, SUB_3_OPP, SUB_4_OPP)) + } featureFlagsClassic.fake.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true) } - } - // shortcut rename - private val Kosmos.connectionsRepository by Fixture { mobileConnectionsRepository.fake } - - private val Kosmos.carrierConfigTracker by Fixture { mock<CarrierConfigTracker>() } - - private val Kosmos.underTest by Fixture { - MobileIconsInteractorKairosImpl( - mobileConnectionsRepository, - carrierConfigTracker, - tableLogger = mock(), - connectivityRepository, - FakeUserSetupRepository(), - testScope.backgroundScope, - context, - featureFlagsClassic, - ) - } + private val Kosmos.underTest + get() = mobileIconsInteractorKairos + + private fun runTest(block: suspend KairosTestScope.() -> Unit) = + kosmos.run { runKairosTest { block() } } @Test - fun filteredSubscriptions_default() = - kosmos.runTest { - val latest by collectLastValue(underTest.filteredSubscriptions) + fun filteredSubscriptions_default() = runTest { + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(emptyList()) + val latest by underTest.filteredSubscriptions.collectLastValue() - assertThat(latest).isEqualTo(listOf<SubscriptionModel>()) - } + assertThat(latest).isEqualTo(emptyList<SubscriptionModel>()) + } // Based on the logic from the old pipeline, we'll never filter subs when there are more than 2 @Test - fun filteredSubscriptions_moreThanTwo_doesNotFilter() = - kosmos.runTest { - connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP)) - connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID) + fun filteredSubscriptions_moreThanTwo_doesNotFilter() = runTest { + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue( + listOf(SUB_1, SUB_3_OPP, SUB_4_OPP) + ) + mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_4_ID) - val latest by collectLastValue(underTest.filteredSubscriptions) + val latest by underTest.filteredSubscriptions.collectLastValue() - assertThat(latest).isEqualTo(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP)) - } + assertThat(latest).isEqualTo(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP)) + } @Test - fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() = - kosmos.runTest { - connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) + fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() = runTest { + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2)) - val latest by collectLastValue(underTest.filteredSubscriptions) + val latest by underTest.filteredSubscriptions.collectLastValue() - assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2)) - } + assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2)) + } @Test - fun filteredSubscriptions_opportunistic_differentGroups_doesNotFilter() = - kosmos.runTest { - connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP)) - connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) + fun filteredSubscriptions_opportunistic_differentGroups_doesNotFilter() = runTest { + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_3_OPP, SUB_4_OPP)) + mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID) - val latest by collectLastValue(underTest.filteredSubscriptions) + val latest by underTest.filteredSubscriptions.collectLastValue() - assertThat(latest).isEqualTo(listOf(SUB_3_OPP, SUB_4_OPP)) - } + assertThat(latest).isEqualTo(listOf(SUB_3_OPP, SUB_4_OPP)) + } @Test - fun filteredSubscriptions_opportunistic_nonGrouped_doesNotFilter() = - kosmos.runTest { - val (sub1, sub2) = - createSubscriptionPair( - subscriptionIds = Pair(SUB_1_ID, SUB_2_ID), - opportunistic = Pair(true, true), - grouped = false, - ) - connectionsRepository.setSubscriptions(listOf(sub1, sub2)) - connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID) + fun filteredSubscriptions_opportunistic_nonGrouped_doesNotFilter() = runTest { + val (sub1, sub2) = + createSubscriptionPair( + subscriptionIds = Pair(SUB_1_ID, SUB_2_ID), + opportunistic = Pair(true, true), + grouped = false, + ) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub2)) + mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_1_ID) - val latest by collectLastValue(underTest.filteredSubscriptions) + val latest by underTest.filteredSubscriptions.collectLastValue() - assertThat(latest).isEqualTo(listOf(sub1, sub2)) - } + assertThat(latest).isEqualTo(listOf(sub1, sub2)) + } @Test - fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_3() = - kosmos.runTest { - val (sub3, sub4) = - createSubscriptionPair( - subscriptionIds = Pair(SUB_3_ID, SUB_4_ID), - opportunistic = Pair(true, true), - grouped = true, - ) - connectionsRepository.setSubscriptions(listOf(sub3, sub4)) - connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) - whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) - .thenReturn(false) + fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_3() = runTest { + val (sub3, sub4) = + createSubscriptionPair( + subscriptionIds = Pair(SUB_3_ID, SUB_4_ID), + opportunistic = Pair(true, true), + grouped = true, + ) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub3, sub4)) + mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID) + whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) + .thenReturn(false) - val latest by collectLastValue(underTest.filteredSubscriptions) + val latest by underTest.filteredSubscriptions.collectLastValue() - // Filtered subscriptions should show the active one when the config is false - assertThat(latest).isEqualTo(listOf(sub3)) - } + // Filtered subscriptions should show the active one when the config is false + assertThat(latest).isEqualTo(listOf(sub3)) + } @Test - fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_4() = - kosmos.runTest { - val (sub3, sub4) = - createSubscriptionPair( - subscriptionIds = Pair(SUB_3_ID, SUB_4_ID), - opportunistic = Pair(true, true), - grouped = true, - ) - connectionsRepository.setSubscriptions(listOf(sub3, sub4)) - connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID) - whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) - .thenReturn(false) + fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_4() = runTest { + val (sub3, sub4) = + createSubscriptionPair( + subscriptionIds = Pair(SUB_3_ID, SUB_4_ID), + opportunistic = Pair(true, true), + grouped = true, + ) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub3, sub4)) + mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_4_ID) + whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) + .thenReturn(false) - val latest by collectLastValue(underTest.filteredSubscriptions) + val latest by underTest.filteredSubscriptions.collectLastValue() - // Filtered subscriptions should show the active one when the config is false - assertThat(latest).isEqualTo(listOf(sub4)) - } + // Filtered subscriptions should show the active one when the config is false + assertThat(latest).isEqualTo(listOf(sub4)) + } @Test fun filteredSubscriptions_oneOpportunistic_grouped_configTrue_showsPrimary_active_1() = - kosmos.runTest { + runTest { val (sub1, sub3) = createSubscriptionPair( subscriptionIds = Pair(SUB_1_ID, SUB_3_ID), opportunistic = Pair(false, true), grouped = true, ) - connectionsRepository.setSubscriptions(listOf(sub1, sub3)) - connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub3)) + mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_1_ID) whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) .thenReturn(true) - val latest by collectLastValue(underTest.filteredSubscriptions) + val latest by underTest.filteredSubscriptions.collectLastValue() // Filtered subscriptions should show the primary (non-opportunistic) if the config is // true @@ -216,19 +194,19 @@ class MobileIconsInteractorKairosTest : SysuiTestCase() { @Test fun filteredSubscriptions_oneOpportunistic_grouped_configTrue_showsPrimary_nonActive_1() = - kosmos.runTest { + runTest { val (sub1, sub3) = createSubscriptionPair( subscriptionIds = Pair(SUB_1_ID, SUB_3_ID), opportunistic = Pair(false, true), grouped = true, ) - connectionsRepository.setSubscriptions(listOf(sub1, sub3)) - connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub3)) + mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID) whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) .thenReturn(true) - val latest by collectLastValue(underTest.filteredSubscriptions) + val latest by underTest.filteredSubscriptions.collectLastValue() // Filtered subscriptions should show the primary (non-opportunistic) if the config is // true @@ -236,135 +214,130 @@ class MobileIconsInteractorKairosTest : SysuiTestCase() { } @Test - fun filteredSubscriptions_vcnSubId_agreesWithActiveSubId_usesActiveAkaVcnSub() = - kosmos.runTest { - val (sub1, sub3) = - createSubscriptionPair( - subscriptionIds = Pair(SUB_1_ID, SUB_3_ID), - opportunistic = Pair(true, true), - grouped = true, - ) - connectionsRepository.setSubscriptions(listOf(sub1, sub3)) - connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) - kosmos.connectivityRepository.fake.vcnSubId.value = SUB_3_ID - whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) - .thenReturn(false) + fun filteredSubscriptions_vcnSubId_agreesWithActiveSubId_usesActiveAkaVcnSub() = runTest { + val (sub1, sub3) = + createSubscriptionPair( + subscriptionIds = Pair(SUB_1_ID, SUB_3_ID), + opportunistic = Pair(true, true), + grouped = true, + ) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub3)) + mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID) + kosmos.connectivityRepository.fake.vcnSubId.value = SUB_3_ID + whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) + .thenReturn(false) - val latest by collectLastValue(underTest.filteredSubscriptions) + val latest by underTest.filteredSubscriptions.collectLastValue() - assertThat(latest).isEqualTo(listOf(sub3)) - } + assertThat(latest).isEqualTo(listOf(sub3)) + } @Test - fun filteredSubscriptions_vcnSubId_disagreesWithActiveSubId_usesVcnSub() = - kosmos.runTest { - val (sub1, sub3) = - createSubscriptionPair( - subscriptionIds = Pair(SUB_1_ID, SUB_3_ID), - opportunistic = Pair(true, true), - grouped = true, - ) - connectionsRepository.setSubscriptions(listOf(sub1, sub3)) - connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) - kosmos.connectivityRepository.fake.vcnSubId.value = SUB_1_ID - whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) - .thenReturn(false) + fun filteredSubscriptions_vcnSubId_disagreesWithActiveSubId_usesVcnSub() = runTest { + val (sub1, sub3) = + createSubscriptionPair( + subscriptionIds = Pair(SUB_1_ID, SUB_3_ID), + opportunistic = Pair(true, true), + grouped = true, + ) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub3)) + mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID) + kosmos.connectivityRepository.fake.vcnSubId.value = SUB_1_ID + whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) + .thenReturn(false) - val latest by collectLastValue(underTest.filteredSubscriptions) + val latest by underTest.filteredSubscriptions.collectLastValue() - assertThat(latest).isEqualTo(listOf(sub1)) - } + assertThat(latest).isEqualTo(listOf(sub1)) + } @Test - fun filteredSubscriptions_doesNotFilterProvisioningWhenFlagIsFalse() = - kosmos.runTest { - // GIVEN the flag is false - featureFlagsClassic.fake.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, false) + fun filteredSubscriptions_doesNotFilterProvisioningWhenFlagIsFalse() = runTest { + // GIVEN the flag is false + featureFlagsClassic.fake.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, false) - // GIVEN 1 sub that is in PROFILE_CLASS_PROVISIONING - val sub1 = - SubscriptionModel( - subscriptionId = SUB_1_ID, - isOpportunistic = false, - carrierName = "Carrier 1", - profileClass = PROFILE_CLASS_PROVISIONING, - ) + // GIVEN 1 sub that is in PROFILE_CLASS_PROVISIONING + val sub1 = + SubscriptionModel( + subscriptionId = SUB_1_ID, + isOpportunistic = false, + carrierName = "Carrier 1", + profileClass = PROFILE_CLASS_PROVISIONING, + ) - connectionsRepository.setSubscriptions(listOf(sub1)) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1)) - // WHEN filtering is applied - val latest by collectLastValue(underTest.filteredSubscriptions) + // WHEN filtering is applied + val latest by underTest.filteredSubscriptions.collectLastValue() - // THEN the provisioning sub is still present (unfiltered) - assertThat(latest).isEqualTo(listOf(sub1)) - } + // THEN the provisioning sub is still present (unfiltered) + assertThat(latest).isEqualTo(listOf(sub1)) + } @Test - fun filteredSubscriptions_filtersOutProvisioningSubs() = - kosmos.runTest { - val sub1 = - SubscriptionModel( - subscriptionId = SUB_1_ID, - isOpportunistic = false, - carrierName = "Carrier 1", - profileClass = PROFILE_CLASS_UNSET, - ) - val sub2 = - SubscriptionModel( - subscriptionId = SUB_2_ID, - isOpportunistic = false, - carrierName = "Carrier 2", - profileClass = PROFILE_CLASS_PROVISIONING, - ) + fun filteredSubscriptions_filtersOutProvisioningSubs() = runTest { + val sub1 = + SubscriptionModel( + subscriptionId = SUB_1_ID, + isOpportunistic = false, + carrierName = "Carrier 1", + profileClass = PROFILE_CLASS_UNSET, + ) + val sub2 = + SubscriptionModel( + subscriptionId = SUB_2_ID, + isOpportunistic = false, + carrierName = "Carrier 2", + profileClass = PROFILE_CLASS_PROVISIONING, + ) - connectionsRepository.setSubscriptions(listOf(sub1, sub2)) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub2)) - val latest by collectLastValue(underTest.filteredSubscriptions) + val latest by underTest.filteredSubscriptions.collectLastValue() - assertThat(latest).isEqualTo(listOf(sub1)) - } + assertThat(latest).isEqualTo(listOf(sub1)) + } /** Note: I'm not sure if this will ever be the case, but we can test it at least */ @Test - fun filteredSubscriptions_filtersOutProvisioningSubsBeforeOpportunistic() = - kosmos.runTest { - // This is a contrived test case, where the active subId is the one that would - // also be filtered by opportunistic filtering. - - // GIVEN grouped, opportunistic subscriptions - val groupUuid = ParcelUuid(UUID.randomUUID()) - val sub1 = - SubscriptionModel( - subscriptionId = 1, - isOpportunistic = true, - groupUuid = groupUuid, - carrierName = "Carrier 1", - profileClass = PROFILE_CLASS_PROVISIONING, - ) + fun filteredSubscriptions_filtersOutProvisioningSubsBeforeOpportunistic() = runTest { + // This is a contrived test case, where the active subId is the one that would + // also be filtered by opportunistic filtering. - val sub2 = - SubscriptionModel( - subscriptionId = 2, - isOpportunistic = true, - groupUuid = groupUuid, - carrierName = "Carrier 2", - profileClass = PROFILE_CLASS_UNSET, - ) + // GIVEN grouped, opportunistic subscriptions + val groupUuid = ParcelUuid(UUID.randomUUID()) + val sub1 = + SubscriptionModel( + subscriptionId = 1, + isOpportunistic = true, + groupUuid = groupUuid, + carrierName = "Carrier 1", + profileClass = PROFILE_CLASS_PROVISIONING, + ) - // GIVEN active subId is 1 - connectionsRepository.setSubscriptions(listOf(sub1, sub2)) - connectionsRepository.setActiveMobileDataSubscriptionId(1) + val sub2 = + SubscriptionModel( + subscriptionId = 2, + isOpportunistic = true, + groupUuid = groupUuid, + carrierName = "Carrier 2", + profileClass = PROFILE_CLASS_UNSET, + ) - // THEN filtering of provisioning subs takes place first, and we result in sub2 + // GIVEN active subId is 1 + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub2)) + mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(1) - val latest by collectLastValue(underTest.filteredSubscriptions) + // THEN filtering of provisioning subs takes place first, and we result in sub2 - assertThat(latest).isEqualTo(listOf(sub2)) - } + val latest by underTest.filteredSubscriptions.collectLastValue() + + assertThat(latest).isEqualTo(listOf(sub2)) + } @Test fun filteredSubscriptions_groupedPairAndNonProvisioned_groupedFilteringStillHappens() = - kosmos.runTest { + runTest { // Grouped filtering only happens when the list of subs is length 2. In this case // we'll show that filtering of provisioning subs happens before, and thus grouped // filtering happens even though the unfiltered list is length 3 @@ -384,87 +357,88 @@ class MobileIconsInteractorKairosTest : SysuiTestCase() { profileClass = PROFILE_CLASS_PROVISIONING, ) - connectionsRepository.setSubscriptions(listOf(sub1, sub2, sub3)) - connectionsRepository.setActiveMobileDataSubscriptionId(1) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub2, sub3)) + mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(1) - val latest by collectLastValue(underTest.filteredSubscriptions) + val latest by underTest.filteredSubscriptions.collectLastValue() assertThat(latest).isEqualTo(listOf(sub1)) } @Test - fun filteredSubscriptions_subNotExclusivelyNonTerrestrial_hasSub() = - kosmos.runTest { - val notExclusivelyNonTerrestrialSub = - SubscriptionModel( - isExclusivelyNonTerrestrial = false, - subscriptionId = 5, - carrierName = "Carrier 5", - profileClass = PROFILE_CLASS_UNSET, - ) + fun filteredSubscriptions_subNotExclusivelyNonTerrestrial_hasSub() = runTest { + val notExclusivelyNonTerrestrialSub = + SubscriptionModel( + isExclusivelyNonTerrestrial = false, + subscriptionId = 5, + carrierName = "Carrier 5", + profileClass = PROFILE_CLASS_UNSET, + ) - connectionsRepository.setSubscriptions(listOf(notExclusivelyNonTerrestrialSub)) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue( + listOf(notExclusivelyNonTerrestrialSub) + ) - val latest by collectLastValue(underTest.filteredSubscriptions) + val latest by underTest.filteredSubscriptions.collectLastValue() - assertThat(latest).isEqualTo(listOf(notExclusivelyNonTerrestrialSub)) - } + assertThat(latest).isEqualTo(listOf(notExclusivelyNonTerrestrialSub)) + } @Test - fun filteredSubscriptions_subExclusivelyNonTerrestrial_doesNotHaveSub() = - kosmos.runTest { - val exclusivelyNonTerrestrialSub = - SubscriptionModel( - isExclusivelyNonTerrestrial = true, - subscriptionId = 5, - carrierName = "Carrier 5", - profileClass = PROFILE_CLASS_UNSET, - ) + fun filteredSubscriptions_subExclusivelyNonTerrestrial_doesNotHaveSub() = runTest { + val exclusivelyNonTerrestrialSub = + SubscriptionModel( + isExclusivelyNonTerrestrial = true, + subscriptionId = 5, + carrierName = "Carrier 5", + profileClass = PROFILE_CLASS_UNSET, + ) - connectionsRepository.setSubscriptions(listOf(exclusivelyNonTerrestrialSub)) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue( + listOf(exclusivelyNonTerrestrialSub) + ) - val latest by collectLastValue(underTest.filteredSubscriptions) + val latest by underTest.filteredSubscriptions.collectLastValue() - assertThat(latest).isEmpty() - } + assertThat(latest).isEmpty() + } @Test - fun filteredSubscription_mixOfExclusivelyNonTerrestrialAndOther_hasOtherSubsOnly() = - kosmos.runTest { - val exclusivelyNonTerrestrialSub = - SubscriptionModel( - isExclusivelyNonTerrestrial = true, - subscriptionId = 5, - carrierName = "Carrier 5", - profileClass = PROFILE_CLASS_UNSET, - ) - val otherSub1 = - SubscriptionModel( - isExclusivelyNonTerrestrial = false, - subscriptionId = 1, - carrierName = "Carrier 1", - profileClass = PROFILE_CLASS_UNSET, - ) - val otherSub2 = - SubscriptionModel( - isExclusivelyNonTerrestrial = false, - subscriptionId = 2, - carrierName = "Carrier 2", - profileClass = PROFILE_CLASS_UNSET, - ) - - connectionsRepository.setSubscriptions( - listOf(otherSub1, exclusivelyNonTerrestrialSub, otherSub2) + fun filteredSubscription_mixOfExclusivelyNonTerrestrialAndOther_hasOtherSubsOnly() = runTest { + val exclusivelyNonTerrestrialSub = + SubscriptionModel( + isExclusivelyNonTerrestrial = true, + subscriptionId = 5, + carrierName = "Carrier 5", + profileClass = PROFILE_CLASS_UNSET, + ) + val otherSub1 = + SubscriptionModel( + isExclusivelyNonTerrestrial = false, + subscriptionId = 1, + carrierName = "Carrier 1", + profileClass = PROFILE_CLASS_UNSET, + ) + val otherSub2 = + SubscriptionModel( + isExclusivelyNonTerrestrial = false, + subscriptionId = 2, + carrierName = "Carrier 2", + profileClass = PROFILE_CLASS_UNSET, ) - val latest by collectLastValue(underTest.filteredSubscriptions) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue( + listOf(otherSub1, exclusivelyNonTerrestrialSub, otherSub2) + ) + + val latest by underTest.filteredSubscriptions.collectLastValue() - assertThat(latest).isEqualTo(listOf(otherSub1, otherSub2)) - } + assertThat(latest).isEqualTo(listOf(otherSub1, otherSub2)) + } @Test fun filteredSubscriptions_exclusivelyNonTerrestrialSub_andOpportunistic_bothFiltersHappen() = - kosmos.runTest { + runTest { // Exclusively non-terrestrial sub val exclusivelyNonTerrestrialSub = SubscriptionModel( @@ -483,10 +457,12 @@ class MobileIconsInteractorKairosTest : SysuiTestCase() { ) // WHEN both an exclusively non-terrestrial sub and opportunistic sub pair is included - connectionsRepository.setSubscriptions(listOf(sub3, sub4, exclusivelyNonTerrestrialSub)) - connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue( + listOf(sub3, sub4, exclusivelyNonTerrestrialSub) + ) + mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID) - val latest by collectLastValue(underTest.filteredSubscriptions) + val latest by underTest.filteredSubscriptions.collectLastValue() // THEN both the only-non-terrestrial sub and the non-active sub are filtered out, // leaving only sub3. @@ -494,484 +470,427 @@ class MobileIconsInteractorKairosTest : SysuiTestCase() { } @Test - fun activeDataConnection_turnedOn() = - kosmos.runTest { - (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID) - as FakeMobileConnectionRepository) - .dataEnabled - .value = true + fun activeDataConnection_turnedOn() = runTest { + val connection1 = + mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId.sample()[SUB_1_ID]!! - val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled) + connection1.dataEnabled.setValue(true) - assertThat(latest).isTrue() - } + val latest by underTest.activeDataConnectionHasDataEnabled.collectLastValue() + + assertThat(latest).isTrue() + } @Test - fun activeDataConnection_turnedOff() = - kosmos.runTest { - (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID) - as FakeMobileConnectionRepository) - .dataEnabled - .value = true + fun activeDataConnection_turnedOff() = runTest { + val connection1 = + mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId.sample()[SUB_1_ID]!! - val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled) + connection1.dataEnabled.setValue(true) + val latest by underTest.activeDataConnectionHasDataEnabled.collectLastValue() - (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID) - as FakeMobileConnectionRepository) - .dataEnabled - .value = false + connection1.dataEnabled.setValue(false) - assertThat(latest).isFalse() - } + assertThat(latest).isFalse() + } @Test - fun activeDataConnection_invalidSubId() = - kosmos.runTest { - val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled) + fun activeDataConnection_invalidSubId() = runTest { + val latest by underTest.activeDataConnectionHasDataEnabled.collectLastValue() - connectionsRepository.setActiveMobileDataSubscriptionId(INVALID_SUBSCRIPTION_ID) + mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId( + INVALID_SUBSCRIPTION_ID + ) - // An invalid active subId should tell us that data is off - assertThat(latest).isFalse() - } + // An invalid active subId should tell us that data is off + assertThat(latest).isFalse() + } @Test - fun failedConnection_default_validated_notFailed() = - kosmos.runTest { - val latest by collectLastValue(underTest.isDefaultConnectionFailed) + fun failedConnection_default_validated_notFailed() = runTest { + val latest by underTest.isDefaultConnectionFailed.collectLastValue() - connectionsRepository.mobileIsDefault.value = true - connectionsRepository.defaultConnectionIsValidated.value = true + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) + mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true) - assertThat(latest).isFalse() - } + assertThat(latest).isFalse() + } @Test - fun failedConnection_notDefault_notValidated_notFailed() = - kosmos.runTest { - val latest by collectLastValue(underTest.isDefaultConnectionFailed) + fun failedConnection_notDefault_notValidated_notFailed() = runTest { + val latest by underTest.isDefaultConnectionFailed.collectLastValue() - connectionsRepository.mobileIsDefault.value = false - connectionsRepository.defaultConnectionIsValidated.value = false + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(false) + mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false) - assertThat(latest).isFalse() - } + assertThat(latest).isFalse() + } @Test - fun failedConnection_default_notValidated_failed() = - kosmos.runTest { - val latest by collectLastValue(underTest.isDefaultConnectionFailed) + fun failedConnection_default_notValidated_failed() = runTest { + val latest by underTest.isDefaultConnectionFailed.collectLastValue() - connectionsRepository.mobileIsDefault.value = true - connectionsRepository.defaultConnectionIsValidated.value = false + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) + mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false) - assertThat(latest).isTrue() - } + assertThat(latest).isTrue() + } @Test - fun failedConnection_carrierMergedDefault_notValidated_failed() = - kosmos.runTest { - val latest by collectLastValue(underTest.isDefaultConnectionFailed) + fun failedConnection_carrierMergedDefault_notValidated_failed() = runTest { + val latest by underTest.isDefaultConnectionFailed.collectLastValue() - connectionsRepository.hasCarrierMergedConnection.value = true - connectionsRepository.defaultConnectionIsValidated.value = false + mobileConnectionsRepositoryKairos.fake.hasCarrierMergedConnection.setValue(true) + mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false) - assertThat(latest).isTrue() - } + assertThat(latest).isTrue() + } /** Regression test for b/275076959. */ @Test - fun failedConnection_dataSwitchInSameGroup_notFailed() = - kosmos.runTest { - val latest by collectLastValue(underTest.isDefaultConnectionFailed) + fun failedConnection_dataSwitchInSameGroup_notFailed() = runTest { + val latest by underTest.isDefaultConnectionFailed.collectLastValue() - connectionsRepository.mobileIsDefault.value = true - connectionsRepository.defaultConnectionIsValidated.value = true - runCurrent() + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) + mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true) + runCurrent() - // WHEN there's a data change in the same subscription group - connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) - connectionsRepository.defaultConnectionIsValidated.value = false - runCurrent() + // WHEN there's a data change in the same subscription group + mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit) + mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false) + runCurrent() - // THEN the default connection is *not* marked as failed because of forced validation - assertThat(latest).isFalse() - } + // THEN the default connection is *not* marked as failed because of forced validation + assertThat(latest).isFalse() + } @Test - fun failedConnection_dataSwitchNotInSameGroup_isFailed() = - kosmos.runTest { - val latest by collectLastValue(underTest.isDefaultConnectionFailed) + fun failedConnection_dataSwitchNotInSameGroup_isFailed() = runTest { + val latest by underTest.isDefaultConnectionFailed.collectLastValue() - connectionsRepository.mobileIsDefault.value = true - connectionsRepository.defaultConnectionIsValidated.value = true - runCurrent() + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) + mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true) + runCurrent() - // WHEN the connection is invalidated without a activeSubChangedInGroupEvent - connectionsRepository.defaultConnectionIsValidated.value = false + // WHEN the connection is invalidated without a activeSubChangedInGroupEvent + mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false) - // THEN the connection is immediately marked as failed - assertThat(latest).isTrue() - } + // THEN the connection is immediately marked as failed + assertThat(latest).isTrue() + } @Test - fun alwaysShowDataRatIcon_configHasTrue() = - kosmos.runTest { - val latest by collectLastValue(underTest.alwaysShowDataRatIcon) + fun alwaysShowDataRatIcon_configHasTrue() = runTest { + val latest by underTest.alwaysShowDataRatIcon.collectLastValue() - val config = MobileMappings.Config() - config.alwaysShowDataRatIcon = true - connectionsRepository.defaultDataSubRatConfig.value = config + val config = MobileMappings.Config() + config.alwaysShowDataRatIcon = true + mobileConnectionsRepositoryKairos.fake.defaultDataSubRatConfig.setValue(config) - assertThat(latest).isTrue() - } + assertThat(latest).isTrue() + } @Test - fun alwaysShowDataRatIcon_configHasFalse() = - kosmos.runTest { - val latest by collectLastValue(underTest.alwaysShowDataRatIcon) + fun alwaysShowDataRatIcon_configHasFalse() = runTest { + val latest by underTest.alwaysShowDataRatIcon.collectLastValue() - val config = MobileMappings.Config() - config.alwaysShowDataRatIcon = false - connectionsRepository.defaultDataSubRatConfig.value = config + val config = MobileMappings.Config() + config.alwaysShowDataRatIcon = false + mobileConnectionsRepositoryKairos.fake.defaultDataSubRatConfig.setValue(config) - assertThat(latest).isFalse() - } + assertThat(latest).isFalse() + } @Test - fun alwaysUseCdmaLevel_configHasTrue() = - kosmos.runTest { - val latest by collectLastValue(underTest.alwaysUseCdmaLevel) + fun alwaysUseCdmaLevel_configHasTrue() = runTest { + val latest by underTest.alwaysUseCdmaLevel.collectLastValue() - val config = MobileMappings.Config() - config.alwaysShowCdmaRssi = true - connectionsRepository.defaultDataSubRatConfig.value = config + val config = MobileMappings.Config() + config.alwaysShowCdmaRssi = true + mobileConnectionsRepositoryKairos.fake.defaultDataSubRatConfig.setValue(config) - assertThat(latest).isTrue() - } + assertThat(latest).isTrue() + } @Test - fun alwaysUseCdmaLevel_configHasFalse() = - kosmos.runTest { - val latest by collectLastValue(underTest.alwaysUseCdmaLevel) + fun alwaysUseCdmaLevel_configHasFalse() = runTest { + val latest by underTest.alwaysUseCdmaLevel.collectLastValue() - val config = MobileMappings.Config() - config.alwaysShowCdmaRssi = false - connectionsRepository.defaultDataSubRatConfig.value = config + val config = MobileMappings.Config() + config.alwaysShowCdmaRssi = false + mobileConnectionsRepositoryKairos.fake.defaultDataSubRatConfig.setValue(config) - assertThat(latest).isFalse() - } + assertThat(latest).isFalse() + } @Test - fun isSingleCarrier_zeroSubscriptions_false() = - kosmos.runTest { - val latest by collectLastValue(underTest.isSingleCarrier) + fun isSingleCarrier_zeroSubscriptions_false() = runTest { + val latest by underTest.isSingleCarrier.collectLastValue() - connectionsRepository.setSubscriptions(emptyList()) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(emptyList()) - assertThat(latest).isFalse() - } + assertThat(latest).isFalse() + } @Test - fun isSingleCarrier_oneSubscription_true() = - kosmos.runTest { - val latest by collectLastValue(underTest.isSingleCarrier) + fun isSingleCarrier_oneSubscription_true() = runTest { + val latest by underTest.isSingleCarrier.collectLastValue() - connectionsRepository.setSubscriptions(listOf(SUB_1)) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1)) - assertThat(latest).isTrue() - } + assertThat(latest).isTrue() + } @Test - fun isSingleCarrier_twoSubscriptions_false() = - kosmos.runTest { - val latest by collectLastValue(underTest.isSingleCarrier) + fun isSingleCarrier_twoSubscriptions_false() = runTest { + val latest by underTest.isSingleCarrier.collectLastValue() - connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2)) - assertThat(latest).isFalse() - } + assertThat(latest).isFalse() + } @Test - fun isSingleCarrier_updates() = - kosmos.runTest { - val latest by collectLastValue(underTest.isSingleCarrier) + fun isSingleCarrier_updates() = runTest { + val latest by underTest.isSingleCarrier.collectLastValue() - connectionsRepository.setSubscriptions(listOf(SUB_1)) - assertThat(latest).isTrue() + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1)) + assertThat(latest).isTrue() - connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) - assertThat(latest).isFalse() - } + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2)) + assertThat(latest).isFalse() + } @Test - fun mobileIsDefault_mobileFalseAndCarrierMergedFalse_false() = - kosmos.runTest { - val latest by collectLastValue(underTest.mobileIsDefault) + fun mobileIsDefault_mobileFalseAndCarrierMergedFalse_false() = runTest { + val latest by underTest.mobileIsDefault.collectLastValue() - connectionsRepository.mobileIsDefault.value = false - connectionsRepository.hasCarrierMergedConnection.value = false + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(false) + mobileConnectionsRepositoryKairos.fake.hasCarrierMergedConnection.setValue(false) - assertThat(latest).isFalse() - } + assertThat(latest).isFalse() + } @Test - fun mobileIsDefault_mobileTrueAndCarrierMergedFalse_true() = - kosmos.runTest { - val latest by collectLastValue(underTest.mobileIsDefault) + fun mobileIsDefault_mobileTrueAndCarrierMergedFalse_true() = runTest { + val latest by underTest.mobileIsDefault.collectLastValue() - connectionsRepository.mobileIsDefault.value = true - connectionsRepository.hasCarrierMergedConnection.value = false + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) + mobileConnectionsRepositoryKairos.fake.hasCarrierMergedConnection.setValue(false) - assertThat(latest).isTrue() - } + assertThat(latest).isTrue() + } /** Regression test for b/272586234. */ @Test - fun mobileIsDefault_mobileFalseAndCarrierMergedTrue_true() = - kosmos.runTest { - val latest by collectLastValue(underTest.mobileIsDefault) + fun mobileIsDefault_mobileFalseAndCarrierMergedTrue_true() = runTest { + val latest by underTest.mobileIsDefault.collectLastValue() - connectionsRepository.mobileIsDefault.value = false - connectionsRepository.hasCarrierMergedConnection.value = true + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(false) + mobileConnectionsRepositoryKairos.fake.hasCarrierMergedConnection.setValue(true) - assertThat(latest).isTrue() - } + assertThat(latest).isTrue() + } @Test - fun mobileIsDefault_updatesWhenRepoUpdates() = - kosmos.runTest { - val latest by collectLastValue(underTest.mobileIsDefault) + fun mobileIsDefault_updatesWhenRepoUpdates() = runTest { + val latest by underTest.mobileIsDefault.collectLastValue() - connectionsRepository.mobileIsDefault.value = true - assertThat(latest).isTrue() + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) + assertThat(latest).isTrue() - connectionsRepository.mobileIsDefault.value = false - assertThat(latest).isFalse() + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(false) + assertThat(latest).isFalse() - connectionsRepository.hasCarrierMergedConnection.value = true - assertThat(latest).isTrue() - } + mobileConnectionsRepositoryKairos.fake.hasCarrierMergedConnection.setValue(true) + assertThat(latest).isTrue() + } // The data switch tests are mostly testing the [forcingCellularValidation] flow, but that flow // is private and can only be tested by looking at [isDefaultConnectionFailed]. @Test - fun dataSwitch_inSameGroup_validatedMatchesPreviousValue_expiresAfter2s() = - kosmos.runTest { - val latest by collectLastValue(underTest.isDefaultConnectionFailed) - - connectionsRepository.mobileIsDefault.value = true - connectionsRepository.defaultConnectionIsValidated.value = true - runCurrent() - - // Trigger a data change in the same subscription group that's not yet validated - connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) - connectionsRepository.defaultConnectionIsValidated.value = false - runCurrent() - - // After 1s, the force validation bit is still present, so the connection is not marked - // as failed - testScope.advanceTimeBy(1000) - assertThat(latest).isFalse() - - // After 2s, the force validation expires so the connection updates to failed - testScope.advanceTimeBy(1001) - assertThat(latest).isTrue() - } + fun dataSwitch_inSameGroup_validatedMatchesPreviousValue_expiresAfter2s() = runTest { + val latest by underTest.isDefaultConnectionFailed.collectLastValue() - @Test - fun dataSwitch_inSameGroup_notValidated_immediatelyMarkedAsFailed() = - kosmos.runTest { - val latest by collectLastValue(underTest.isDefaultConnectionFailed) + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) + mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true) + runCurrent() - connectionsRepository.mobileIsDefault.value = true - connectionsRepository.defaultConnectionIsValidated.value = false - runCurrent() + // Trigger a data change in the same subscription group that's not yet validated + mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit) + mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false) + runCurrent() - connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) + // After 1s, the force validation bit is still present, so the connection is not marked + // as failed + testScope.advanceTimeBy(1000) + assertThat(latest).isFalse() - assertThat(latest).isTrue() - } + // After 2s, the force validation expires so the connection updates to failed + testScope.advanceTimeBy(1001) + assertThat(latest).isTrue() + } @Test - fun dataSwitch_loseValidation_thenSwitchHappens_clearsForcedBit() = - kosmos.runTest { - val latest by collectLastValue(underTest.isDefaultConnectionFailed) + fun dataSwitch_inSameGroup_notValidated_immediatelyMarkedAsFailed() = runTest { + val latest by underTest.isDefaultConnectionFailed.collectLastValue() - // GIVEN the network starts validated - connectionsRepository.mobileIsDefault.value = true - connectionsRepository.defaultConnectionIsValidated.value = true - runCurrent() + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) + mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false) + runCurrent() - // WHEN a data change happens in the same group - connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) + mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit) - // WHEN the validation bit is lost - connectionsRepository.defaultConnectionIsValidated.value = false - runCurrent() - - // WHEN another data change happens in the same group - connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) - - // THEN the forced validation bit is still used... - assertThat(latest).isFalse() - - testScope.advanceTimeBy(1000) - assertThat(latest).isFalse() - - // ... but expires after 2s - testScope.advanceTimeBy(1001) - assertThat(latest).isTrue() - } + assertThat(latest).isTrue() + } @Test - fun dataSwitch_whileAlreadyForcingValidation_resetsClock() = - kosmos.runTest { - val latest by collectLastValue(underTest.isDefaultConnectionFailed) - connectionsRepository.mobileIsDefault.value = true - connectionsRepository.defaultConnectionIsValidated.value = true - runCurrent() + fun dataSwitch_loseValidation_thenSwitchHappens_clearsForcedBit() = runTest { + val latest by underTest.isDefaultConnectionFailed.collectLastValue() - connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) + // GIVEN the network starts validated + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) + mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true) + runCurrent() - testScope.advanceTimeBy(1000) + // WHEN a data change happens in the same group + mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit) - // WHEN another change in same group event happens - connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) - connectionsRepository.defaultConnectionIsValidated.value = false - runCurrent() + // WHEN the validation bit is lost + mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false) + runCurrent() - // THEN the forced validation remains for exactly 2 more seconds from now + // WHEN another data change happens in the same group + mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit) - // 1.500s from second event - testScope.advanceTimeBy(1500) - assertThat(latest).isFalse() + // THEN the forced validation bit is still used... + assertThat(latest).isFalse() - // 2.001s from the second event - testScope.advanceTimeBy(501) - assertThat(latest).isTrue() - } + testScope.advanceTimeBy(1000) + assertThat(latest).isFalse() - @Test - fun isForceHidden_repoHasMobileHidden_true() = - kosmos.runTest { - val latest by collectLastValue(underTest.isForceHidden) + // ... but expires after 2s + testScope.advanceTimeBy(1001) + assertThat(latest).isTrue() + } - kosmos.connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE)) + @Test + fun dataSwitch_whileAlreadyForcingValidation_resetsClock() = runTest { + val latest by underTest.isDefaultConnectionFailed.collectLastValue() + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) + mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true) + runCurrent() - assertThat(latest).isTrue() - } + mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit) - @Test - fun isForceHidden_repoDoesNotHaveMobileHidden_false() = - kosmos.runTest { - val latest by collectLastValue(underTest.isForceHidden) + testScope.advanceTimeBy(1000) - kosmos.connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI)) + // WHEN another change in same group event happens + mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit) + mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false) + runCurrent() - assertThat(latest).isFalse() - } + // THEN the forced validation remains for exactly 2 more seconds from now - @Test - fun iconInteractor_cachedPerSubId() = - kosmos.runTest { - val interactor1 = underTest.getMobileConnectionInteractorForSubId(SUB_1_ID) - val interactor2 = underTest.getMobileConnectionInteractorForSubId(SUB_1_ID) + // 1.500s from second event + testScope.advanceTimeBy(1500) + assertThat(latest).isFalse() - assertThat(interactor1).isNotNull() - assertThat(interactor1).isSameInstanceAs(interactor2) - } + // 2.001s from the second event + testScope.advanceTimeBy(501) + assertThat(latest).isTrue() + } @Test - fun deviceBasedEmergencyMode_emergencyCallsOnly_followsDeviceServiceStateFromRepo() = - kosmos.runTest { - val latest by collectLastValue(underTest.isDeviceInEmergencyCallsOnlyMode) + fun isForceHidden_repoHasMobileHidden_true() = runTest { + val latest by underTest.isForceHidden.collectLastValue() - connectionsRepository.isDeviceEmergencyCallCapable.value = true + kosmos.connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE)) - assertThat(latest).isTrue() + assertThat(latest).isTrue() + } + + @Test + fun isForceHidden_repoDoesNotHaveMobileHidden_false() = runTest { + val latest by underTest.isForceHidden.collectLastValue() - connectionsRepository.isDeviceEmergencyCallCapable.value = false + kosmos.connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI)) - assertThat(latest).isFalse() - } + assertThat(latest).isFalse() + } @Test - fun defaultDataSubId_tracksRepo() = - kosmos.runTest { - val latest by collectLastValue(underTest.defaultDataSubId) + fun deviceBasedEmergencyMode_emergencyCallsOnly_followsDeviceServiceStateFromRepo() = runTest { + val latest by underTest.isDeviceInEmergencyCallsOnlyMode.collectLastValue() - connectionsRepository.defaultDataSubId.value = 1 + mobileConnectionsRepositoryKairos.fake.isDeviceEmergencyCallCapable.setValue(true) - assertThat(latest).isEqualTo(1) + assertThat(latest).isTrue() - connectionsRepository.defaultDataSubId.value = 2 + mobileConnectionsRepositoryKairos.fake.isDeviceEmergencyCallCapable.setValue(false) - assertThat(latest).isEqualTo(2) - } + assertThat(latest).isFalse() + } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) - fun isStackable_tracksNumberOfSubscriptions() = - kosmos.runTest { - val latest by collectLastValue(underTest.isStackable) + fun isStackable_tracksNumberOfSubscriptions() = runTest { + val latest by underTest.isStackable.collectLastValue() - connectionsRepository.setSubscriptions(listOf(SUB_1)) - assertThat(latest).isFalse() + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1)) + assertThat(latest).isFalse() - connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) - assertThat(latest).isTrue() + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2)) + assertThat(latest).isTrue() - connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2, SUB_3_OPP)) - assertThat(latest).isFalse() - } + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue( + listOf(SUB_1, SUB_2, SUB_3_OPP) + ) + assertThat(latest).isFalse() + } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) - fun isStackable_checksForTerrestrialConnections() = - kosmos.runTest { - val latest by collectLastValue(underTest.isStackable) + fun isStackable_checksForTerrestrialConnections() = runTest { + val latest by underTest.isStackable.collectLastValue() - connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) - setNumberOfLevelsForSubId(SUB_1_ID, 5) - setNumberOfLevelsForSubId(SUB_2_ID, 5) - assertThat(latest).isTrue() + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2)) + setNumberOfLevelsForSubId(SUB_1_ID, 5) + setNumberOfLevelsForSubId(SUB_2_ID, 5) + assertThat(latest).isTrue() - (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID) - as FakeMobileConnectionRepository) - .isNonTerrestrial - .value = true + mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId + .sample()[SUB_1_ID]!! + .isNonTerrestrial + .setValue(true) - assertThat(latest).isFalse() - } + assertThat(latest).isFalse() + } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) - fun isStackable_checksForNumberOfBars() = - kosmos.runTest { - val latest by collectLastValue(underTest.isStackable) + fun isStackable_checksForNumberOfBars() = runTest { + val latest by underTest.isStackable.collectLastValue() - // Number of levels is the same for both - connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) - setNumberOfLevelsForSubId(SUB_1_ID, 5) - setNumberOfLevelsForSubId(SUB_2_ID, 5) + // Number of levels is the same for both + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2)) + setNumberOfLevelsForSubId(SUB_1_ID, 5) + setNumberOfLevelsForSubId(SUB_2_ID, 5) - assertThat(latest).isTrue() + assertThat(latest).isTrue() - // Change the number of levels to be different than SUB_2 - setNumberOfLevelsForSubId(SUB_1_ID, 6) + // Change the number of levels to be different than SUB_2 + setNumberOfLevelsForSubId(SUB_1_ID, 6) - assertThat(latest).isFalse() - } + assertThat(latest).isFalse() + } - private fun setNumberOfLevelsForSubId(subId: Int, numberOfLevels: Int) { - with(kosmos) { - (fakeMobileConnectionsRepository.getRepoForSubId(subId) - as FakeMobileConnectionRepository) - .numberOfLevels - .value = numberOfLevels - } + private suspend fun KairosTestScope.setNumberOfLevelsForSubId(subId: Int, numberOfLevels: Int) { + mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId + .sample()[subId]!! + .numberOfLevels + .setValue(numberOfLevels) } /** diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelKairosTest.kt new file mode 100644 index 000000000000..57e63a595b8f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelKairosTest.kt @@ -0,0 +1,198 @@ +/* + * 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.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlagsClassic +import com.android.systemui.flags.Flags +import com.android.systemui.log.table.logcatTableLogBuffer +import com.android.systemui.statusbar.connectivity.MobileIconCarrierIdOverridesFake +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractorImpl +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl +import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel +import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants +import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository +import com.android.systemui.testKosmos +import com.android.systemui.util.CarrierConfigTracker +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class LocationBasedMobileIconViewModelKairosTest : SysuiTestCase() { + private val kosmos = testKosmos() + + private lateinit var commonImpl: MobileIconViewModelCommonKairos + private lateinit var homeIcon: HomeMobileIconViewModelKairos + private lateinit var qsIcon: QsMobileIconViewModelKairos + private lateinit var keyguardIcon: KeyguardMobileIconViewModelKairos + private lateinit var iconsInteractor: MobileIconsInteractor + private lateinit var interactor: MobileIconInteractor + private val connectionsRepository = kosmos.fakeMobileConnectionsRepository + private lateinit var repository: FakeMobileConnectionRepository + private lateinit var airplaneModeInteractor: AirplaneModeInteractor + + private val connectivityRepository = FakeConnectivityRepository() + private val flags = + FakeFeatureFlagsClassic().also { + it.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true) + } + + @Mock private lateinit var constants: ConnectivityConstants + private val tableLogBuffer = + logcatTableLogBuffer(kosmos, "LocationBasedMobileIconViewModelTest") + @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + airplaneModeInteractor = + AirplaneModeInteractor( + FakeAirplaneModeRepository(), + FakeConnectivityRepository(), + connectionsRepository, + ) + repository = + FakeMobileConnectionRepository(SUB_1_ID, tableLogBuffer).apply { + isInService.value = true + cdmaLevel.value = 1 + primaryLevel.value = 1 + isEmergencyOnly.value = false + numberOfLevels.value = 4 + resolvedNetworkType.value = ResolvedNetworkType.DefaultNetworkType(lookupKey = "3G") + dataConnectionState.value = DataConnectionState.Connected + } + + connectionsRepository.activeMobileDataRepository.value = repository + + connectivityRepository.apply { setMobileConnected() } + + iconsInteractor = + MobileIconsInteractorImpl( + connectionsRepository, + carrierConfigTracker, + tableLogBuffer, + connectivityRepository, + FakeUserSetupRepository(), + testScope.backgroundScope, + context, + flags, + ) + + interactor = + MobileIconInteractorImpl( + testScope.backgroundScope, + iconsInteractor.activeDataConnectionHasDataEnabled, + iconsInteractor.alwaysShowDataRatIcon, + iconsInteractor.alwaysUseCdmaLevel, + iconsInteractor.isSingleCarrier, + iconsInteractor.mobileIsDefault, + iconsInteractor.defaultMobileIconMapping, + iconsInteractor.defaultMobileIconGroup, + iconsInteractor.isDefaultConnectionFailed, + iconsInteractor.isForceHidden, + repository, + context, + MobileIconCarrierIdOverridesFake(), + ) + + commonImpl = + MobileIconViewModelKairos( + SUB_1_ID, + interactor, + airplaneModeInteractor, + constants, + testScope.backgroundScope, + ) + + homeIcon = HomeMobileIconViewModelKairos(commonImpl, mock()) + qsIcon = QsMobileIconViewModelKairos(commonImpl) + keyguardIcon = KeyguardMobileIconViewModelKairos(commonImpl) + } + + @Test + fun locationBasedViewModelsReceiveSameIconIdWhenCommonImplUpdates() = + testScope.runTest { + var latestHome: SignalIconModel? = null + val homeJob = homeIcon.icon.onEach { latestHome = it }.launchIn(this) + + var latestQs: SignalIconModel? = null + val qsJob = qsIcon.icon.onEach { latestQs = it }.launchIn(this) + + var latestKeyguard: SignalIconModel? = null + val keyguardJob = keyguardIcon.icon.onEach { latestKeyguard = it }.launchIn(this) + + var expected = defaultSignal(level = 1) + + assertThat(latestHome).isEqualTo(expected) + assertThat(latestQs).isEqualTo(expected) + assertThat(latestKeyguard).isEqualTo(expected) + + repository.setAllLevels(2) + expected = defaultSignal(level = 2) + + assertThat(latestHome).isEqualTo(expected) + assertThat(latestQs).isEqualTo(expected) + assertThat(latestKeyguard).isEqualTo(expected) + + homeJob.cancel() + qsJob.cancel() + keyguardJob.cancel() + } + + companion object { + private const val SUB_1_ID = 1 + private const val NUM_LEVELS = 4 + + /** Convenience constructor for these tests */ + fun defaultSignal(level: Int = 1): SignalIconModel { + return SignalIconModel.Cellular( + level, + NUM_LEVELS, + showExclamationMark = false, + carrierNetworkChange = false, + ) + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairosTest.kt new file mode 100644 index 000000000000..6b114a8256f2 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairosTest.kt @@ -0,0 +1,1077 @@ +/* + * 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.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.mobile.MobileMappings +import com.android.settingslib.mobile.TelephonyIcons.G +import com.android.settingslib.mobile.TelephonyIcons.THREE_G +import com.android.settingslib.mobile.TelephonyIcons.UNKNOWN +import com.android.systemui.Flags.FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.FakeFeatureFlagsClassic +import com.android.systemui.flags.Flags +import com.android.systemui.log.table.logcatTableLogBuffer +import com.android.systemui.res.R +import com.android.systemui.statusbar.connectivity.MobileIconCarrierIdOverridesFake +import com.android.systemui.statusbar.core.NewStatusBarIcons +import com.android.systemui.statusbar.core.StatusBarRootModernization +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository.Companion.DEFAULT_NETWORK_NAME +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractorImpl +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl +import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel +import com.android.systemui.statusbar.pipeline.mobile.ui.model.MobileContentDescription +import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy +import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository +import com.android.systemui.testKosmos +import com.android.systemui.util.CarrierConfigTracker +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.yield +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class MobileIconViewModelKairosTest : SysuiTestCase() { + private val kosmos = testKosmos() + + private var connectivityRepository = FakeConnectivityRepository() + + private lateinit var underTest: MobileIconViewModelKairos + private lateinit var interactor: MobileIconInteractorImpl + private lateinit var iconsInteractor: MobileIconsInteractorImpl + private lateinit var repository: FakeMobileConnectionRepository + private lateinit var connectionsRepository: FakeMobileConnectionsRepository + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository + private lateinit var airplaneModeInteractor: AirplaneModeInteractor + @Mock private lateinit var constants: ConnectivityConstants + private val tableLogBuffer = logcatTableLogBuffer(kosmos, "MobileIconViewModelTest") + @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker + + private val flags = + FakeFeatureFlagsClassic().also { + it.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true) + } + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(constants.hasDataCapabilities).thenReturn(true) + + connectionsRepository = + FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogBuffer) + + repository = + FakeMobileConnectionRepository(SUB_1_ID, tableLogBuffer).apply { + setNetworkTypeKey(connectionsRepository.GSM_KEY) + isInService.value = true + dataConnectionState.value = DataConnectionState.Connected + dataEnabled.value = true + } + connectionsRepository.activeMobileDataRepository.value = repository + connectionsRepository.mobileIsDefault.value = true + + airplaneModeRepository = FakeAirplaneModeRepository() + airplaneModeInteractor = + AirplaneModeInteractor( + airplaneModeRepository, + connectivityRepository, + kosmos.fakeMobileConnectionsRepository, + ) + + iconsInteractor = + MobileIconsInteractorImpl( + connectionsRepository, + carrierConfigTracker, + tableLogBuffer, + connectivityRepository, + FakeUserSetupRepository(), + testScope.backgroundScope, + context, + flags, + ) + + interactor = + MobileIconInteractorImpl( + testScope.backgroundScope, + iconsInteractor.activeDataConnectionHasDataEnabled, + iconsInteractor.alwaysShowDataRatIcon, + iconsInteractor.alwaysUseCdmaLevel, + iconsInteractor.isSingleCarrier, + iconsInteractor.mobileIsDefault, + iconsInteractor.defaultMobileIconMapping, + iconsInteractor.defaultMobileIconGroup, + iconsInteractor.isDefaultConnectionFailed, + iconsInteractor.isForceHidden, + repository, + context, + MobileIconCarrierIdOverridesFake(), + ) + createAndSetViewModel() + } + + @Test + fun isVisible_notDataCapable_alwaysFalse() = + testScope.runTest { + // Create a new view model here so the constants are properly read + whenever(constants.hasDataCapabilities).thenReturn(false) + createAndSetViewModel() + + var latest: Boolean? = null + val job = underTest.isVisible.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun isVisible_notAirplane_notForceHidden_true() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.isVisible.onEach { latest = it }.launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(false) + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun isVisible_airplaneAndNotAllowed_false() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.isVisible.onEach { latest = it }.launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(true) + repository.isAllowedDuringAirplaneMode.value = false + connectivityRepository.setForceHiddenIcons(setOf()) + + assertThat(latest).isFalse() + + job.cancel() + } + + /** Regression test for b/291993542. */ + @Test + fun isVisible_airplaneButAllowed_true() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.isVisible.onEach { latest = it }.launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(true) + repository.isAllowedDuringAirplaneMode.value = true + connectivityRepository.setForceHiddenIcons(setOf()) + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun isVisible_forceHidden_false() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.isVisible.onEach { latest = it }.launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(false) + connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE)) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun isVisible_respondsToUpdates() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.isVisible.onEach { latest = it }.launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(false) + connectivityRepository.setForceHiddenIcons(setOf()) + + assertThat(latest).isTrue() + + airplaneModeRepository.setIsAirplaneMode(true) + assertThat(latest).isFalse() + + repository.isAllowedDuringAirplaneMode.value = true + assertThat(latest).isTrue() + + connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE)) + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun isVisible_satellite_respectsAirplaneMode() = + testScope.runTest { + val latest by collectLastValue(underTest.isVisible) + + repository.isNonTerrestrial.value = true + airplaneModeInteractor.setIsAirplaneMode(false) + + assertThat(latest).isTrue() + + airplaneModeInteractor.setIsAirplaneMode(true) + + assertThat(latest).isFalse() + } + + @Test + fun contentDescription_notInService_usesNoPhone() = + testScope.runTest { + val latest by collectLastValue(underTest.contentDescription) + + repository.isInService.value = false + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + } + + @Test + fun contentDescription_includesNetworkName() = + testScope.runTest { + val latest by collectLastValue(underTest.contentDescription) + + repository.isInService.value = true + repository.networkName.value = NetworkNameModel.SubscriptionDerived("Test Network Name") + repository.numberOfLevels.value = 5 + repository.setAllLevels(3) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular("Test Network Name", THREE_BARS)) + } + + @Test + fun contentDescription_inService_usesLevel() = + testScope.runTest { + val latest by collectLastValue(underTest.contentDescription) + + repository.setAllLevels(2) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS)) + + repository.setAllLevels(0) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + } + + @Test + fun contentDescription_nonInflated_invalidLevelUsesNoSignalText() = + testScope.runTest { + val latest by collectLastValue(underTest.contentDescription) + + repository.inflateSignalStrength.value = false + repository.setAllLevels(-1) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + + repository.setAllLevels(100) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + } + + @Test + fun contentDescription_nonInflated_levelStrings() = + testScope.runTest { + val latest by collectLastValue(underTest.contentDescription) + + repository.inflateSignalStrength.value = false + repository.setAllLevels(0) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + + repository.setAllLevels(1) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, ONE_BAR)) + + repository.setAllLevels(2) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS)) + + repository.setAllLevels(3) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, THREE_BARS)) + + repository.setAllLevels(4) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FULL_BARS)) + } + + @Test + fun contentDescription_inflated_invalidLevelUsesNoSignalText() = + testScope.runTest { + val latest by collectLastValue(underTest.contentDescription) + + repository.inflateSignalStrength.value = true + repository.numberOfLevels.value = 6 + + repository.setAllLevels(-2) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + + repository.setAllLevels(100) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + } + + @Test + fun contentDescription_inflated_levelStrings() = + testScope.runTest { + val latest by collectLastValue(underTest.contentDescription) + + repository.inflateSignalStrength.value = true + repository.numberOfLevels.value = 6 + + // Note that the _repo_ level is 1 lower than the reported level through the interactor + + repository.setAllLevels(0) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, ONE_BAR)) + + repository.setAllLevels(1) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS)) + + repository.setAllLevels(2) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, THREE_BARS)) + + repository.setAllLevels(3) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FOUR_BARS)) + + repository.setAllLevels(4) + + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FULL_BARS)) + } + + @Test + fun contentDescription_nonInflated_testABunchOfLevelsForNull() = + testScope.runTest { + val latest by collectLastValue(underTest.contentDescription) + + repository.inflateSignalStrength.value = false + repository.numberOfLevels.value = 5 + + // -1 and 5 are out of the bounds for non-inflated content descriptions + for (i in -1..5) { + repository.setAllLevels(i) + when (i) { + -1, + 5 -> + assertWithMessage("Level $i is expected to be 'no signal'") + .that((latest as MobileContentDescription.Cellular).levelDescriptionRes) + .isEqualTo(NO_SIGNAL) + else -> + assertWithMessage("Level $i is expected not to be null") + .that(latest) + .isNotNull() + } + } + } + + @Test + fun contentDescription_inflated_testABunchOfLevelsForNull() = + testScope.runTest { + val latest by collectLastValue(underTest.contentDescription) + repository.inflateSignalStrength.value = true + repository.numberOfLevels.value = 6 + // -1 and 6 are out of the bounds for inflated content descriptions + // Note that the interactor adds 1 to the reported level, hence the -2 to 5 range + for (i in -2..5) { + repository.setAllLevels(i) + when (i) { + -2, + 5 -> + assertWithMessage("Level $i is expected to be 'no signal'") + .that((latest as MobileContentDescription.Cellular).levelDescriptionRes) + .isEqualTo(NO_SIGNAL) + else -> + assertWithMessage("Level $i is not expected to be null") + .that(latest) + .isNotNull() + } + } + } + + @Test + fun networkType_dataEnabled_groupIsRepresented() = + testScope.runTest { + val expected = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription), + ) + connectionsRepository.mobileIsDefault.value = true + repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) + + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(expected) + + job.cancel() + } + + @Test + fun networkType_null_whenDisabled() = + testScope.runTest { + repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) + repository.setDataEnabled(false) + connectionsRepository.mobileIsDefault.value = true + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + assertThat(latest).isNull() + + job.cancel() + } + + @Test + fun networkType_null_whenCarrierNetworkChangeActive() = + testScope.runTest { + repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) + repository.carrierNetworkChangeActive.value = true + connectionsRepository.mobileIsDefault.value = true + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + assertThat(latest).isNull() + + job.cancel() + } + + @Test + fun networkTypeIcon_notNull_whenEnabled() = + testScope.runTest { + val expected = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription), + ) + repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) + repository.setDataEnabled(true) + repository.dataConnectionState.value = DataConnectionState.Connected + connectionsRepository.mobileIsDefault.value = true + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(expected) + + job.cancel() + } + + @Test + fun networkType_nullWhenDataDisconnects() = + testScope.runTest { + val initial = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription), + ) + + repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(initial) + + repository.dataConnectionState.value = DataConnectionState.Disconnected + + assertThat(latest).isNull() + + job.cancel() + } + + @Test + fun networkType_null_changeToDisabled() = + testScope.runTest { + val expected = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription), + ) + repository.dataEnabled.value = true + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(expected) + + repository.dataEnabled.value = false + + assertThat(latest).isNull() + + job.cancel() + } + + @Test + fun networkType_alwaysShow_shownEvenWhenDisabled() = + testScope.runTest { + repository.dataEnabled.value = false + + connectionsRepository.defaultDataSubRatConfig.value = + MobileMappings.Config().also { it.alwaysShowDataRatIcon = true } + + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + val expected = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription), + ) + assertThat(latest).isEqualTo(expected) + + job.cancel() + } + + @Test + fun networkType_alwaysShow_shownEvenWhenDisconnected() = + testScope.runTest { + repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) + repository.dataConnectionState.value = DataConnectionState.Disconnected + + connectionsRepository.defaultDataSubRatConfig.value = + MobileMappings.Config().also { it.alwaysShowDataRatIcon = true } + + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + val expected = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription), + ) + assertThat(latest).isEqualTo(expected) + + job.cancel() + } + + @Test + fun networkType_alwaysShow_shownEvenWhenFailedConnection() = + testScope.runTest { + repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) + connectionsRepository.mobileIsDefault.value = true + connectionsRepository.defaultDataSubRatConfig.value = + MobileMappings.Config().also { it.alwaysShowDataRatIcon = true } + + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + val expected = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription), + ) + assertThat(latest).isEqualTo(expected) + + job.cancel() + } + + @Test + fun networkType_alwaysShow_usesDefaultIconWhenInvalid() = + testScope.runTest { + // The UNKNOWN icon group doesn't have a valid data type icon ID, and the logic from the + // old pipeline was to use the default icon group if the map doesn't exist + repository.setNetworkTypeKey(UNKNOWN.name) + connectionsRepository.defaultDataSubRatConfig.value = + MobileMappings.Config().also { it.alwaysShowDataRatIcon = true } + + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + val expected = + Icon.Resource( + connectionsRepository.defaultMobileIconGroup.value.dataType, + ContentDescription.Resource(G.dataContentDescription), + ) + + assertThat(latest).isEqualTo(expected) + + job.cancel() + } + + @Test + fun networkType_alwaysShow_shownWhenNotDefault() = + testScope.runTest { + repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) + connectionsRepository.mobileIsDefault.value = false + connectionsRepository.defaultDataSubRatConfig.value = + MobileMappings.Config().also { it.alwaysShowDataRatIcon = true } + + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + val expected = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription), + ) + assertThat(latest).isEqualTo(expected) + + job.cancel() + } + + @Test + fun networkType_notShownWhenNotDefault() = + testScope.runTest { + repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) + repository.dataConnectionState.value = DataConnectionState.Connected + connectionsRepository.mobileIsDefault.value = false + + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + assertThat(latest).isNull() + + job.cancel() + } + + @Test + fun roaming() = + testScope.runTest { + repository.setAllRoaming(true) + + var latest: Boolean? = null + val job = underTest.roaming.onEach { latest = it }.launchIn(this) + + assertThat(latest).isTrue() + + repository.setAllRoaming(false) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun dataActivity_nullWhenConfigIsOff() = + testScope.runTest { + // Create a new view model here so the constants are properly read + whenever(constants.shouldShowActivityConfig).thenReturn(false) + createAndSetViewModel() + + var inVisible: Boolean? = null + val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this) + + var outVisible: Boolean? = null + val outJob = underTest.activityInVisible.onEach { outVisible = it }.launchIn(this) + + var containerVisible: Boolean? = null + val containerJob = + underTest.activityInVisible.onEach { containerVisible = it }.launchIn(this) + + repository.dataActivityDirection.value = + DataActivityModel(hasActivityIn = true, hasActivityOut = true) + + assertThat(inVisible).isFalse() + assertThat(outVisible).isFalse() + assertThat(containerVisible).isFalse() + + inJob.cancel() + outJob.cancel() + containerJob.cancel() + } + + @Test + @DisableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS) + fun dataActivity_configOn_testIndicators_staticFlagOff() = + testScope.runTest { + // Create a new view model here so the constants are properly read + whenever(constants.shouldShowActivityConfig).thenReturn(true) + createAndSetViewModel() + + var inVisible: Boolean? = null + val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this) + + var outVisible: Boolean? = null + val outJob = underTest.activityOutVisible.onEach { outVisible = it }.launchIn(this) + + var containerVisible: Boolean? = null + val containerJob = + underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this) + + repository.dataActivityDirection.value = + DataActivityModel(hasActivityIn = true, hasActivityOut = false) + + yield() + + assertThat(inVisible).isTrue() + assertThat(outVisible).isFalse() + assertThat(containerVisible).isTrue() + + repository.dataActivityDirection.value = + DataActivityModel(hasActivityIn = false, hasActivityOut = true) + + assertThat(inVisible).isFalse() + assertThat(outVisible).isTrue() + assertThat(containerVisible).isTrue() + + repository.dataActivityDirection.value = + DataActivityModel(hasActivityIn = false, hasActivityOut = false) + + assertThat(inVisible).isFalse() + assertThat(outVisible).isFalse() + assertThat(containerVisible).isFalse() + + inJob.cancel() + outJob.cancel() + containerJob.cancel() + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS) + fun dataActivity_configOn_testIndicators_staticFlagOn() = + testScope.runTest { + // Create a new view model here so the constants are properly read + whenever(constants.shouldShowActivityConfig).thenReturn(true) + createAndSetViewModel() + + var inVisible: Boolean? = null + val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this) + + var outVisible: Boolean? = null + val outJob = underTest.activityOutVisible.onEach { outVisible = it }.launchIn(this) + + var containerVisible: Boolean? = null + val containerJob = + underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this) + + repository.dataActivityDirection.value = + DataActivityModel(hasActivityIn = true, hasActivityOut = false) + + yield() + + assertThat(inVisible).isTrue() + assertThat(outVisible).isFalse() + assertThat(containerVisible).isTrue() + + repository.dataActivityDirection.value = + DataActivityModel(hasActivityIn = false, hasActivityOut = true) + + assertThat(inVisible).isFalse() + assertThat(outVisible).isTrue() + assertThat(containerVisible).isTrue() + + repository.dataActivityDirection.value = + DataActivityModel(hasActivityIn = false, hasActivityOut = false) + + assertThat(inVisible).isFalse() + assertThat(outVisible).isFalse() + assertThat(containerVisible).isTrue() + + inJob.cancel() + outJob.cancel() + containerJob.cancel() + } + + @Test + fun netTypeBackground_nullWhenNoPrioritizedCapabilities() = + testScope.runTest { + createAndSetViewModel() + + val latest by collectLastValue(underTest.networkTypeBackground) + + repository.hasPrioritizedNetworkCapabilities.value = false + + assertThat(latest).isNull() + } + + @Test + @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + fun netTypeBackground_sliceUiEnabled_notNullWhenPrioritizedCapabilities_newIcons() = + testScope.runTest { + createAndSetViewModel() + + val latest by collectLastValue(underTest.networkTypeBackground) + + repository.hasPrioritizedNetworkCapabilities.value = true + + assertThat(latest) + .isEqualTo(Icon.Resource(R.drawable.mobile_network_type_background_updated, null)) + } + + @Test + @DisableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + fun netTypeBackground_sliceUiDisabled_notNullWhenPrioritizedCapabilities_oldIcons() = + testScope.runTest { + createAndSetViewModel() + + val latest by collectLastValue(underTest.networkTypeBackground) + + repository.hasPrioritizedNetworkCapabilities.value = true + + assertThat(latest) + .isEqualTo(Icon.Resource(R.drawable.mobile_network_type_background, null)) + } + + @Test + fun nonTerrestrial_defaultProperties() = + testScope.runTest { + repository.isNonTerrestrial.value = true + + val roaming by collectLastValue(underTest.roaming) + val networkTypeIcon by collectLastValue(underTest.networkTypeIcon) + val networkTypeBackground by collectLastValue(underTest.networkTypeBackground) + val activityInVisible by collectLastValue(underTest.activityInVisible) + val activityOutVisible by collectLastValue(underTest.activityOutVisible) + val activityContainerVisible by collectLastValue(underTest.activityContainerVisible) + + assertThat(roaming).isFalse() + assertThat(networkTypeIcon).isNull() + assertThat(networkTypeBackground).isNull() + assertThat(activityInVisible).isFalse() + assertThat(activityOutVisible).isFalse() + assertThat(activityContainerVisible).isFalse() + } + + @Test + fun nonTerrestrial_ignoresDefaultProperties() = + testScope.runTest { + repository.isNonTerrestrial.value = true + + val roaming by collectLastValue(underTest.roaming) + val networkTypeIcon by collectLastValue(underTest.networkTypeIcon) + val networkTypeBackground by collectLastValue(underTest.networkTypeBackground) + val activityInVisible by collectLastValue(underTest.activityInVisible) + val activityOutVisible by collectLastValue(underTest.activityOutVisible) + val activityContainerVisible by collectLastValue(underTest.activityContainerVisible) + + repository.setAllRoaming(true) + repository.setNetworkTypeKey(connectionsRepository.LTE_KEY) + // sets the background on cellular + repository.hasPrioritizedNetworkCapabilities.value = true + repository.dataActivityDirection.value = + DataActivityModel(hasActivityIn = true, hasActivityOut = true) + + assertThat(roaming).isFalse() + assertThat(networkTypeIcon).isNull() + assertThat(networkTypeBackground).isNull() + assertThat(activityInVisible).isFalse() + assertThat(activityOutVisible).isFalse() + assertThat(activityContainerVisible).isFalse() + } + + @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) + @Test + fun nonTerrestrial_usesSatelliteIcon_flagOff() = + testScope.runTest { + repository.isNonTerrestrial.value = true + repository.setAllLevels(0) + repository.satelliteLevel.value = 0 + + val latest by + collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) + + // Level 0 -> no connection + assertThat(latest).isNotNull() + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) + + // 1-2 -> 1 bar + repository.setAllLevels(1) + repository.satelliteLevel.value = 1 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + repository.setAllLevels(2) + repository.satelliteLevel.value = 2 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + // 3-4 -> 2 bars + repository.setAllLevels(3) + repository.satelliteLevel.value = 3 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + + repository.setAllLevels(4) + repository.satelliteLevel.value = 4 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + } + + @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) + @Test + fun nonTerrestrial_usesSatelliteIcon_flagOn() = + testScope.runTest { + repository.isNonTerrestrial.value = true + repository.satelliteLevel.value = 0 + + val latest by + collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) + + // Level 0 -> no connection + assertThat(latest).isNotNull() + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) + + // 1-2 -> 1 bar + repository.satelliteLevel.value = 1 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + repository.satelliteLevel.value = 2 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + // 3-4 -> 2 bars + repository.satelliteLevel.value = 3 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + + repository.satelliteLevel.value = 4 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + } + + @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) + @Test + fun satelliteIcon_ignoresInflateSignalStrength_flagOff() = + testScope.runTest { + // Note that this is the exact same test as above, but with inflateSignalStrength set to + // true we note that the level is unaffected by inflation + repository.inflateSignalStrength.value = true + repository.isNonTerrestrial.value = true + repository.setAllLevels(0) + repository.satelliteLevel.value = 0 + + val latest by + collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) + + // Level 0 -> no connection + assertThat(latest).isNotNull() + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) + + // 1-2 -> 1 bar + repository.setAllLevels(1) + repository.satelliteLevel.value = 1 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + repository.setAllLevels(2) + repository.satelliteLevel.value = 2 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + // 3-4 -> 2 bars + repository.setAllLevels(3) + repository.satelliteLevel.value = 3 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + + repository.setAllLevels(4) + repository.satelliteLevel.value = 4 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + } + + @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) + @Test + fun satelliteIcon_ignoresInflateSignalStrength_flagOn() = + testScope.runTest { + // Note that this is the exact same test as above, but with inflateSignalStrength set to + // true we note that the level is unaffected by inflation + repository.inflateSignalStrength.value = true + repository.isNonTerrestrial.value = true + repository.satelliteLevel.value = 0 + + val latest by + collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) + + // Level 0 -> no connection + assertThat(latest).isNotNull() + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) + + // 1-2 -> 1 bar + repository.satelliteLevel.value = 1 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + repository.satelliteLevel.value = 2 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + // 3-4 -> 2 bars + repository.satelliteLevel.value = 3 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + + repository.satelliteLevel.value = 4 + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + } + + private fun createAndSetViewModel() { + underTest = + MobileIconViewModelKairos( + SUB_1_ID, + interactor, + airplaneModeInteractor, + constants, + testScope.backgroundScope, + ) + } + + companion object { + private const val SUB_1_ID = 1 + + // For convenience, just define these as constants + private val NO_SIGNAL = R.string.accessibility_no_signal + private val ONE_BAR = R.string.accessibility_one_bar + private val TWO_BARS = R.string.accessibility_two_bars + private val THREE_BARS = R.string.accessibility_three_bars + private val FOUR_BARS = R.string.accessibility_four_bars + private val FULL_BARS = R.string.accessibility_signal_full + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosTest.kt new file mode 100644 index 000000000000..e921430394c2 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosTest.kt @@ -0,0 +1,385 @@ +/* + * 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.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlagsClassic +import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger +import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger +import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy +import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants +import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.isActive +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class MobileIconsViewModelKairosTest : SysuiTestCase() { + private val kosmos = testKosmos() + + private lateinit var underTest: MobileIconsViewModelKairos + private val interactor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) + private val flags = FakeFeatureFlagsClassic() + + private lateinit var airplaneModeInteractor: AirplaneModeInteractor + @Mock private lateinit var constants: ConnectivityConstants + @Mock private lateinit var logger: MobileViewLogger + @Mock private lateinit var verboseLogger: VerboseMobileViewLogger + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + airplaneModeInteractor = + AirplaneModeInteractor( + FakeAirplaneModeRepository(), + FakeConnectivityRepository(), + kosmos.fakeMobileConnectionsRepository, + ) + + underTest = + MobileIconsViewModelKairos( + logger, + verboseLogger, + interactor, + airplaneModeInteractor, + constants, + testScope.backgroundScope, + ) + + interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + } + + @Test + fun subscriptionIdsFlow_matchesInteractor() = + testScope.runTest { + var latest: List<Int>? = null + val job = underTest.subscriptionIdsFlow.onEach { latest = it }.launchIn(this) + + interactor.filteredSubscriptions.value = + listOf( + SubscriptionModel( + subscriptionId = 1, + isOpportunistic = false, + carrierName = "Carrier 1", + profileClass = PROFILE_CLASS_UNSET, + ) + ) + assertThat(latest).isEqualTo(listOf(1)) + + interactor.filteredSubscriptions.value = + listOf( + SubscriptionModel( + subscriptionId = 2, + isOpportunistic = false, + carrierName = "Carrier 2", + profileClass = PROFILE_CLASS_UNSET, + ), + SubscriptionModel( + subscriptionId = 5, + isOpportunistic = true, + carrierName = "Carrier 5", + profileClass = PROFILE_CLASS_UNSET, + ), + SubscriptionModel( + subscriptionId = 7, + isOpportunistic = true, + carrierName = "Carrier 7", + profileClass = PROFILE_CLASS_UNSET, + ), + ) + assertThat(latest).isEqualTo(listOf(2, 5, 7)) + + interactor.filteredSubscriptions.value = emptyList() + assertThat(latest).isEmpty() + + job.cancel() + } + + @Test + fun caching_mobileIconViewModelIsReusedForSameSubId() = + testScope.runTest { + val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME) + val model2 = underTest.viewModelForSub(1, StatusBarLocation.QS) + + assertThat(model1.commonImpl).isSameInstanceAs(model2.commonImpl) + } + + @Test + fun caching_invalidViewModelsAreRemovedFromCacheWhenSubDisappears() = + testScope.runTest { + // Retrieve models to trigger caching + val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME) + val model2 = underTest.viewModelForSub(2, StatusBarLocation.QS) + + // Both impls are cached + assertThat(underTest.reuseCache.keys).containsExactly(1, 2) + + // SUB_1 is removed from the list... + interactor.filteredSubscriptions.value = listOf(SUB_2) + + // ... and dropped from the cache + assertThat(underTest.reuseCache.keys).containsExactly(2) + } + + @Test + fun caching_invalidatedViewModelsAreCanceled() = + testScope.runTest { + // Retrieve models to trigger caching + val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME) + val model2 = underTest.viewModelForSub(2, StatusBarLocation.QS) + + var scope1 = underTest.reuseCache[1]?.second + var scope2 = underTest.reuseCache[2]?.second + + // Scopes are not canceled + assertTrue(scope1!!.isActive) + assertTrue(scope2!!.isActive) + + // SUB_1 is removed from the list... + interactor.filteredSubscriptions.value = listOf(SUB_2) + + // scope1 is canceled + assertFalse(scope1!!.isActive) + assertTrue(scope2!!.isActive) + } + + @Test + fun firstMobileSubShowingNetworkTypeIcon_noSubs_false() = + testScope.runTest { + var latest: Boolean? = null + val job = + underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this) + + interactor.filteredSubscriptions.value = emptyList() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun firstMobileSubShowingNetworkTypeIcon_oneSub_notShowingRat_false() = + testScope.runTest { + var latest: Boolean? = null + val job = + underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this) + + interactor.filteredSubscriptions.value = listOf(SUB_1) + // The unknown icon group doesn't show a RAT + interactor.getInteractorForSubId(1)!!.networkTypeIconGroup.value = + NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun firstMobileSubShowingNetworkTypeIcon_oneSub_showingRat_true() = + testScope.runTest { + var latest: Boolean? = null + val job = + underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this) + + interactor.filteredSubscriptions.value = listOf(SUB_1) + // The 3G icon group will show a RAT + interactor.getInteractorForSubId(1)!!.networkTypeIconGroup.value = + NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G) + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun firstMobileSubShowingNetworkTypeIcon_updatesAsSubUpdates() = + testScope.runTest { + var latest: Boolean? = null + val job = + underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this) + + interactor.filteredSubscriptions.value = listOf(SUB_1) + val sub1Interactor = interactor.getInteractorForSubId(1)!! + + sub1Interactor.networkTypeIconGroup.value = + NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G) + assertThat(latest).isTrue() + + sub1Interactor.networkTypeIconGroup.value = + NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN) + assertThat(latest).isFalse() + + sub1Interactor.networkTypeIconGroup.value = + NetworkTypeIconModel.DefaultIcon(TelephonyIcons.LTE) + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun firstMobileSubShowingNetworkTypeIcon_multipleSubs_lastSubNotShowingRat_false() = + testScope.runTest { + var latest: Boolean? = null + val job = + underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this) + + interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + interactor.getInteractorForSubId(1)?.networkTypeIconGroup?.value = + NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G) + interactor.getInteractorForSubId(2)!!.networkTypeIconGroup.value = + NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun firstMobileSubShowingNetworkTypeIcon_multipleSubs_lastSubShowingRat_true() = + testScope.runTest { + var latest: Boolean? = null + val job = + underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this) + + interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + interactor.getInteractorForSubId(1)?.networkTypeIconGroup?.value = + NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN) + interactor.getInteractorForSubId(2)!!.networkTypeIconGroup.value = + NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G) + + assertThat(latest).isTrue() + job.cancel() + } + + @Test + fun firstMobileSubShowingNetworkTypeIcon_subListUpdates_valAlsoUpdates() = + testScope.runTest { + var latest: Boolean? = null + val job = + underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this) + + interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + interactor.getInteractorForSubId(1)?.networkTypeIconGroup?.value = + NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN) + interactor.getInteractorForSubId(2)!!.networkTypeIconGroup.value = + NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G) + + assertThat(latest).isTrue() + + // WHEN the sub list gets new subscriptions where the last subscription is not showing + // the network type icon + interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2, SUB_3) + interactor.getInteractorForSubId(3)!!.networkTypeIconGroup.value = + NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN) + + // THEN the flow updates + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun firstMobileSubShowingNetworkTypeIcon_subListReorders_valAlsoUpdates() = + testScope.runTest { + var latest: Boolean? = null + val job = + underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this) + + interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + // Immediately switch the order so that we've created both interactors + interactor.filteredSubscriptions.value = listOf(SUB_2, SUB_1) + val sub1Interactor = interactor.getInteractorForSubId(1)!! + val sub2Interactor = interactor.getInteractorForSubId(2)!! + + interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + sub1Interactor.networkTypeIconGroup.value = + NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN) + sub2Interactor.networkTypeIconGroup.value = + NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G) + assertThat(latest).isTrue() + + // WHEN sub1 becomes last and sub1 has no network type icon + interactor.filteredSubscriptions.value = listOf(SUB_2, SUB_1) + + // THEN the flow updates + assertThat(latest).isFalse() + + // WHEN sub2 becomes last and sub2 has a network type icon + interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + + // THEN the flow updates + assertThat(latest).isTrue() + + job.cancel() + } + + companion object { + private val SUB_1 = + SubscriptionModel( + subscriptionId = 1, + isOpportunistic = false, + carrierName = "Carrier 1", + profileClass = PROFILE_CLASS_UNSET, + ) + private val SUB_2 = + SubscriptionModel( + subscriptionId = 2, + isOpportunistic = false, + carrierName = "Carrier 2", + profileClass = PROFILE_CLASS_UNSET, + ) + private val SUB_3 = + SubscriptionModel( + subscriptionId = 3, + isOpportunistic = false, + carrierName = "Carrier 3", + profileClass = PROFILE_CLASS_UNSET, + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosTest.kt new file mode 100644 index 000000000000..ce35d9d8610f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosTest.kt @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.mobile.ui.viewmodel + +import android.platform.test.annotations.EnableFlags +import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.statusbar.core.NewStatusBarIcons +import com.android.systemui.statusbar.core.StatusBarRootModernization +import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class StackedMobileIconViewModelKairosTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val testScope = kosmos.testScope + + private val Kosmos.underTest: StackedMobileIconViewModelKairos by Fixture { + stackedMobileIconViewModelKairos + } + + @Before + fun setUp() { + kosmos.underTest.activateIn(testScope) + } + + @Test + @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + fun dualSim_filtersOutNonDualConnections() = + kosmos.runTest { + fakeMobileIconsInteractor.filteredSubscriptions.value = listOf() + assertThat(underTest.dualSim).isNull() + + fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1) + assertThat(underTest.dualSim).isNull() + + fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2, SUB_3) + assertThat(underTest.dualSim).isNull() + + fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + assertThat(underTest.dualSim).isNotNull() + } + + @Test + @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + fun dualSim_filtersOutNonCellularIcons() = + kosmos.runTest { + fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1) + assertThat(underTest.dualSim).isNull() + + fakeMobileIconsInteractor + .getInteractorForSubId(SUB_1.subscriptionId)!! + .signalLevelIcon + .value = + SignalIconModel.Satellite( + level = 0, + icon = Icon.Resource(res = 0, contentDescription = null), + ) + fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + assertThat(underTest.dualSim).isNull() + } + + @Test + @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) + fun dualSim_tracksActiveSubId() = + kosmos.runTest { + // Active sub id is null, order is unchanged + fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + setIconLevel(SUB_1.subscriptionId, 1) + setIconLevel(SUB_2.subscriptionId, 2) + + assertThat(underTest.dualSim!!.primary.level).isEqualTo(1) + assertThat(underTest.dualSim!!.secondary.level).isEqualTo(2) + + // Active sub is 2, order is swapped + fakeMobileIconsInteractor.activeMobileDataSubscriptionId.value = SUB_2.subscriptionId + + assertThat(underTest.dualSim!!.primary.level).isEqualTo(2) + assertThat(underTest.dualSim!!.secondary.level).isEqualTo(1) + } + + private fun setIconLevel(subId: Int, level: Int) { + with(kosmos.fakeMobileIconsInteractor.getInteractorForSubId(subId)!!) { + signalLevelIcon.value = + (signalLevelIcon.value as SignalIconModel.Cellular).copy(level = level) + } + } + + companion object { + private val SUB_1 = + SubscriptionModel( + subscriptionId = 1, + isOpportunistic = false, + carrierName = "Carrier 1", + profileClass = PROFILE_CLASS_UNSET, + ) + private val SUB_2 = + SubscriptionModel( + subscriptionId = 2, + isOpportunistic = false, + carrierName = "Carrier 2", + profileClass = PROFILE_CLASS_UNSET, + ) + private val SUB_3 = + SubscriptionModel( + subscriptionId = 3, + isOpportunistic = false, + carrierName = "Carrier 3", + profileClass = PROFILE_CLASS_UNSET, + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt new file mode 100644 index 000000000000..6d3813c90bfd --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.topwindoweffects + +import android.view.View +import android.view.WindowManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.app.viewcapture.ViewCapture +import com.android.app.viewcapture.ViewCaptureAwareWindowManager +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyevent.data.repository.fakeKeyEventRepository +import com.android.systemui.keyevent.data.repository.keyEventRepository +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor +import com.android.systemui.keyevent.domain.interactor.keyEventInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.android.systemui.topwindoweffects.data.repository.fakeSqueezeEffectRepository +import com.android.systemui.topwindoweffects.domain.interactor.SqueezeEffectInteractor +import com.android.systemui.topwindoweffects.ui.compose.EffectsWindowRoot +import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.doNothing +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class TopLevelWindowEffectsTest : SysuiTestCase() { + + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + @Mock + private lateinit var windowManager: WindowManager + + @Mock + private lateinit var viewCapture: Lazy<ViewCapture> + + @Mock + private lateinit var viewModelFactory: SqueezeEffectViewModel.Factory + + private val Kosmos.underTest by Kosmos.Fixture { + TopLevelWindowEffects( + context = mContext, + applicationScope = testScope.backgroundScope, + windowManager = ViewCaptureAwareWindowManager( + windowManager = windowManager, + lazyViewCapture = viewCapture, + isViewCaptureEnabled = false + ), + keyEventInteractor = keyEventInteractor, + viewModelFactory = viewModelFactory, + squeezeEffectInteractor = SqueezeEffectInteractor( + squeezeEffectRepository = fakeSqueezeEffectRepository + ) + ) + } + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + doNothing().whenever(windowManager).addView(any<View>(), any<WindowManager.LayoutParams>()) + doNothing().whenever(windowManager).removeView(any<View>()) + doNothing().whenever(windowManager).removeView(any<EffectsWindowRoot>()) + } + + @Test + fun noWindowWhenSqueezeEffectDisabled() = + kosmos.runTest { + fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = false + + underTest.start() + + verify(windowManager, never()).addView(any<View>(), any<WindowManager.LayoutParams>()) + } + + @Test + fun addViewToWindowWhenSqueezeEffectEnabled() = + kosmos.runTest { + fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = true + fakeKeyEventRepository.setPowerButtonDown(true) + + underTest.start() + + verify(windowManager, times(1)).addView(any<View>(), + any<WindowManager.LayoutParams>()) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryTest.kt new file mode 100644 index 000000000000..9b01fd3242e5 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.topwindoweffects.data.repository + +import android.os.Handler +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.provider.Settings.Global.POWER_BUTTON_LONG_PRESS +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.shared.Flags +import com.android.systemui.testKosmos +import com.android.systemui.util.settings.FakeGlobalSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SqueezeEffectRepositoryTest : SysuiTestCase() { + + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val globalSettings = FakeGlobalSettings(StandardTestDispatcher()) + + @Mock + private lateinit var bgHandler: Handler + + private val Kosmos.underTest by Kosmos.Fixture { + SqueezeEffectRepositoryImpl( + bgHandler = bgHandler, + bgCoroutineContext = testScope.testScheduler, + globalSettings = globalSettings + ) + } + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + + @DisableFlags(Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT) + @Test + fun testSqueezeEffectDisabled_WhenFlagDisabled() = + kosmos.runTest { + val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled) + + assertThat(isSqueezeEffectEnabled).isFalse() + } + + @EnableFlags(Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT) + @Test + fun testSqueezeEffectDisabled_WhenFlagEnabled_GlobalSettingsDisabled() = + kosmos.runTest { + globalSettings.putInt(POWER_BUTTON_LONG_PRESS, 0) + + val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled) + + assertThat(isSqueezeEffectEnabled).isFalse() + } + + @EnableFlags(Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT) + @Test + fun testSqueezeEffectEnabled_WhenFlagEnabled_GlobalSettingEnabled() = + kosmos.runTest { + globalSettings.putInt(POWER_BUTTON_LONG_PRESS, 5) + + val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled) + + assertThat(isSqueezeEffectEnabled).isTrue() + } +}
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractorTest.kt new file mode 100644 index 000000000000..a94d49c5d40e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractorTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.topwindoweffects.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.android.systemui.topwindoweffects.data.repository.fakeSqueezeEffectRepository +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SqueezeEffectInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val Kosmos.underTest by Kosmos.Fixture { + SqueezeEffectInteractor( + squeezeEffectRepository = fakeSqueezeEffectRepository + ) + } + + @Test + fun testIsSqueezeEffectDisabled_whenDisabledInRepository() = + kosmos.runTest { + fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = false + + val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled) + + assertThat(isSqueezeEffectEnabled).isFalse() + } + + @Test + fun testIsSqueezeEffectEnabled_whenEnabledInRepository() = + kosmos.runTest { + fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = true + + val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled) + + assertThat(isSqueezeEffectEnabled).isTrue() + } +}
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/ClientTrackingWakeLockTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/ClientTrackingWakeLockTest.kt index fdfcdc486c02..9a58365116fd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/ClientTrackingWakeLockTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/ClientTrackingWakeLockTest.kt @@ -16,14 +16,13 @@ package com.android.systemui.util.wakelock -import android.os.Build import android.os.PowerManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.log.assertLogsWtf import org.junit.After import org.junit.Assert -import org.junit.Assume import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -82,12 +81,11 @@ class ClientTrackingWakeLockTest : SysuiTestCase() { @Test fun wakeLock_releasedTooManyTimes_stillReleased_noThrow() { - Assume.assumeFalse(Build.IS_ENG) mWakeLock.acquire(WHY) mWakeLock.acquire(WHY_2) mWakeLock.release(WHY) mWakeLock.release(WHY_2) - mWakeLock.release(WHY) + assertLogsWtf { mWakeLock.release(WHY) } Assert.assertFalse(mInner.isHeld) } @@ -104,9 +102,8 @@ class ClientTrackingWakeLockTest : SysuiTestCase() { @Test fun prodBuild_wakeLock_releaseWithoutAcquire_noThrow() { - Assume.assumeFalse(Build.IS_ENG) // shouldn't throw an exception on production builds - mWakeLock.release(WHY) + assertLogsWtf { mWakeLock.release(WHY) } } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java index 90aecfb62e91..3951670c4125 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java @@ -19,16 +19,15 @@ package com.android.systemui.util.wakelock; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import android.os.Build; import android.os.PowerManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.log.LogAssertKt; import org.junit.After; -import org.junit.Assume; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -93,8 +92,7 @@ public class WakeLockTest extends SysuiTestCase { @Test public void prodBuild_wakeLock_releaseWithoutAcquire_noThrow() { - Assume.assumeFalse(Build.IS_ENG); // shouldn't throw an exception on production builds - mWakeLock.release(WHY); + LogAssertKt.assertRunnableLogsWtf(() -> mWakeLock.release(WHY)); } } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt index b52db83d513c..7657a2179d4f 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt @@ -13,7 +13,6 @@ */ package com.android.systemui.plugins.clocks -import android.graphics.RectF import com.android.systemui.plugins.annotations.ProtectedInterface import com.android.systemui.plugins.annotations.SimpleProperty import java.io.PrintWriter @@ -50,5 +49,5 @@ interface ClockController { } interface ClockEventListener { - fun onBoundsChanged(bounds: RectF) + fun onBoundsChanged(bounds: VRectF) } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt index 029e54658f60..a658c15a1a99 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt @@ -13,6 +13,7 @@ */ package com.android.systemui.plugins.clocks +import android.content.Context import android.graphics.Rect import com.android.systemui.plugins.annotations.ProtectedInterface @@ -44,6 +45,7 @@ interface ClockFaceEvents { * render within the centered targetRect to avoid obstructing other elements. The specified * targetRegion is relative to the parent view. */ + @Deprecated("No longer necessary, pending removal") fun onTargetRegionChanged(targetRegion: Rect?) /** Called to notify the clock about its display. */ @@ -60,4 +62,12 @@ data class ThemeConfig( * value denotes that we should use the seed color for the current system theme. */ val seedColor: Int?, -) +) { + fun getDefaultColor(context: Context): Int { + return when { + seedColor != null -> seedColor!! + isDarkTheme -> context.resources.getColor(android.R.color.system_accent1_100) + else -> context.resources.getColor(android.R.color.system_accent2_600) + } + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt index 02a3902a042c..f9ff75d5fdc8 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt @@ -16,7 +16,6 @@ package com.android.systemui.plugins.clocks -import android.graphics.Rect import android.view.View import android.view.View.MeasureSpec import com.android.systemui.log.core.LogLevel @@ -56,12 +55,9 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) : } fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { - d({ "onLayout($bool1, ${Rect(int1, int2, long1.toInt(), long2.toInt())})" }) { + d({ "onLayout($bool1, ${VRect(long1.toULong())})" }) { bool1 = changed - int1 = left - int2 = top - long1 = right.toLong() - long2 = bottom.toLong() + long1 = VRect(left, top, right, bottom).data.toLong() } } @@ -108,8 +104,11 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) : } } - fun animateDoze() { - d("animateDoze()") + fun animateDoze(isDozing: Boolean, isAnimated: Boolean) { + d({ "animateDoze(isDozing=$bool1, isAnimated=$bool2)" }) { + bool1 = isDozing + bool2 = isAnimated + } } fun animateCharge() { @@ -117,10 +116,7 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) : } fun animateFidget(x: Float, y: Float) { - d({ "animateFidget($str1, $str2)" }) { - str1 = x.toString() - str2 = y.toString() - } + d({ "animateFidget(${VPointF(long1.toULong())})" }) { long1 = VPointF(x, y).data.toLong() } } companion object { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/VPoint.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VPoint.kt index 3dae5305542b..1fb37ec28835 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/VPoint.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VPoint.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.shared.clocks +package com.android.systemui.plugins.clocks import android.graphics.Point import android.graphics.PointF @@ -30,22 +30,20 @@ private val Y_MASK: ULong = 0x00000000FFFFFFFFU private fun unpackX(data: ULong): Int = ((data and X_MASK) shr 32).toInt() -private fun unpackY(data: ULong): Int = (data and Y_MASK).toInt() +private fun unpackY(data: ULong): Int = ((data and Y_MASK) shr 0).toInt() private fun pack(x: Int, y: Int): ULong { - return ((x.toULong() shl 32) and X_MASK) or (y.toULong() and Y_MASK) + return ((x.toULong() shl 32) and X_MASK) or ((y.toULong() shl 0) and Y_MASK) } @JvmInline -value class VPointF(private val data: ULong) { +value class VPointF(val data: ULong) { val x: Float get() = Float.fromBits(unpackX(data)) val y: Float get() = Float.fromBits(unpackY(data)) - constructor() : this(0f, 0f) - constructor(pt: PointF) : this(pt.x, pt.y) constructor(x: Int, y: Int) : this(x.toFloat(), y.toFloat()) @@ -139,15 +137,13 @@ value class VPointF(private val data: ULong) { } @JvmInline -value class VPoint(private val data: ULong) { +value class VPoint(val data: ULong) { val x: Int get() = unpackX(data) val y: Int get() = unpackY(data) - constructor() : this(0, 0) - constructor(x: Int, y: Int) : this(pack(x, y)) fun toPoint() = Point(x, y) diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt new file mode 100644 index 000000000000..3c1adf22a405 --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.plugins.clocks + +import android.graphics.Rect +import android.graphics.RectF +import android.util.Half + +private val LEFT_MASK: ULong = 0xFFFF000000000000U +private val TOP_MASK: ULong = 0x0000FFFF00000000U +private val RIGHT_MASK: ULong = 0x00000000FFFF0000U +private val BOTTOM_MASK: ULong = 0x000000000000FFFFU + +private fun unpackLeft(data: ULong): Short = ((data and LEFT_MASK) shr 48).toShort() + +private fun unpackTop(data: ULong): Short = ((data and TOP_MASK) shr 32).toShort() + +private fun unpackRight(data: ULong): Short = ((data and RIGHT_MASK) shr 16).toShort() + +private fun unpackBottom(data: ULong): Short = ((data and BOTTOM_MASK) shr 0).toShort() + +private fun pack(left: Short, top: Short, right: Short, bottom: Short): ULong { + return ((left.toULong() shl 48) and LEFT_MASK) or + ((top.toULong() shl 32) and TOP_MASK) or + ((right.toULong() shl 16) and RIGHT_MASK) or + ((bottom.toULong() shl 0) and BOTTOM_MASK) +} + +@JvmInline +value class VRectF(val data: ULong) { + val left: Float + get() = fromBits(unpackLeft(data)) + + val top: Float + get() = fromBits(unpackTop(data)) + + val right: Float + get() = fromBits(unpackRight(data)) + + val bottom: Float + get() = fromBits(unpackBottom(data)) + + val width: Float + get() = right - left + + val height: Float + get() = bottom - top + + constructor(rect: RectF) : this(rect.left, rect.top, rect.right, rect.bottom) + + constructor( + rect: Rect + ) : this( + left = rect.left.toFloat(), + top = rect.top.toFloat(), + right = rect.right.toFloat(), + bottom = rect.bottom.toFloat(), + ) + + constructor( + left: Float, + top: Float, + right: Float, + bottom: Float, + ) : this(pack(toBits(left), toBits(top), toBits(right), toBits(bottom))) + + val center: VPointF + get() = VPointF(left, top) + size / 2f + + val size: VPointF + get() = VPointF(width, height) + + override fun toString() = "($left, $top) -> ($right, $bottom)" + + companion object { + private fun toBits(value: Float): Short = Half.halfToShortBits(Half.toHalf(value)) + + private fun fromBits(value: Short): Float = Half.toFloat(Half.intBitsToHalf(value.toInt())) + + fun fromCenter(center: VPointF, size: VPointF): VRectF { + return VRectF( + center.x - size.x / 2, + center.y - size.y / 2, + center.x + size.x / 2, + center.y + size.y / 2, + ) + } + + fun fromTopLeft(pos: VPointF, size: VPointF): VRectF { + return VRectF(pos.x, pos.y, pos.x + size.x, pos.y + size.y) + } + + val ZERO = VRectF(0f, 0f, 0f, 0f) + } +} + +@JvmInline +value class VRect(val data: ULong) { + val left: Int + get() = unpackLeft(data).toInt() + + val top: Int + get() = unpackTop(data).toInt() + + val right: Int + get() = unpackRight(data).toInt() + + val bottom: Int + get() = unpackBottom(data).toInt() + + val width: Int + get() = right - left + + val height: Int + get() = bottom - top + + constructor( + rect: Rect + ) : this( + left = rect.left.toShort(), + top = rect.top.toShort(), + right = rect.right.toShort(), + bottom = rect.bottom.toShort(), + ) + + constructor( + left: Int, + top: Int, + right: Int, + bottom: Int, + ) : this( + left = left.toShort(), + top = top.toShort(), + right = right.toShort(), + bottom = bottom.toShort(), + ) + + constructor( + left: Short, + top: Short, + right: Short, + bottom: Short, + ) : this(pack(left, top, right, bottom)) + + val center: VPoint + get() = VPoint(left, top) + size / 2 + + val size: VPoint + get() = VPoint(width, height) + + override fun toString() = "($left, $top) -> ($right, $bottom)" + + companion object { + val ZERO = VRect(0, 0, 0, 0) + + fun fromCenter(center: VPoint, size: VPoint): VRect { + return VRect( + (center.x - size.x / 2).toShort(), + (center.y - size.y / 2).toShort(), + (center.x + size.x / 2).toShort(), + (center.y + size.y / 2).toShort(), + ) + } + + fun fromTopLeft(pos: VPoint, size: VPoint): VRect { + return VRect( + pos.x.toShort(), + pos.y.toShort(), + (pos.x + size.x).toShort(), + (pos.y + size.y).toShort(), + ) + } + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt index f920b187e7e5..f59dda049aa1 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt @@ -24,6 +24,8 @@ data class WeatherData( @VisibleForTesting const val TEMPERATURE_KEY = "temperature" private const val INVALID_WEATHER_ICON_STATE = -1 + @JvmStatic + @JvmOverloads fun fromBundle(extras: Bundle, touchAction: WeatherTouchAction? = null): WeatherData? { val description = extras.getString(DESCRIPTION_KEY) val state = @@ -46,7 +48,7 @@ data class WeatherData( state = state, useCelsius = extras.getBoolean(USE_CELSIUS_KEY), temperature = temperature, - touchAction = touchAction + touchAction = touchAction, ) if (DEBUG) { Log.i(TAG, "Weather data parsed $result from $extras") @@ -87,53 +89,53 @@ data class WeatherData( } // Values for WeatherStateIcon must stay in sync with go/g3-WeatherStateIcon - enum class WeatherStateIcon(val id: Int) { - UNKNOWN_ICON(0), + enum class WeatherStateIcon(val id: Int, val icon: String) { + UNKNOWN_ICON(0, ""), // Clear, day & night. - SUNNY(1), - CLEAR_NIGHT(2), + SUNNY(1, "a"), + CLEAR_NIGHT(2, "f"), // Mostly clear, day & night. - MOSTLY_SUNNY(3), - MOSTLY_CLEAR_NIGHT(4), + MOSTLY_SUNNY(3, "b"), + MOSTLY_CLEAR_NIGHT(4, "n"), // Partly cloudy, day & night. - PARTLY_CLOUDY(5), - PARTLY_CLOUDY_NIGHT(6), + PARTLY_CLOUDY(5, "b"), + PARTLY_CLOUDY_NIGHT(6, "n"), // Mostly cloudy, day & night. - MOSTLY_CLOUDY_DAY(7), - MOSTLY_CLOUDY_NIGHT(8), - CLOUDY(9), - HAZE_FOG_DUST_SMOKE(10), - DRIZZLE(11), - HEAVY_RAIN(12), - SHOWERS_RAIN(13), + MOSTLY_CLOUDY_DAY(7, "e"), + MOSTLY_CLOUDY_NIGHT(8, "e"), + CLOUDY(9, "e"), + HAZE_FOG_DUST_SMOKE(10, "d"), + DRIZZLE(11, "c"), + HEAVY_RAIN(12, "c"), + SHOWERS_RAIN(13, "c"), // Scattered showers, day & night. - SCATTERED_SHOWERS_DAY(14), - SCATTERED_SHOWERS_NIGHT(15), + SCATTERED_SHOWERS_DAY(14, "c"), + SCATTERED_SHOWERS_NIGHT(15, "c"), // Isolated scattered thunderstorms, day & night. - ISOLATED_SCATTERED_TSTORMS_DAY(16), - ISOLATED_SCATTERED_TSTORMS_NIGHT(17), - STRONG_TSTORMS(18), - BLIZZARD(19), - BLOWING_SNOW(20), - FLURRIES(21), - HEAVY_SNOW(22), + ISOLATED_SCATTERED_TSTORMS_DAY(16, "i"), + ISOLATED_SCATTERED_TSTORMS_NIGHT(17, "i"), + STRONG_TSTORMS(18, "i"), + BLIZZARD(19, "j"), + BLOWING_SNOW(20, "j"), + FLURRIES(21, "h"), + HEAVY_SNOW(22, "j"), // Scattered snow showers, day & night. - SCATTERED_SNOW_SHOWERS_DAY(23), - SCATTERED_SNOW_SHOWERS_NIGHT(24), - SNOW_SHOWERS_SNOW(25), - MIXED_RAIN_HAIL_RAIN_SLEET(26), - SLEET_HAIL(27), - TORNADO(28), - TROPICAL_STORM_HURRICANE(29), - WINDY_BREEZY(30), - WINTRY_MIX_RAIN_SNOW(31); + SCATTERED_SNOW_SHOWERS_DAY(23, "h"), + SCATTERED_SNOW_SHOWERS_NIGHT(24, "h"), + SNOW_SHOWERS_SNOW(25, "g"), + MIXED_RAIN_HAIL_RAIN_SLEET(26, "h"), + SLEET_HAIL(27, "h"), + TORNADO(28, "l"), + TROPICAL_STORM_HURRICANE(29, "m"), + WINDY_BREEZY(30, "k"), + WINTRY_MIX_RAIN_SNOW(31, "h"); companion object { fun fromInt(value: Int) = values().firstOrNull { it.id == value } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt index be0362fd7481..ac7a85742385 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt @@ -16,15 +16,13 @@ package com.android.systemui.plugins.qs -/** - * The base view model class for rendering the Tile's TileDetailsView. - */ -abstract class TileDetailsViewModel { +/** The view model interface for rendering the Tile's TileDetailsView. */ +interface TileDetailsViewModel { // The callback when the settings button is clicked. Currently this is the same as the on tile // long press callback - abstract fun clickOnSettingsButton() + fun clickOnSettingsButton() - abstract fun getTitle(): String + val title: String - abstract fun getSubTitle(): String + val subTitle: String } diff --git a/packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml b/packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml new file mode 100644 index 000000000000..d398f60ddc3c --- /dev/null +++ b/packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/notification_ripple_untinted_color"> + <item> + <inset + android:insetLeft="0dp" + android:insetTop="8dp" + android:insetRight="0dp" + android:insetBottom="8dp"> + <shape android:shape="rectangle"> + <corners android:radius="@dimen/notification_2025_smart_reply_button_corner_radius" /> + <stroke android:width="@dimen/smart_reply_button_stroke_width" + android:color="@color/smart_reply_button_stroke" /> + <solid android:color="@color/smart_reply_button_background"/> + </shape> + </inset> + </item> +</ripple> diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml index 915563b1ae20..c7add163dffa 100644 --- a/packages/SystemUI/res/layout/clipboard_overlay.xml +++ b/packages/SystemUI/res/layout/clipboard_overlay.xml @@ -85,6 +85,7 @@ android:paddingVertical="@dimen/overlay_action_container_padding_vertical" android:elevation="4dp" android:scrollbars="none" + android:importantForAccessibility="no" app:layout_constraintHorizontal_bias="0" app:layout_constraintWidth_percent="1.0" app:layout_constraintWidth_max="wrap" @@ -176,6 +177,8 @@ app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="4dp" android:layout_marginBottom="2dp" + android:importantForAccessibility="yes" + android:contentDescription="@string/clipboard_overlay_window_name" android:background="@drawable/clipboard_minimized_background_inset"> <ImageView android:src="@drawable/ic_content_paste" diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml index 6f8b4cd3e4a9..9b629ace76af 100644 --- a/packages/SystemUI/res/layout/media_output_dialog.xml +++ b/packages/SystemUI/res/layout/media_output_dialog.xml @@ -15,8 +15,8 @@ ~ limitations under the License. --> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/media_output_dialog" android:layout_width="@dimen/large_dialog_width" android:layout_height="wrap_content" @@ -35,24 +35,25 @@ android:orientation="horizontal"> <ImageView android:id="@+id/header_icon" - android:layout_width="72dp" - android:layout_height="72dp" + android:layout_width="@dimen/media_output_dialog_header_album_icon_size" + android:layout_height="@dimen/media_output_dialog_header_album_icon_size" + android:layout_marginEnd="@dimen/media_output_dialog_header_icon_padding" android:importantForAccessibility="no"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingStart="12dp" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:gravity="center_vertical" + android:gravity="top" android:orientation="horizontal"> <ImageView android:id="@+id/app_source_icon" android:layout_width="20dp" android:layout_height="20dp" + android:layout_marginBottom="7dp" android:gravity="center_vertical" android:importantForAccessibility="no"/> @@ -103,12 +104,11 @@ android:layout_height="wrap_content" > </ViewStub> - <LinearLayout + <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/device_list" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_weight="1" - android:orientation="vertical"> + android:layout_height="0dp" + android:layout_weight="1"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/list_result" @@ -116,8 +116,11 @@ android:paddingTop="8dp" android:clipToPadding="false" android:layout_width="match_parent" - android:layout_height="wrap_content"/> - </LinearLayout> + android:layout_height="wrap_content" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintHeight_max="@dimen/media_output_dialog_list_max_height"/> + </androidx.constraintlayout.widget.ConstraintLayout> <LinearLayout android:layout_width="match_parent" diff --git a/packages/SystemUI/res/layout/notif_half_shelf.xml b/packages/SystemUI/res/layout/notif_half_shelf.xml index 9a66ca9f3baa..603cc00a312c 100644 --- a/packages/SystemUI/res/layout/notif_half_shelf.xml +++ b/packages/SystemUI/res/layout/notif_half_shelf.xml @@ -71,15 +71,18 @@ android:textSize="16sp" /> - <Switch - android:id="@+id/toggle" + <com.google.android.material.materialswitch.MaterialSwitch + android:theme="@style/Theme.Material3.DynamicColors.DayNight" + android:id="@+id/material_toggle" + android:filterTouchesWhenObscured="false" + android:clickable="true" + android:focusable="true" + android:padding="8dp" android:layout_height="48dp" android:layout_width="wrap_content" android:layout_gravity="center_vertical" - android:padding="8dp" - android:track="@drawable/settingslib_track_selector" - android:thumb="@drawable/settingslib_thumb_selector" - android:theme="@style/MainSwitch.Settingslib"/> + style="@style/SettingslibSwitchStyle.Expressive"/> + </com.android.systemui.statusbar.notification.row.AppControlView> <ScrollView diff --git a/packages/SystemUI/res/layout/notif_half_shelf_row.xml b/packages/SystemUI/res/layout/notif_half_shelf_row.xml index b2eaa6ce92b5..4bc37f82a9d7 100644 --- a/packages/SystemUI/res/layout/notif_half_shelf_row.xml +++ b/packages/SystemUI/res/layout/notif_half_shelf_row.xml @@ -80,15 +80,16 @@ /> </RelativeLayout> - <Switch - android:id="@+id/toggle" + <com.google.android.material.materialswitch.MaterialSwitch + android:theme="@style/Theme.Material3.DynamicColors.DayNight" + android:id="@+id/material_toggle" + android:filterTouchesWhenObscured="false" + android:clickable="true" + android:focusable="true" + android:padding="8dp" android:layout_height="48dp" android:layout_width="wrap_content" android:layout_gravity="center_vertical" - android:padding="8dp" - android:track="@drawable/settingslib_track_selector" - android:thumb="@drawable/settingslib_thumb_selector" - android:theme="@style/MainSwitch.Settingslib" - /> + style="@style/SettingslibSwitchStyle.Expressive"/> </LinearLayout> </com.android.systemui.statusbar.notification.row.ChannelRow> diff --git a/packages/SystemUI/res/layout/notification_2025_smart_action_button.xml b/packages/SystemUI/res/layout/notification_2025_smart_action_button.xml new file mode 100644 index 000000000000..ed905885a76f --- /dev/null +++ b/packages/SystemUI/res/layout/notification_2025_smart_action_button.xml @@ -0,0 +1,35 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT 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:paddingHorizontal is set dynamically in SmartReplyView. --> +<Button xmlns:android="http://schemas.android.com/apk/res/android" + style="@android:style/Widget.Material.Button" + android:stateListAnimator="@null" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:minWidth="0dp" + android:minHeight="@dimen/notification_2025_smart_reply_button_min_height" + android:paddingVertical="@dimen/smart_reply_button_padding_vertical" + android:background="@drawable/notification_2025_smart_reply_button_background" + android:gravity="center" + android:fontFamily="google-sans-flex" + android:textSize="@dimen/smart_reply_button_font_size" + android:textColor="@color/smart_reply_button_text" + android:paddingStart="@dimen/smart_reply_button_action_padding_left" + android:paddingEnd="@dimen/smart_reply_button_padding_horizontal" + android:drawablePadding="@dimen/smart_action_button_icon_padding" + android:textStyle="normal" + android:ellipsize="none"/> diff --git a/packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml b/packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml new file mode 100644 index 000000000000..4f543e5099bf --- /dev/null +++ b/packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT 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:paddingHorizontal is set dynamically in SmartReplyView. --> +<Button xmlns:android="http://schemas.android.com/apk/res/android" + style="@android:style/Widget.Material.Button" + android:stateListAnimator="@null" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:minWidth="0dp" + android:minHeight="@dimen/notification_2025_smart_reply_button_min_height" + android:paddingVertical="@dimen/smart_reply_button_padding_vertical" + android:background="@drawable/notification_2025_smart_reply_button_background" + android:gravity="center" + android:fontFamily="google-sans-flex" + android:textSize="@dimen/smart_reply_button_font_size" + android:textColor="@color/smart_reply_button_text" + android:paddingStart="@dimen/smart_reply_button_padding_horizontal" + android:paddingEnd="@dimen/smart_reply_button_padding_horizontal" + android:textStyle="normal" + android:ellipsize="none"/> diff --git a/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json b/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json index b1d6a270bc67..3f03fcff7603 100644 --- a/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json +++ b/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json @@ -1 +1 @@ -{"v":"5.7.13","fr":60,"ip":0,"op":55,"w":80,"h":80,"nm":"unlocked_to_checkmark_success","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[47.143,32,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.761,0],[0,-2.7],[0,0]],"o":[[0,0],[0,-2.7],[-2.761,0],[0,0],[0,0]],"v":[[5,5],[5,-0.111],[0,-5],[-5,-0.111],[-5.01,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[38,45,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[18,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[37.999,44.999,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.42,0],[0,1.42],[1.42,0],[0,-1.42]],"o":[[1.42,0],[0,-1.42],[-1.42,0],[0,1.42]],"v":[[0,2.571],[2.571,0],[0,-2.571],[-2.571,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40.5,40.75,0],"ix":2,"l":2},"a":{"a":0,"k":[12.5,-6.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[60,60,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":19,"s":[112,112,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-10.556,-9.889],[7.444,6.555],[34.597,-20.486]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":910,"st":10,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[93.5,93.5,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[4]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":10,"op":77,"st":10,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".grey700","cl":"grey700","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.278431385756,0.278431385756,0.278431385756,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":20,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file +{"v":"5.7.13","fr":60,"ip":0,"op":55,"w":80,"h":80,"nm":"unlocked_to_checkmark_success","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[47.143,32,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.761,0],[0,-2.7],[0,0]],"o":[[0,0],[0,-2.7],[-2.761,0],[0,0],[0,0]],"v":[[5,5],[5,-0.111],[0,-5],[-5,-0.111],[-5.01,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[38,45,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[18,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[37.999,44.999,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.42,0],[0,1.42],[1.42,0],[0,-1.42]],"o":[[1.42,0],[0,-1.42],[-1.42,0],[0,1.42]],"v":[[0,2.571],[2.571,0],[0,-2.571],[-2.571,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40.5,40.75,0],"ix":2,"l":2},"a":{"a":0,"k":[12.5,-6.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[60,60,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":19,"s":[112,112,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-10.556,-9.889],[7.444,6.555],[34.597,-20.486]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":910,"st":10,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[93.5,93.5,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[4]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":10,"op":77,"st":10,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.278431385756,0.278431385756,0.278431385756,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":20,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 8342a9cc244b..d0ae307b6919 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -283,7 +283,7 @@ <dimen name="notification_max_height">358dp</dimen> <!-- Height of a large promoted ongoing notification in the status bar --> - <dimen name="notification_max_height_for_promoted_ongoing">272dp</dimen> + <dimen name="notification_max_height_for_promoted_ongoing">218dp</dimen> <!-- Height of a heads up notification in the status bar for legacy custom views --> <dimen name="notification_max_heads_up_height_legacy">128dp</dimen> @@ -1157,6 +1157,8 @@ <dimen name="smart_action_button_icon_size">18dp</dimen> <dimen name="smart_action_button_icon_padding">8dp</dimen> <dimen name="smart_action_button_outline_stroke_width">2dp</dimen> + <dimen name="notification_2025_smart_reply_button_corner_radius">18dp</dimen> + <dimen name="notification_2025_smart_reply_button_min_height">48dp</dimen> <!-- Magic Action params. --> <!-- Corner radius = half of min_height to create rounded sides. --> @@ -2166,7 +2168,7 @@ <dimen name="volume_dialog_background_top_margin">-28dp</dimen> <dimen name="volume_dialog_window_margin">14dp</dimen> - <dimen name="volume_dialog_components_spacing">8dp</dimen> + <dimen name="volume_dialog_components_spacing">10dp</dimen> <dimen name="volume_dialog_floating_sliders_spacing">8dp</dimen> <dimen name="volume_dialog_floating_sliders_vertical_padding">10dp</dimen> <dimen name="volume_dialog_floating_sliders_vertical_padding_negative"> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index c06c17a0844f..8c1fd65d96d4 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2357,8 +2357,8 @@ <string name="system_multitasking_lhs">Use split screen with app on the left</string> <!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] --> <string name="system_multitasking_full_screen">Use full screen</string> - <!-- User visible title for the keyboard shortcut that switches to desktop view [CHAR LIMIT=70] --> - <string name="system_multitasking_desktop_view">Use desktop view</string> + <!-- User visible title for the keyboard shortcut that switches to desktop windowing [CHAR LIMIT=70] --> + <string name="system_multitasking_desktop_view">Use desktop windowing</string> <!-- User visible title for the keyboard shortcut that switches to app on right or below while using split screen [CHAR LIMIT=70] --> <string name="system_multitasking_splitscreen_focus_rhs">Switch to app on right or below while using split screen</string> <!-- User visible title for the keyboard shortcut that switches to app on left or above while using split screen [CHAR LIMIT=70] --> diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 1549b699eee6..763b1072f968 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -21,7 +21,6 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.res.Resources -import android.graphics.RectF import android.os.Trace import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS import android.provider.Settings.Global.ZEN_MODE_OFF @@ -60,6 +59,7 @@ import com.android.systemui.plugins.clocks.ClockEventListener import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.plugins.clocks.ClockMessageBuffers import com.android.systemui.plugins.clocks.ClockTickRate +import com.android.systemui.plugins.clocks.VRectF import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.plugins.clocks.ZenData import com.android.systemui.plugins.clocks.ZenData.ZenMode @@ -250,7 +250,7 @@ constructor( private var largeClockOnSecondaryDisplay = false val dozeAmount = MutableStateFlow(0f) - val onClockBoundsChanged = MutableStateFlow<RectF?>(null) + val onClockBoundsChanged = MutableStateFlow<VRectF>(VRectF.ZERO) private fun isDarkTheme(): Boolean { val isLightTheme = TypedValue() @@ -315,7 +315,7 @@ constructor( private val clockListener = object : ClockEventListener { - override fun onBoundsChanged(bounds: RectF) { + override fun onBoundsChanged(bounds: VRectF) { onClockBoundsChanged.value = bounds } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index ec97b8a96c1f..b8726101602c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -21,6 +21,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.res.ColorStateList; import android.content.res.Resources; +import android.hardware.input.InputManager; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; @@ -219,6 +220,7 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> private final KeyguardKeyboardInteractor mKeyguardKeyboardInteractor; private final BouncerHapticPlayer mBouncerHapticPlayer; private final UserActivityNotifier mUserActivityNotifier; + private final InputManager mInputManager; @Inject public Factory(KeyguardUpdateMonitor keyguardUpdateMonitor, @@ -235,7 +237,8 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> UiEventLogger uiEventLogger, KeyguardKeyboardInteractor keyguardKeyboardInteractor, BouncerHapticPlayer bouncerHapticPlayer, - UserActivityNotifier userActivityNotifier) { + UserActivityNotifier userActivityNotifier, + InputManager inputManager) { mKeyguardUpdateMonitor = keyguardUpdateMonitor; mLockPatternUtils = lockPatternUtils; mLatencyTracker = latencyTracker; @@ -254,6 +257,7 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> mKeyguardKeyboardInteractor = keyguardKeyboardInteractor; mBouncerHapticPlayer = bouncerHapticPlayer; mUserActivityNotifier = userActivityNotifier; + mInputManager = inputManager; } /** Create a new {@link KeyguardInputViewController}. */ @@ -285,22 +289,23 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> emergencyButtonController, mFalsingCollector, mDevicePostureController, mFeatureFlags, mSelectedUserInteractor, mUiEventLogger, mKeyguardKeyboardInteractor, mBouncerHapticPlayer, - mUserActivityNotifier); + mUserActivityNotifier, mInputManager); } else if (keyguardInputView instanceof KeyguardSimPinView) { return new KeyguardSimPinViewController((KeyguardSimPinView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mTelephonyManager, mFalsingCollector, emergencyButtonController, mFeatureFlags, mSelectedUserInteractor, - mKeyguardKeyboardInteractor, mBouncerHapticPlayer, mUserActivityNotifier); + mKeyguardKeyboardInteractor, mBouncerHapticPlayer, mUserActivityNotifier, + mInputManager); } else if (keyguardInputView instanceof KeyguardSimPukView) { return new KeyguardSimPukViewController((KeyguardSimPukView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mTelephonyManager, mFalsingCollector, emergencyButtonController, mFeatureFlags, mSelectedUserInteractor, - mKeyguardKeyboardInteractor, mBouncerHapticPlayer, mUserActivityNotifier - ); + mKeyguardKeyboardInteractor, mBouncerHapticPlayer, mUserActivityNotifier, + mInputManager); } throw new RuntimeException("Unable to find controller for " + keyguardInputView); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java index 0e9d8fec9717..ec9aedfc7551 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java @@ -18,12 +18,14 @@ package com.android.keyguard; import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED; +import static com.android.internal.widget.flags.Flags.hideLastCharWithPhysicalInput; import static com.android.systemui.Flags.pinInputFieldStyledFocusState; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.StateListDrawable; +import android.hardware.input.InputManager; import android.util.TypedValue; import android.view.KeyEvent; import android.view.MotionEvent; @@ -43,11 +45,13 @@ import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinBasedInputView> - extends KeyguardAbsKeyInputViewController<T> { + extends KeyguardAbsKeyInputViewController<T> implements InputManager.InputDeviceListener { private final FalsingCollector mFalsingCollector; private final KeyguardKeyboardInteractor mKeyguardKeyboardInteractor; protected PasswordTextView mPasswordEntry; + private Boolean mShowAnimations; + private InputManager mInputManager; private final OnKeyListener mOnKeyListener = (v, keyCode, event) -> { if (event.getAction() == KeyEvent.ACTION_DOWN) { @@ -79,7 +83,8 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB SelectedUserInteractor selectedUserInteractor, KeyguardKeyboardInteractor keyguardKeyboardInteractor, BouncerHapticPlayer bouncerHapticPlayer, - UserActivityNotifier userActivityNotifier) { + UserActivityNotifier userActivityNotifier, + InputManager inputManager) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, falsingCollector, emergencyButtonController, featureFlags, selectedUserInteractor, @@ -87,6 +92,51 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB mFalsingCollector = falsingCollector; mKeyguardKeyboardInteractor = keyguardKeyboardInteractor; mPasswordEntry = mView.findViewById(mView.getPasswordTextViewId()); + mInputManager = inputManager; + mShowAnimations = null; + } + + private void updateAnimations(Boolean showAnimations) { + if (!hideLastCharWithPhysicalInput()) return; + + if (showAnimations == null) { + showAnimations = !mLockPatternUtils + .isPinEnhancedPrivacyEnabled(mSelectedUserInteractor.getSelectedUserId()); + } + if (mShowAnimations != null && showAnimations.equals(mShowAnimations)) return; + mShowAnimations = showAnimations; + + for (NumPadKey button : mView.getButtons()) { + button.setAnimationEnabled(mShowAnimations); + } + mPasswordEntry.setShowPassword(mShowAnimations); + } + + @Override + public void onInputDeviceAdded(int deviceId) { + if (!hideLastCharWithPhysicalInput()) return; + + // If we were showing animations before maybe the new device is a keyboard. + if (mShowAnimations) { + updateAnimations(null); + } + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + if (!hideLastCharWithPhysicalInput()) return; + + // If we were hiding animations because of a keyboard the keyboard may have been unplugged. + if (!mShowAnimations) { + updateAnimations(null); + } + } + + @Override + public void onInputDeviceChanged(int deviceId) { + if (!hideLastCharWithPhysicalInput()) return; + + updateAnimations(null); } @Override @@ -95,7 +145,13 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB boolean showAnimations = !mLockPatternUtils .isPinEnhancedPrivacyEnabled(mSelectedUserInteractor.getSelectedUserId()); - mPasswordEntry.setShowPassword(showAnimations); + if (hideLastCharWithPhysicalInput()) { + mInputManager.registerInputDeviceListener(this, null); + updateAnimations(showAnimations); + } else { + mPasswordEntry.setShowPassword(showAnimations); + } + for (NumPadKey button : mView.getButtons()) { button.setOnTouchListener((v, event) -> { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { @@ -103,7 +159,9 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB } return false; }); - button.setAnimationEnabled(showAnimations); + if (!hideLastCharWithPhysicalInput()) { + button.setAnimationEnabled(showAnimations); + } button.setBouncerHapticHelper(mBouncerHapticPlayer); } mPasswordEntry.setOnKeyListener(mOnKeyListener); @@ -191,6 +249,10 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB protected void onViewDetached() { super.onViewDetached(); + if (hideLastCharWithPhysicalInput()) { + mInputManager.unregisterInputDeviceListener(this); + } + for (NumPadKey button : mView.getButtons()) { button.setOnTouchListener(null); button.setBouncerHapticHelper(null); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index 9ae4cc6a4b4f..eefcab38ecd3 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -18,6 +18,7 @@ package com.android.keyguard; import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE; +import android.hardware.input.InputManager; import android.view.View; import com.android.internal.logging.UiEvent; @@ -63,11 +64,13 @@ public class KeyguardPinViewController SelectedUserInteractor selectedUserInteractor, UiEventLogger uiEventLogger, KeyguardKeyboardInteractor keyguardKeyboardInteractor, BouncerHapticPlayer bouncerHapticPlayer, - UserActivityNotifier userActivityNotifier) { + UserActivityNotifier userActivityNotifier, + InputManager inputManager) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, - keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier); + keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier, inputManager + ); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mPostureController = postureController; mLockPatternUtils = lockPatternUtils; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java index 24f77d77dbe1..a5bb62c04d00 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java @@ -29,6 +29,7 @@ import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; +import android.hardware.input.InputManager; import android.telephony.PinResult; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; @@ -97,11 +98,13 @@ public class KeyguardSimPinViewController SelectedUserInteractor selectedUserInteractor, KeyguardKeyboardInteractor keyguardKeyboardInteractor, BouncerHapticPlayer bouncerHapticPlayer, - UserActivityNotifier userActivityNotifier) { + UserActivityNotifier userActivityNotifier, + InputManager inputManager) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, - keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier); + keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier, inputManager + ); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mTelephonyManager = telephonyManager; mSimImageView = mView.findViewById(R.id.keyguard_sim); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java index e17e8cc05f7e..adede3dc058d 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java @@ -25,6 +25,7 @@ import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; +import android.hardware.input.InputManager; import android.telephony.PinResult; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; @@ -95,11 +96,13 @@ public class KeyguardSimPukViewController SelectedUserInteractor selectedUserInteractor, KeyguardKeyboardInteractor keyguardKeyboardInteractor, BouncerHapticPlayer bouncerHapticPlayer, - UserActivityNotifier userActivityNotifier) { + UserActivityNotifier userActivityNotifier, + InputManager inputManager) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, - keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier); + keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier, inputManager + ); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mTelephonyManager = telephonyManager; mSimImageView = mView.findViewById(R.id.keyguard_sim); diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 4a8e4ed3f6f1..f72087efc03e 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -32,6 +32,7 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.fragments.FragmentService; +import com.android.systemui.media.NotificationMediaManager; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationModeController; @@ -42,7 +43,6 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; -import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.stack.AmbientState; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java index 08559f2eca8d..fb3bc620ee68 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java @@ -63,7 +63,7 @@ import com.android.systemui.accessibility.hearingaid.HearingDevicesListAdapter.H import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.bluetooth.qsdialog.ActiveHearingDeviceItemFactory; import com.android.systemui.bluetooth.qsdialog.AvailableHearingDeviceItemFactory; -import com.android.systemui.bluetooth.qsdialog.ConnectedDeviceItemFactory; +import com.android.systemui.bluetooth.qsdialog.ConnectedHearingDeviceItemFactory; import com.android.systemui.bluetooth.qsdialog.DeviceItem; import com.android.systemui.bluetooth.qsdialog.DeviceItemFactory; import com.android.systemui.bluetooth.qsdialog.DeviceItemType; @@ -71,6 +71,7 @@ import com.android.systemui.bluetooth.qsdialog.SavedHearingDeviceItemFactory; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.qs.shared.QSSettingsPackageRepository; import com.android.systemui.res.R; import com.android.systemui.statusbar.phone.SystemUIDialog; @@ -111,6 +112,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, private final HearingDevicesUiEventLogger mUiEventLogger; private final boolean mShowPairNewDevice; private final int mLaunchSourceId; + private final QSSettingsPackageRepository mQSSettingsPackageRepository; private SystemUIDialog mDialog; private HearingDevicesListAdapter mDeviceListAdapter; @@ -142,12 +144,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, private final List<DeviceItemFactory> mHearingDeviceItemFactoryList = List.of( new ActiveHearingDeviceItemFactory(), new AvailableHearingDeviceItemFactory(), - // TODO(b/331305850): setHearingAidInfo() for connected but not connect to profile - // hearing device only called from - // settings/bluetooth/DeviceListPreferenceFragment#handleLeScanResult, so we don't know - // it is connected but not yet connect to profile hearing device in systemui. - // Show all connected but not connect to profile bluetooth device for now. - new ConnectedDeviceItemFactory(), + new ConnectedHearingDeviceItemFactory(), new SavedHearingDeviceItemFactory() ); @@ -171,7 +168,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, @Main Executor mainExecutor, @Background Executor bgExecutor, AudioManager audioManager, - HearingDevicesUiEventLogger uiEventLogger) { + HearingDevicesUiEventLogger uiEventLogger, + QSSettingsPackageRepository qsSettingsPackageRepository) { mShowPairNewDevice = showPairNewDevice; mSystemUIDialogFactory = systemUIDialogFactory; mActivityStarter = activityStarter; @@ -183,6 +181,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, mProfileManager = localBluetoothManager.getProfileManager(); mUiEventLogger = uiEventLogger; mLaunchSourceId = launchSourceId; + mQSSettingsPackageRepository = qsSettingsPackageRepository; } @Override @@ -198,11 +197,11 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, public void onDeviceItemGearClicked(@NonNull DeviceItem deviceItem, @NonNull View view) { mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_GEAR_CLICK, mLaunchSourceId); dismissDialogIfExists(); - Intent intent = new Intent(ACTION_BLUETOOTH_DEVICE_DETAILS); Bundle bundle = new Bundle(); bundle.putString(KEY_BLUETOOTH_ADDRESS, deviceItem.getCachedBluetoothDevice().getAddress()); - intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + Intent intent = new Intent(ACTION_BLUETOOTH_DEVICE_DETAILS) + .setPackage(mQSSettingsPackageRepository.getSettingsPackageName()) + .putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle); mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0, mDialogTransitionAnimator.createActivityTransitionController(view)); } @@ -401,8 +400,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, pairButton.setOnClickListener(v -> { mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_PAIR, mLaunchSourceId); dismissDialogIfExists(); - final Intent intent = new Intent(Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + final Intent intent = new Intent(Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS) + .setPackage(mQSSettingsPackageRepository.getSettingsPackageName()); mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0, mDialogTransitionAnimator.createActivityTransitionController(dialog)); }); @@ -523,8 +522,9 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, com.android.internal.R.color.materialColorOnPrimaryContainer)); } text.setText(item.getToolName()); - Intent intent = item.getToolIntent(); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + Intent intent = item.getToolIntent() + .setPackage(mQSSettingsPackageRepository.getSettingsPackageName()); + view.setOnClickListener(v -> { final String name = intent.getComponent() != null ? intent.getComponent().flattenToString() diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt index 5863a9385234..7d8752ef7222 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt @@ -21,18 +21,14 @@ import com.android.systemui.plugins.qs.TileDetailsViewModel class BluetoothDetailsViewModel( private val onSettingsClick: () -> Unit, val detailsContentViewModel: BluetoothDetailsContentViewModel, -) : TileDetailsViewModel() { +) : TileDetailsViewModel { override fun clickOnSettingsButton() { onSettingsClick() } - override fun getTitle(): String { - // TODO: b/378513956 Update the placeholder text - return "Bluetooth" - } + // TODO: b/378513956 Update the placeholder text + override val title = "Bluetooth" - override fun getSubTitle(): String { - // TODO: b/378513956 Update the placeholder text - return "Tap to connect or disconnect a device" - } + // TODO: b/378513956 Update the placeholder text + override val subTitle = "Tap to connect or disconnect a device" } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt index 858cc00b86b9..208e498126c1 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt @@ -214,7 +214,7 @@ internal class AvailableHearingDeviceItemFactory : AvailableMediaDeviceItemFacto } } -internal class ConnectedDeviceItemFactory : DeviceItemFactory() { +internal open class ConnectedDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, @@ -238,6 +238,19 @@ internal class ConnectedDeviceItemFactory : DeviceItemFactory() { } } +internal class ConnectedHearingDeviceItemFactory : ConnectedDeviceItemFactory() { + override fun isFilterMatched( + context: Context, + cachedDevice: CachedBluetoothDevice, + isOngoingCall: Boolean, + audioSharingAvailable: Boolean, + ): Boolean { + return cachedDevice.isHearingDevice && + cachedDevice.bondState == BluetoothDevice.BOND_BONDED && + cachedDevice.device.isConnected + } +} + internal open class SavedDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( context: Context, @@ -274,7 +287,7 @@ internal class SavedHearingDeviceItemFactory : SavedDeviceItemFactory() { context, cachedDevice.getDevice(), ) && - cachedDevice.isHearingAidDevice && + cachedDevice.isHearingDevice && cachedDevice.bondState == BluetoothDevice.BOND_BONDED && !cachedDevice.isConnected } diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt index 52204b84346d..6aeb35b3b158 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt @@ -19,6 +19,7 @@ package com.android.systemui.brightness.ui.compose import android.content.Context import android.view.MotionEvent import androidx.annotation.VisibleForTesting +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.VectorConverter @@ -40,6 +41,7 @@ import androidx.compose.material3.SliderDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -323,7 +325,7 @@ private fun Modifier.sliderBackground(color: Color) = drawWithCache { fun BrightnessSliderContainer( viewModel: BrightnessSliderViewModel, modifier: Modifier = Modifier, - containerColor: Color = colorResource(R.color.shade_scrim_background_dark), + containerColors: ContainerColors, ) { val gamma = viewModel.currentBrightness.value if (gamma == BrightnessSliderViewModel.initialValue.value) { // Ignore initial negative value. @@ -344,6 +346,16 @@ fun BrightnessSliderContainer( DisposableEffect(Unit) { onDispose { viewModel.setIsDragging(false) } } + var dragging by remember { mutableStateOf(false) } + + // Use dragging instead of viewModel.showMirror so the color starts changing as soon as the + // dragging state changes. If not, we may be waiting for the background to finish fading in + // when stopping dragging + val containerColor by + animateColorAsState( + if (dragging) containerColors.mirrorColor else containerColors.idleColor + ) + Box( modifier = modifier @@ -360,10 +372,12 @@ fun BrightnessSliderContainer( onRestrictedClick = viewModel::showPolicyRestrictionDialog, onDrag = { viewModel.setIsDragging(true) + dragging = true coroutineScope.launch { viewModel.onDrag(Drag.Dragging(GammaBrightness(it))) } }, onStop = { viewModel.setIsDragging(false) + dragging = false coroutineScope.launch { viewModel.onDrag(Drag.Stopped(GammaBrightness(it))) } }, modifier = @@ -392,6 +406,15 @@ fun BrightnessSliderContainer( } } +data class ContainerColors(val idleColor: Color, val mirrorColor: Color) { + companion object { + fun singleColor(color: Color) = ContainerColors(color, color) + + val defaultContainerColor: Color + @Composable @ReadOnlyComposable get() = colorResource(R.color.shade_panel_fallback) + } +} + private object Dimensions { val SliderBackgroundFrameSize = DpSize(10.dp, 6.dp) val SliderBackgroundRoundedCorner = 24.dp diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt index 42f1b738ec20..6c3535a42a6e 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt @@ -27,17 +27,17 @@ import android.view.ViewConfiguration import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import androidx.core.view.accessibility.AccessibilityNodeInfoCompat +import com.android.systemui.Flags.doubleTapToSleep import com.android.systemui.log.TouchHandlingViewLogger import com.android.systemui.shade.TouchLogger -import kotlin.math.pow -import kotlin.math.sqrt import kotlinx.coroutines.DisposableHandle /** - * View designed to handle long-presses. + * View designed to handle long-presses and double taps. * - * The view will not handle any long pressed by default. To set it up, set up a listener and, when - * ready to start consuming long-presses, set [setLongPressHandlingEnabled] to `true`. + * The view will not handle any gestures by default. To set it up, set up a listener and, when ready + * to start consuming gestures, set the gesture's enable function ([setLongPressHandlingEnabled], + * [setDoublePressHandlingEnabled]) to `true`. */ class TouchHandlingView( context: Context, @@ -62,6 +62,9 @@ class TouchHandlingView( /** Notifies that the gesture was too short for a long press, it is actually a click. */ fun onSingleTapDetected(view: View, x: Int, y: Int) = Unit + + /** Notifies that a double tap has been detected by the given view. */ + fun onDoubleTapDetected(view: View) = Unit } var listener: Listener? = null @@ -70,6 +73,7 @@ class TouchHandlingView( private val interactionHandler: TouchHandlingViewInteractionHandler by lazy { TouchHandlingViewInteractionHandler( + context = context, postDelayed = { block, timeoutMs -> val dispatchToken = Any() @@ -84,6 +88,9 @@ class TouchHandlingView( onSingleTapDetected = { x, y -> listener?.onSingleTapDetected(this@TouchHandlingView, x = x, y = y) }, + onDoubleTapDetected = { + if (doubleTapToSleep()) listener?.onDoubleTapDetected(this@TouchHandlingView) + }, longPressDuration = longPressDuration, allowedTouchSlop = allowedTouchSlop, logger = logger, @@ -100,13 +107,17 @@ class TouchHandlingView( interactionHandler.isLongPressHandlingEnabled = isEnabled } + fun setDoublePressHandlingEnabled(isEnabled: Boolean) { + interactionHandler.isDoubleTapHandlingEnabled = isEnabled + } + override fun dispatchTouchEvent(event: MotionEvent): Boolean { return TouchLogger.logDispatchTouch("long_press", event, super.dispatchTouchEvent(event)) } @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(event: MotionEvent?): Boolean { - return interactionHandler.onTouchEvent(event?.toModel()) + override fun onTouchEvent(event: MotionEvent): Boolean { + return interactionHandler.onTouchEvent(event) } private fun setupAccessibilityDelegate() { @@ -154,33 +165,3 @@ class TouchHandlingView( } } } - -private fun MotionEvent.toModel(): TouchHandlingViewInteractionHandler.MotionEventModel { - return when (actionMasked) { - MotionEvent.ACTION_DOWN -> - TouchHandlingViewInteractionHandler.MotionEventModel.Down(x = x.toInt(), y = y.toInt()) - MotionEvent.ACTION_MOVE -> - TouchHandlingViewInteractionHandler.MotionEventModel.Move( - distanceMoved = distanceMoved() - ) - MotionEvent.ACTION_UP -> - TouchHandlingViewInteractionHandler.MotionEventModel.Up( - distanceMoved = distanceMoved(), - gestureDuration = gestureDuration(), - ) - MotionEvent.ACTION_CANCEL -> TouchHandlingViewInteractionHandler.MotionEventModel.Cancel - else -> TouchHandlingViewInteractionHandler.MotionEventModel.Other - } -} - -private fun MotionEvent.distanceMoved(): Float { - return if (historySize > 0) { - sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2)) - } else { - 0f - } -} - -private fun MotionEvent.gestureDuration(): Long { - return eventTime - downTime -} diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt index 5863fc644c8e..fe509d74edc0 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt @@ -17,12 +17,20 @@ package com.android.systemui.common.ui.view +import android.content.Context import android.graphics.Point +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.ViewConfiguration import com.android.systemui.log.TouchHandlingViewLogger +import kotlin.math.pow +import kotlin.math.sqrt +import kotlin.properties.Delegates import kotlinx.coroutines.DisposableHandle /** Encapsulates logic to handle complex touch interactions with a [TouchHandlingView]. */ class TouchHandlingViewInteractionHandler( + context: Context, /** * Callback to run the given [Runnable] with the given delay, returning a [DisposableHandle] * allowing the delayed runnable to be canceled before it is run. @@ -34,6 +42,8 @@ class TouchHandlingViewInteractionHandler( private val onLongPressDetected: (x: Int, y: Int) -> Unit, /** Callback reporting the a single tap gesture was detected at the given coordinates. */ private val onSingleTapDetected: (x: Int, y: Int) -> Unit, + /** Callback reporting that a double tap gesture was detected. */ + private val onDoubleTapDetected: () -> Unit, /** Time for the touch to be considered a long-press in ms */ var longPressDuration: () -> Long, /** @@ -58,48 +68,98 @@ class TouchHandlingViewInteractionHandler( } var isLongPressHandlingEnabled: Boolean = false + var isDoubleTapHandlingEnabled: Boolean = false var scheduledLongPressHandle: DisposableHandle? = null + private var doubleTapAwaitingUp: Boolean = false + private var lastDoubleTapDownEventTime: Long? = null + /** Record coordinate for last DOWN event for single tap */ val lastEventDownCoordinate = Point(-1, -1) - fun onTouchEvent(event: MotionEventModel?): Boolean { - if (!isLongPressHandlingEnabled) { - return false - } - return when (event) { - is MotionEventModel.Down -> { - scheduleLongPress(event.x, event.y) - lastEventDownCoordinate.x = event.x - lastEventDownCoordinate.y = event.y - true + private val gestureDetector = + GestureDetector( + context, + object : GestureDetector.SimpleOnGestureListener() { + override fun onDoubleTap(event: MotionEvent): Boolean { + if (isDoubleTapHandlingEnabled) { + doubleTapAwaitingUp = true + lastDoubleTapDownEventTime = event.eventTime + return true + } + return false + } + }, + ) + + fun onTouchEvent(event: MotionEvent): Boolean { + if (isDoubleTapHandlingEnabled) { + gestureDetector.onTouchEvent(event) + if (event.actionMasked == MotionEvent.ACTION_UP && doubleTapAwaitingUp) { + lastDoubleTapDownEventTime?.let { time -> + if ( + event.eventTime - time < ViewConfiguration.getDoubleTapTimeout() + ) { + cancelScheduledLongPress() + onDoubleTapDetected() + } + } + doubleTapAwaitingUp = false + } else if (event.actionMasked == MotionEvent.ACTION_CANCEL && doubleTapAwaitingUp) { + doubleTapAwaitingUp = false } - is MotionEventModel.Move -> { - if (event.distanceMoved > allowedTouchSlop) { - logger?.cancelingLongPressDueToTouchSlop(event.distanceMoved, allowedTouchSlop) + } + + if (isLongPressHandlingEnabled) { + val motionEventModel = event.toModel() + + return when (motionEventModel) { + is MotionEventModel.Down -> { + scheduleLongPress(motionEventModel.x, motionEventModel.y) + lastEventDownCoordinate.x = motionEventModel.x + lastEventDownCoordinate.y = motionEventModel.y + true + } + + is MotionEventModel.Move -> { + if (motionEventModel.distanceMoved > allowedTouchSlop) { + logger?.cancelingLongPressDueToTouchSlop( + motionEventModel.distanceMoved, + allowedTouchSlop, + ) + cancelScheduledLongPress() + } + false + } + + is MotionEventModel.Up -> { + logger?.onUpEvent( + motionEventModel.distanceMoved, + allowedTouchSlop, + motionEventModel.gestureDuration, + ) cancelScheduledLongPress() + if ( + motionEventModel.distanceMoved <= allowedTouchSlop && + motionEventModel.gestureDuration < longPressDuration() + ) { + logger?.dispatchingSingleTap() + dispatchSingleTap(lastEventDownCoordinate.x, lastEventDownCoordinate.y) + } + false } - false - } - is MotionEventModel.Up -> { - logger?.onUpEvent(event.distanceMoved, allowedTouchSlop, event.gestureDuration) - cancelScheduledLongPress() - if ( - event.distanceMoved <= allowedTouchSlop && - event.gestureDuration < longPressDuration() - ) { - logger?.dispatchingSingleTap() - dispatchSingleTap(lastEventDownCoordinate.x, lastEventDownCoordinate.y) + + is MotionEventModel.Cancel -> { + logger?.motionEventCancelled() + cancelScheduledLongPress() + false } - false - } - is MotionEventModel.Cancel -> { - logger?.motionEventCancelled() - cancelScheduledLongPress() - false + + else -> false } - else -> false } + + return false } private fun scheduleLongPress(x: Int, y: Int) { @@ -134,4 +194,30 @@ class TouchHandlingViewInteractionHandler( onSingleTapDetected(x, y) } + + private fun MotionEvent.toModel(): MotionEventModel { + return when (actionMasked) { + MotionEvent.ACTION_DOWN -> MotionEventModel.Down(x = x.toInt(), y = y.toInt()) + MotionEvent.ACTION_MOVE -> MotionEventModel.Move(distanceMoved = distanceMoved()) + MotionEvent.ACTION_UP -> + MotionEventModel.Up( + distanceMoved = distanceMoved(), + gestureDuration = gestureDuration(), + ) + MotionEvent.ACTION_CANCEL -> MotionEventModel.Cancel + else -> MotionEventModel.Other + } + } + + private fun MotionEvent.distanceMoved(): Float { + return if (historySize > 0) { + sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2)) + } else { + 0f + } + } + + private fun MotionEvent.gestureDuration(): Long { + return eventTime - downTime + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 140db7b7a0b7..62a98d7a48ea 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -377,12 +377,15 @@ constructor( MutableStateFlow(false) } - val inAllowedKeyguardState = - keyguardTransitionInteractor.startedKeyguardTransitionStep.map { - it.to == KeyguardState.LOCKSCREEN || it.to == KeyguardState.GLANCEABLE_HUB - } - - allOf(inAllowedDeviceState, inAllowedKeyguardState) + if (v2FlagEnabled()) { + val inAllowedKeyguardState = + keyguardTransitionInteractor.startedKeyguardTransitionStep.map { + it.to == KeyguardState.LOCKSCREEN || it.to == KeyguardState.GLANCEABLE_HUB + } + allOf(inAllowedDeviceState, inAllowedKeyguardState) + } else { + inAllowedDeviceState + } } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index a25faa3a7aec..11b42a8eafd6 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -36,6 +36,8 @@ import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerImpl; import com.android.systemui.doze.DozeHost; import com.android.systemui.education.dagger.ContextualEducationModule; +import com.android.systemui.topwindoweffects.dagger.SqueezeEffectRepositoryModule; +import com.android.systemui.topwindoweffects.dagger.TopLevelWindowEffectsModule; import com.android.systemui.emergency.EmergencyGestureModule; import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialModule; import com.android.systemui.keyboard.shortcut.ShortcutHelperModule; @@ -160,12 +162,14 @@ import javax.inject.Named; StatusBarPhoneModule.class, SystemActionsModule.class, ShadeModule.class, + SqueezeEffectRepositoryModule.class, StartCentralSurfacesModule.class, SceneContainerFrameworkModule.class, SysUICoroutinesModule.class, SysUIUnfoldStartableModule.class, UnfoldTransitionModule.Startables.class, ToastModule.class, + TopLevelWindowEffectsModule.class, TouchpadTutorialModule.class, VolumeModule.class, WallpaperModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt index 69da67e055fe..1e7bec257432 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricAuthInteractor.kt @@ -68,10 +68,4 @@ constructor( emptyFlow() } } - - /** Triggered if a face failure occurs regardless of the mode. */ - val faceFailure: Flow<FailedFaceAuthenticationStatus> = - deviceEntryFaceAuthInteractor.authenticationStatus.filterIsInstance< - FailedFaceAuthenticationStatus - >() } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt index 38e0503440f9..09936839c590 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt @@ -22,7 +22,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardBypassInteractor import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.util.kotlin.FlowDumperImpl @@ -49,8 +48,6 @@ class DeviceEntryHapticsInteractor constructor( biometricSettingsRepository: BiometricSettingsRepository, deviceEntryBiometricAuthInteractor: DeviceEntryBiometricAuthInteractor, - deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor, - keyguardBypassInteractor: KeyguardBypassInteractor, deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, deviceEntrySourceInteractor: DeviceEntrySourceInteractor, fingerprintPropertyRepository: FingerprintPropertyRepository, @@ -83,7 +80,12 @@ constructor( emit(recentPowerButtonPressThresholdMs * -1L - 1L) } - private val playHapticsOnDeviceEntry: Flow<Boolean> = + /** + * Indicates when success haptics should play when the device is entered. This always occurs on + * successful fingerprint authentications. It also occurs on successful face authentication but + * only if the lockscreen is bypassed. + */ + val playSuccessHapticOnDeviceEntry: Flow<Unit> = deviceEntrySourceInteractor.deviceEntryFromBiometricSource .sample( combine( @@ -93,29 +95,17 @@ constructor( ::Triple, ) ) - .map { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) -> + .filter { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) -> val sideFpsAllowsHaptic = !powerButtonDown && systemClock.uptimeMillis() - lastPowerButtonWakeup > recentPowerButtonPressThresholdMs val allowHaptic = !sideFpsEnrolled || sideFpsAllowsHaptic if (!allowHaptic) { - logger.d( - "Skip success entry haptic from power button. Recent power button press or button is down." - ) + logger.d("Skip success haptic. Recent power button press or button is down.") } allowHaptic } - - private val playHapticsOnFaceAuthSuccessAndBypassDisabled: Flow<Boolean> = - deviceEntryFaceAuthInteractor.isAuthenticated - .filter { it } - .sample(keyguardBypassInteractor.isBypassAvailable) - .map { !it } - - val playSuccessHaptic: Flow<Unit> = - merge(playHapticsOnDeviceEntry, playHapticsOnFaceAuthSuccessAndBypassDisabled) - .filter { it } // map to Unit .map {} .dumpWhileCollecting("playSuccessHaptic") @@ -123,7 +113,7 @@ constructor( private val playErrorHapticForBiometricFailure: Flow<Unit> = merge( deviceEntryFingerprintAuthInteractor.fingerprintFailure, - deviceEntryBiometricAuthInteractor.faceFailure, + deviceEntryBiometricAuthInteractor.faceOnlyFaceFailure, ) // map to Unit .map {} diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt index 083191c8ecde..a56710ee3772 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt @@ -32,9 +32,6 @@ class FakePerDisplayRepository<T> : PerDisplayRepository<T> { return instances[displayId] } - override val displayIds: Set<Int> - get() = instances.keys - override val debugName: String get() = "FakePerDisplayRepository" } diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt index d27e33e53dbb..d1d013542fbf 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt @@ -86,9 +86,6 @@ interface PerDisplayRepository<T> { /** Gets the cached instance or create a new one for a given display. */ operator fun get(displayId: Int): T? - /** List of display ids for which this repository has an instance. */ - val displayIds: Set<Int> - /** Debug name for this repository, mainly for tracing and logging. */ val debugName: String } @@ -122,9 +119,6 @@ constructor( backgroundApplicationScope.launch("$debugName#start") { start() } } - override val displayIds: Set<Int> - get() = perDisplayInstances.keys - private suspend fun start() { dumpManager.registerNormalDumpable("PerDisplayRepository-${debugName}", this) displayRepository.displayIds.collectLatest { displayIds -> @@ -199,8 +193,6 @@ class DefaultDisplayOnlyInstanceRepositoryImpl<T>( private val lazyDefaultDisplayInstance by lazy { instanceProvider.createInstance(Display.DEFAULT_DISPLAY) } - override val displayIds: Set<Int> = setOf(Display.DEFAULT_DISPLAY) - override fun get(displayId: Int): T? = lazyDefaultDisplayInstance } @@ -214,7 +206,5 @@ class DefaultDisplayOnlyInstanceRepositoryImpl<T>( */ class SingleInstanceRepositoryImpl<T>(override val debugName: String, private val instance: T) : PerDisplayRepository<T> { - override val displayIds: Set<Int> = setOf(Display.DEFAULT_DISPLAY) - override fun get(displayId: Int): T? = instance } diff --git a/packages/SystemUI/src/com/android/systemui/keyevent/data/repository/KeyEventRepository.kt b/packages/SystemUI/src/com/android/systemui/keyevent/data/repository/KeyEventRepository.kt index 9da9a7328659..32257df40d02 100644 --- a/packages/SystemUI/src/com/android/systemui/keyevent/data/repository/KeyEventRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyevent/data/repository/KeyEventRepository.kt @@ -18,7 +18,7 @@ package com.android.systemui.keyevent.data.repository import android.view.KeyEvent import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.CommandQueue import javax.inject.Inject @@ -29,6 +29,9 @@ import kotlinx.coroutines.flow.Flow interface KeyEventRepository { /** Observable for whether the power button key is pressed/down or not. */ val isPowerButtonDown: Flow<Boolean> + + /** Observable for when the power button is being pressed but till the duration of long press */ + val isPowerButtonLongPressed: Flow<Boolean> } @SysUISingleton @@ -51,6 +54,21 @@ constructor( awaitClose { commandQueue.removeCallback(callback) } } + override val isPowerButtonLongPressed: Flow<Boolean> = conflatedCallbackFlow { + val callback = + object : CommandQueue.Callbacks { + override fun handleSystemKey(event: KeyEvent) { + if (event.keyCode == KeyEvent.KEYCODE_POWER) { + trySendWithFailureLogging(event.action == KeyEvent.ACTION_DOWN + && event.isLongPress, TAG, "updated isPowerButtonLongPressed") + } + } + } + trySendWithFailureLogging(false, TAG, "init isPowerButtonLongPressed") + commandQueue.addCallback(callback) + awaitClose { commandQueue.removeCallback(callback) } + } + companion object { private const val TAG = "KeyEventRepositoryImpl" } diff --git a/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt index 9949fa589cd5..ec9bbfb1bc77 100644 --- a/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt @@ -32,4 +32,5 @@ constructor( repository: KeyEventRepository, ) { val isPowerButtonDown = repository.isPowerButtonDown + val isPowerButtonLongPressed = repository.isPowerButtonLongPressed } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java index 3b85b571023f..9ade5036b9a8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java @@ -51,10 +51,10 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.SystemUIAppComponentFactoryBase; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.media.NotificationMediaManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; -import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.KeyguardBypassController; diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt index 705eaa22aa9a..55534c4f1444 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt @@ -20,11 +20,14 @@ package com.android.systemui.keyguard.domain.interactor import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.os.PowerManager +import android.provider.Settings import android.view.accessibility.AccessibilityManager import androidx.annotation.VisibleForTesting import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger +import com.android.systemui.Flags.doubleTapToSleep import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -36,10 +39,13 @@ import com.android.systemui.res.R import com.android.systemui.shade.PulsingGestureListener import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper +import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepository +import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -66,10 +72,13 @@ constructor( private val accessibilityManager: AccessibilityManagerWrapper, private val pulsingGestureListener: PulsingGestureListener, private val faceAuthInteractor: DeviceEntryFaceAuthInteractor, + private val secureSettingsRepository: UserAwareSecureSettingsRepository, + private val powerManager: PowerManager, + private val systemClock: SystemClock, ) { /** Whether the long-press handling feature should be enabled. */ val isLongPressHandlingEnabled: StateFlow<Boolean> = - if (isFeatureEnabled()) { + if (isLongPressFeatureEnabled()) { combine( transitionInteractor.isFinishedIn(KeyguardState.LOCKSCREEN), repository.isQuickSettingsVisible, @@ -85,6 +94,30 @@ constructor( initialValue = false, ) + /** Whether the double tap handling handling feature should be enabled. */ + val isDoubleTapHandlingEnabled: StateFlow<Boolean> = + if (isDoubleTapFeatureEnabled()) { + combine( + transitionInteractor.transitionValue(KeyguardState.LOCKSCREEN), + repository.isQuickSettingsVisible, + isDoubleTapSettingEnabled(), + ) { + isFullyTransitionedToLockScreen, + isQuickSettingsVisible, + isDoubleTapSettingEnabled -> + isFullyTransitionedToLockScreen == 1f && + !isQuickSettingsVisible && + isDoubleTapSettingEnabled + } + } else { + flowOf(false) + } + .stateIn( + scope = scope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) + private val _isMenuVisible = MutableStateFlow(false) /** Model for whether the menu should be shown. */ val isMenuVisible: StateFlow<Boolean> = @@ -116,7 +149,7 @@ constructor( private var delayedHideMenuJob: Job? = null init { - if (isFeatureEnabled()) { + if (isLongPressFeatureEnabled()) { broadcastDispatcher .broadcastFlow(IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) .onEach { hideMenu() } @@ -175,17 +208,30 @@ constructor( /** Notifies that the lockscreen has been double clicked. */ fun onDoubleClick() { - pulsingGestureListener.onDoubleTapEvent() + if (isDoubleTapHandlingEnabled.value) { + powerManager.goToSleep(systemClock.uptimeMillis()) + } else { + pulsingGestureListener.onDoubleTapEvent() + } + } + + private fun isDoubleTapSettingEnabled(): Flow<Boolean> { + return secureSettingsRepository.boolSetting(Settings.Secure.DOUBLE_TAP_TO_SLEEP) } private fun showSettings() { _shouldOpenSettings.value = true } - private fun isFeatureEnabled(): Boolean { + private fun isLongPressFeatureEnabled(): Boolean { return context.resources.getBoolean(R.bool.long_press_keyguard_customize_lockscreen_enabled) } + private fun isDoubleTapFeatureEnabled(): Boolean { + return doubleTapToSleep() && + context.resources.getBoolean(com.android.internal.R.bool.config_supportDoubleTapSleep) + } + /** Updates application state to ask to show the menu. */ private fun showMenu() { _isMenuVisible.value = true diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt index 17e14c3e83da..70a827d5e45b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt @@ -48,7 +48,7 @@ object DeviceEntryIconViewBinder { /** * Updates UI for: * - device entry containing view (parent view for the below views) - * - long-press handling view (transparent, no UI) + * - touch handling view (transparent, no UI) * - foreground icon view (lock/unlock/fingerprint) * - background view (optional) */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt index 741b149f29da..92b9da6790d1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt @@ -31,6 +31,37 @@ import com.android.systemui.plugins.clocks.ClockPreviewConfig object KeyguardPreviewSmartspaceViewBinder { @JvmStatic + fun bind(parentView: View, viewModel: KeyguardPreviewSmartspaceViewModel) { + if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { + val largeDateView = + parentView.findViewById<View>( + com.android.systemui.shared.R.id.date_smartspace_view_large + ) + val smallDateView = + parentView.findViewById<View>(com.android.systemui.shared.R.id.date_smartspace_view) + parentView.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch("$TAG#viewModel.selectedClockSize") { + viewModel.previewingClockSize.collect { + when (it) { + ClockSizeSetting.DYNAMIC -> { + smallDateView?.visibility = View.GONE + largeDateView?.visibility = View.VISIBLE + } + + ClockSizeSetting.SMALL -> { + smallDateView?.visibility = View.VISIBLE + largeDateView?.visibility = View.GONE + } + } + } + } + } + } + } + } + + @JvmStatic fun bind( smartspace: View, viewModel: KeyguardPreviewSmartspaceViewModel, @@ -44,6 +75,7 @@ object KeyguardPreviewSmartspaceViewBinder { when (it) { ClockSizeSetting.DYNAMIC -> viewModel.getLargeClockSmartspaceTopPadding(clockPreviewConfig) + ClockSizeSetting.SMALL -> viewModel.getSmallClockSmartspaceTopPadding(clockPreviewConfig) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 45801ba3517a..aeb327035c79 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -333,7 +333,7 @@ object KeyguardRootViewBinder { if (deviceEntryHapticsInteractor != null && vibratorHelper != null) { launch { - deviceEntryHapticsInteractor.playSuccessHaptic.collect { + deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry.collect { if (msdlFeedback()) { msdlPlayer?.playToken( MSDLToken.UNLOCK, @@ -474,7 +474,7 @@ object KeyguardRootViewBinder { val transition = blueprintViewModel.currentTransition.value val shouldAnimate = transition != null && transition.config.type.animateNotifChanges if (prevTransition == transition && shouldAnimate) { - logger.w("Skipping; layout during transition") + logger.w("Skipping onNotificationContainerBoundsChanged during transition") return } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt index e81d5354ec6e..5ef2d6fd3256 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt @@ -29,6 +29,7 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.plugins.clocks.VRectF import com.android.systemui.res.R import com.android.systemui.shared.R as sharedR import kotlinx.coroutines.DisposableHandle @@ -135,7 +136,7 @@ object KeyguardSmartspaceViewBinder { } } - if (clockBounds == null) return@collect + if (clockBounds == VRectF.ZERO) return@collect if (isLargeClock) { val largeDateHeight = keyguardRootView diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt index 195413a80f4b..485e1ce5b2f7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt @@ -75,6 +75,13 @@ object KeyguardTouchViewBinder { onSingleTap(x, y) } + + override fun onDoubleTapDetected(view: View) { + if (falsingManager.isFalseDoubleTap()) { + return + } + viewModel.onDoubleClick() + } } view.repeatWhenAttached { @@ -90,9 +97,20 @@ object KeyguardTouchViewBinder { } } } + launch("$TAG#viewModel.isDoubleTapHandlingEnabled") { + viewModel.isDoubleTapHandlingEnabled.collect { isEnabled -> + view.setDoublePressHandlingEnabled(isEnabled) + view.contentDescription = + if (isEnabled) { + view.resources.getString(R.string.accessibility_desc_lock_screen) + } else { + null + } + } + } } } } - private const val TAG = "KeyguardLongPressViewBinder" + private const val TAG = "KeyguardTouchViewBinder" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 242926b3e1d1..d749e3c11378 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -68,6 +68,7 @@ import com.android.systemui.monet.ColorScheme import com.android.systemui.monet.Style import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockPreviewConfig +import com.android.systemui.plugins.clocks.ContextExt.getId import com.android.systemui.plugins.clocks.ThemeConfig import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.res.R @@ -126,6 +127,7 @@ constructor( private val displayId = bundle.getInt(KEY_DISPLAY_ID, DEFAULT_DISPLAY) private val display: Display? = displayManager.getDisplay(displayId) + /** * Returns a key that should make the KeyguardPreviewRenderer unique and if two of them have the * same key they will be treated as the same KeyguardPreviewRenderer. Primary this is used to @@ -144,6 +146,8 @@ constructor( get() = checkNotNull(host.surfacePackage) private var smartSpaceView: View? = null + private var largeDateView: View? = null + private var smallDateView: View? = null private val disposables = DisposableHandles() private var isDestroyed = false @@ -181,7 +185,7 @@ constructor( ContextThemeWrapper(context.createDisplayContext(it), context.getTheme()) } ?: context - val rootView = FrameLayout(previewContext) + val rootView = ConstraintLayout(previewContext) setupKeyguardRootView(previewContext, rootView) @@ -252,6 +256,24 @@ constructor( fun onClockSizeSelected(clockSize: ClockSizeSetting) { smartspaceViewModel.setOverrideClockSize(clockSize) + if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { + when (clockSize) { + ClockSizeSetting.DYNAMIC -> { + largeDateView?.post { + smallDateView?.visibility = View.GONE + largeDateView?.visibility = View.VISIBLE + } + } + + ClockSizeSetting.SMALL -> { + largeDateView?.post { + smallDateView?.visibility = View.VISIBLE + largeDateView?.visibility = View.GONE + } + } + } + smartSpaceView?.post { smartSpaceView?.visibility = View.GONE } + } } fun destroy() { @@ -280,7 +302,7 @@ constructor( * * The end padding is as follows: Below clock padding end */ - private fun setUpSmartspace(previewContext: Context, parentView: ViewGroup) { + private fun setUpSmartspace(previewContext: Context, parentView: ConstraintLayout) { if ( !lockscreenSmartspaceController.isEnabled || !lockscreenSmartspaceController.isDateWeatherDecoupled @@ -292,40 +314,90 @@ constructor( parentView.removeView(smartSpaceView) } - smartSpaceView = - lockscreenSmartspaceController.buildAndConnectDateView( - parent = parentView, - isLargeClock = false, - ) + if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { + val cs = ConstraintSet() + cs.clone(parentView) + cs.apply { + val largeClockViewId = previewContext.getId("lockscreen_clock_view_large") + val smallClockViewId = previewContext.getId("lockscreen_clock_view") + largeDateView = + lockscreenSmartspaceController + .buildAndConnectDateView(parentView, true) + ?.also { view -> + constrainWidth(view.id, ConstraintSet.WRAP_CONTENT) + constrainHeight(view.id, ConstraintSet.WRAP_CONTENT) + connect(view.id, START, largeClockViewId, START) + connect(view.id, ConstraintSet.END, largeClockViewId, ConstraintSet.END) + connect( + view.id, + TOP, + largeClockViewId, + ConstraintSet.BOTTOM, + smartspaceViewModel.getDateWeatherEndPadding(previewContext), + ) + } + smallDateView = + lockscreenSmartspaceController + .buildAndConnectDateView(parentView, false) + ?.also { view -> + constrainWidth(view.id, ConstraintSet.WRAP_CONTENT) + constrainHeight(view.id, ConstraintSet.WRAP_CONTENT) + connect( + view.id, + START, + smallClockViewId, + ConstraintSet.END, + context.resources.getDimensionPixelSize( + R.dimen.smartspace_padding_horizontal + ), + ) + connect(view.id, TOP, smallClockViewId, TOP) + connect( + view.id, + ConstraintSet.BOTTOM, + smallClockViewId, + ConstraintSet.BOTTOM, + ) + } + parentView.addView(largeDateView) + parentView.addView(smallDateView) + } + cs.applyTo(parentView) + } else { + smartSpaceView = + lockscreenSmartspaceController.buildAndConnectDateView( + parent = parentView, + isLargeClock = false, + ) - val topPadding: Int = - smartspaceViewModel.getLargeClockSmartspaceTopPadding( - ClockPreviewConfig( - previewContext, - getPreviewShadeLayoutWide(display!!), - SceneContainerFlag.isEnabled, + val topPadding: Int = + smartspaceViewModel.getLargeClockSmartspaceTopPadding( + ClockPreviewConfig( + previewContext, + getPreviewShadeLayoutWide(display!!), + SceneContainerFlag.isEnabled, + ) ) - ) - val startPadding: Int = smartspaceViewModel.getDateWeatherStartPadding(previewContext) - val endPadding: Int = smartspaceViewModel.getDateWeatherEndPadding(previewContext) - - smartSpaceView?.let { - it.setPaddingRelative(startPadding, topPadding, endPadding, 0) - it.isClickable = false - it.isInvisible = true - parentView.addView( - it, - FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.WRAP_CONTENT, - ), - ) + val startPadding: Int = smartspaceViewModel.getDateWeatherStartPadding(previewContext) + val endPadding: Int = smartspaceViewModel.getDateWeatherEndPadding(previewContext) + + smartSpaceView?.let { + it.setPaddingRelative(startPadding, topPadding, endPadding, 0) + it.isClickable = false + it.isInvisible = true + parentView.addView( + it, + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + ), + ) + } + smartSpaceView?.alpha = if (shouldHighlightSelectedAffordance) DIM_ALPHA else 1.0f } - - smartSpaceView?.alpha = if (shouldHighlightSelectedAffordance) DIM_ALPHA else 1.0f } - private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) { + private fun setupKeyguardRootView(previewContext: Context, rootView: ConstraintLayout) { val keyguardRootView = KeyguardRootView(previewContext, null) rootView.addView( keyguardRootView, @@ -341,6 +413,13 @@ constructor( if (!shouldHideClock) { setUpClock(previewContext, rootView) + if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { + setUpSmartspace(previewContext, keyguardRootView) + KeyguardPreviewSmartspaceViewBinder.bind( + keyguardRootView, + smartspaceViewModel, + ) + } KeyguardPreviewClockViewBinder.bind( keyguardRootView, clockViewModel, @@ -354,19 +433,22 @@ constructor( ) } - setUpSmartspace(previewContext, rootView) - - smartSpaceView?.let { - KeyguardPreviewSmartspaceViewBinder.bind( - it, - smartspaceViewModel, - clockPreviewConfig = - ClockPreviewConfig( - previewContext, - getPreviewShadeLayoutWide(display!!), - SceneContainerFlag.isEnabled, - ), - ) + if (!com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { + setUpSmartspace(previewContext, keyguardRootView) + smartSpaceView?.let { + KeyguardPreviewSmartspaceViewBinder.bind( + it, + smartspaceViewModel, + clockPreviewConfig = + ClockPreviewConfig( + previewContext, + getPreviewShadeLayoutWide(display!!), + SceneContainerFlag.isEnabled, + lockId = null, + udfpsTop = null, + ), + ) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt index c4a7e1ed95e1..55fac3c7644b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt @@ -36,6 +36,7 @@ import com.android.systemui.keyguard.ui.viewmodel.DreamingToGlanceableHubTransit import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToAodTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToDreamingTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GoneToDozingTransitionViewModel @@ -272,6 +273,12 @@ abstract class DeviceEntryIconTransitionModule { @Binds @IntoSet + abstract fun glanceableHubToLockscreen( + impl: GlanceableHubToLockscreenTransitionViewModel + ): DeviceEntryIconTransition + + @Binds + @IntoSet abstract fun occludedToGlanceableHub( impl: OccludedToGlanceableHubTransitionViewModel ): DeviceEntryIconTransition 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 0ccb24a9858a..20fc88446ce3 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 @@ -61,6 +61,7 @@ constructor( primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel, lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel, glanceableHubToAodTransitionViewModel: GlanceableHubToAodTransitionViewModel, + glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel, ) { val color: Flow<Int> = deviceEntryIconViewModel.useBackgroundProtection.flatMapLatest { useBackground -> @@ -108,6 +109,7 @@ constructor( .deviceEntryBackgroundViewAlpha, lockscreenToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha, glanceableHubToAodTransitionViewModel.deviceEntryBackgroundViewAlpha, + glanceableHubToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha, ) .merge() .onStart { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt index b4b4c82c59b9..bcbe66642d11 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt @@ -27,6 +27,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import com.android.systemui.keyguard.ui.StateToValue +import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import com.android.systemui.keyguard.ui.transitions.GlanceableHubTransition import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes @@ -49,7 +50,7 @@ constructor( @ShadeDisplayAware configurationInteractor: ConfigurationInteractor, animationFlow: KeyguardTransitionAnimationFlow, private val blurFactory: GlanceableHubBlurComponent.Factory, -) : GlanceableHubTransition { +) : GlanceableHubTransition, DeviceEntryIconTransition { private val transitionAnimation = animationFlow .setup( @@ -102,4 +103,8 @@ constructor( val notificationTranslationX: Flow<Float> = keyguardTranslationX.map { it.value }.filterNotNull() + + val deviceEntryBackgroundViewAlpha: Flow<Float> = keyguardAlpha + + override val deviceEntryParentViewAlpha: Flow<Float> = keyguardAlpha } 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 d4676bc9c146..6d8a943d3e28 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 @@ -132,6 +132,8 @@ constructor( private val occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel, private val occludedToDozingTransitionViewModel: OccludedToDozingTransitionViewModel, private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel, + private val occludedToPrimaryBouncerTransitionViewModel: + OccludedToPrimaryBouncerTransitionViewModel, private val offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel, private val primaryBouncerToAodTransitionViewModel: PrimaryBouncerToAodTransitionViewModel, private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel, @@ -288,6 +290,7 @@ constructor( occludedToAodTransitionViewModel.lockscreenAlpha, occludedToDozingTransitionViewModel.lockscreenAlpha, occludedToLockscreenTransitionViewModel.lockscreenAlpha, + occludedToPrimaryBouncerTransitionViewModel.lockscreenAlpha, offToLockscreenTransitionViewModel.lockscreenAlpha, primaryBouncerToAodTransitionViewModel.lockscreenAlpha, primaryBouncerToGoneTransitionViewModel.lockscreenAlpha, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt index 1d2edc6d406b..d4e7af48adfe 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt @@ -33,6 +33,9 @@ constructor( /** Whether the long-press handling feature should be enabled. */ val isLongPressHandlingEnabled: Flow<Boolean> = interactor.isLongPressHandlingEnabled + /** Whether the double tap handling feature should be enabled. */ + val isDoubleTapHandlingEnabled: Flow<Boolean> = interactor.isDoubleTapHandlingEnabled + /** Notifies that the user has long-pressed on the lock screen. * * @param isA11yAction: Whether the action was performed as an a11y action diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt index f14a5a282e88..67c3071db390 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt @@ -45,6 +45,12 @@ constructor( ) .setupWithoutSceneContainer(edge = Edge.create(OCCLUDED, PRIMARY_BOUNCER)) + /** + * Reasserts that lockscreen content should not be visible. It is possible the keyguard alpha is + * set to 1f if coming from an expanded shade that collapsed to launch an occluding activity. + */ + val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f) + override val windowBlurRadius: Flow<Float> = shadeDependentFlows.transitionFlow( flowWhenShadeIsExpanded = diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index faa6c52162ce..a85b9b04c1ce 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -327,7 +327,8 @@ public class LogModule { @SysUISingleton @KeyguardBlueprintLog public static LogBuffer provideKeyguardBlueprintLog(LogBufferFactory factory) { - return factory.create("KeyguardBlueprintLog", 100); + // TODO(b/389987229): Reduce back to 100 + return factory.create("KeyguardBlueprintLog", 1000); } /** @@ -337,7 +338,8 @@ public class LogModule { @SysUISingleton @KeyguardClockLog public static LogBuffer provideKeyguardClockLog(LogBufferFactory factory) { - return factory.create("KeyguardClockLog", 100); + // TODO(b/389987229): Reduce back to 100 + return factory.create("KeyguardClockLog", 1000); } /** @@ -347,7 +349,8 @@ public class LogModule { @SysUISingleton @KeyguardSmallClockLog public static LogBuffer provideKeyguardSmallClockLog(LogBufferFactory factory) { - return factory.create("KeyguardSmallClockLog", 100); + // TODO(b/389987229): Reduce back to 100 + return factory.create("KeyguardSmallClockLog", 1000); } /** @@ -357,7 +360,8 @@ public class LogModule { @SysUISingleton @KeyguardLargeClockLog public static LogBuffer provideKeyguardLargeClockLog(LogBufferFactory factory) { - return factory.create("KeyguardLargeClockLog", 100); + // TODO(b/389987229): Reduce back to 100 + return factory.create("KeyguardLargeClockLog", 1000); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/media/NotificationMediaManager.java index 18f4b4ab9b88..db4c7a5b2ee5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/media/NotificationMediaManager.java @@ -11,9 +11,9 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License + * limitations under the License. */ -package com.android.systemui.statusbar; +package com.android.systemui.media; import static com.android.systemui.Flags.mediaControlsUserInitiatedDeleteintent; @@ -40,6 +40,8 @@ import com.android.systemui.dump.DumpManager; 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; +import com.android.systemui.statusbar.NotificationPresenter; +import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.dagger.CentralSurfacesModule; import com.android.systemui.statusbar.notification.collection.NotifCollection; import com.android.systemui.statusbar.notification.collection.NotifPipeline; diff --git a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java index e7c2a454e16c..b71d8c995e51 100644 --- a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java +++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java @@ -17,6 +17,7 @@ package com.android.systemui.media; import android.annotation.Nullable; +import android.content.ContentProvider; import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; @@ -126,6 +127,8 @@ public class RingtonePlayer implements CoreStartable { Log.d(TAG, "play(token=" + token + ", uri=" + uri + ", uid=" + Binder.getCallingUid() + ")"); } + enforceUriUserId(uri); + Client client; synchronized (mClients) { client = mClients.get(token); @@ -207,6 +210,7 @@ public class RingtonePlayer implements CoreStartable { @Override public String getTitle(Uri uri) { + enforceUriUserId(uri); final UserHandle user = Binder.getCallingUserHandle(); return Ringtone.getTitle(getContextForUser(user), uri, false /*followSettingsUri*/, false /*allowRemote*/); @@ -214,6 +218,7 @@ public class RingtonePlayer implements CoreStartable { @Override public ParcelFileDescriptor openRingtone(Uri uri) { + enforceUriUserId(uri); final UserHandle user = Binder.getCallingUserHandle(); final ContentResolver resolver = getContextForUser(user).getContentResolver(); @@ -241,6 +246,28 @@ public class RingtonePlayer implements CoreStartable { } }; + /** + * Must be called from the Binder calling thread. + * Ensures caller is from the same userId as the content they're trying to access. + * @param uri the URI to check + * @throws SecurityException when in a non-system call and userId in uri differs from the + * caller's userId + */ + private void enforceUriUserId(Uri uri) throws SecurityException { + final int uriUserId = ContentProvider.getUserIdFromUri(uri, UserHandle.myUserId()); + // for a non-system call, verify the URI to play belongs to the same user as the caller + if (UserHandle.isApp(Binder.getCallingUid()) && (UserHandle.myUserId() != uriUserId)) { + final String errorMessage = "Illegal access to uri=" + uri + + " content associated with user=" + uriUserId + + ", current userID: " + UserHandle.myUserId(); + if (android.media.audio.Flags.ringtoneUserUriCheck()) { + throw new SecurityException(errorMessage); + } else { + Log.e(TAG, errorMessage, new Exception()); + } + } + } + private Context getContextForUser(UserHandle user) { try { return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user); 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 46cf0a63e93d..309b6751176c 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 @@ -66,6 +66,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.media.NotificationMediaManager.isPlayingState import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification import com.android.systemui.media.controls.domain.resume.MediaResumeListener import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser @@ -83,7 +84,6 @@ import com.android.systemui.media.controls.util.MediaDataUtils import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.res.R -import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.statusbar.notification.row.HybridGroupManager import com.android.systemui.util.Assert import com.android.systemui.util.Utils 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 5fef81f4596a..da462e6713c6 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 @@ -30,6 +30,8 @@ import android.service.notification.StatusBarNotification import android.util.Log import androidx.media.utils.MediaConstants import com.android.systemui.Flags +import com.android.systemui.media.NotificationMediaManager.isConnectingState +import com.android.systemui.media.NotificationMediaManager.isPlayingState import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_COMPACT_ACTIONS import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_NOTIFICATION_ACTIONS import com.android.systemui.media.controls.shared.MediaControlDrawables @@ -38,8 +40,6 @@ import com.android.systemui.media.controls.shared.model.MediaButton import com.android.systemui.media.controls.shared.model.MediaNotificationAction import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R -import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState -import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.util.kotlin.logI private const val TAG = "MediaActions" diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt index 8bb7303a8386..dbd2250a75b0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt @@ -51,6 +51,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.graphics.ImageLoader +import com.android.systemui.media.NotificationMediaManager.isPlayingState import com.android.systemui.media.controls.shared.model.MediaAction import com.android.systemui.media.controls.shared.model.MediaButton import com.android.systemui.media.controls.shared.model.MediaData @@ -60,7 +61,6 @@ import com.android.systemui.media.controls.util.MediaControllerFactory import com.android.systemui.media.controls.util.MediaDataUtils import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.res.R -import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.statusbar.notification.row.HybridGroupManager import com.android.systemui.util.kotlin.logD import java.util.concurrent.ConcurrentHashMap 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 fe852ce7979f..94df4b353c94 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 @@ -66,6 +66,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.media.NotificationMediaManager.isPlayingState import com.android.systemui.media.controls.data.repository.MediaDataRepository import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor @@ -85,7 +86,6 @@ import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag -import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.statusbar.notification.row.HybridGroupManager import com.android.systemui.util.Assert import com.android.systemui.util.Utils diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt index 49b53c2d78ae..dfb32e66dae5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt @@ -37,6 +37,7 @@ import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice import com.android.settingslib.media.PhoneMediaDevice import com.android.settingslib.media.flags.Flags +import com.android.systemui.Flags.mediaControlsDeviceManagerBackgroundExecution import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.controls.shared.MediaControlDrawables @@ -94,8 +95,39 @@ constructor( data: MediaData, immediately: Boolean, receivedSmartspaceCardLatency: Int, - isSsReactivated: Boolean + isSsReactivated: Boolean, ) { + if (mediaControlsDeviceManagerBackgroundExecution()) { + bgExecutor.execute { onMediaLoaded(key, oldKey, data) } + } else { + onMediaLoaded(key, oldKey, data) + } + } + + override fun onMediaDataRemoved(key: String, userInitiated: Boolean) { + if (mediaControlsDeviceManagerBackgroundExecution()) { + bgExecutor.execute { onMediaRemoved(key, userInitiated) } + } else { + onMediaRemoved(key, userInitiated) + } + } + + fun dump(pw: PrintWriter) { + with(pw) { + println("MediaDeviceManager state:") + entries.forEach { (key, entry) -> + println(" key=$key") + entry.dump(pw) + } + } + } + + @MainThread + private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) { + listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) } + } + + private fun onMediaLoaded(key: String, oldKey: String?, data: MediaData) { if (oldKey != null && oldKey != key) { val oldEntry = entries.remove(oldKey) oldEntry?.stop() @@ -104,9 +136,13 @@ constructor( if (entry == null || entry.token != data.token) { entry?.stop() if (data.device != null) { - // If we were already provided device info (e.g. from RCN), keep that and don't - // listen for updates, but process once to push updates to listeners - processDevice(key, oldKey, data.device) + // If we were already provided device info (e.g. from RCN), keep that and + // don't listen for updates, but process once to push updates to listeners + if (mediaControlsDeviceManagerBackgroundExecution()) { + fgExecutor.execute { processDevice(key, oldKey, data.device) } + } else { + processDevice(key, oldKey, data.device) + } return } val controller = data.token?.let { controllerFactory.create(it) } @@ -120,27 +156,18 @@ constructor( } } - override fun onMediaDataRemoved(key: String, userInitiated: Boolean) { + private fun onMediaRemoved(key: String, userInitiated: Boolean) { val token = entries.remove(key) token?.stop() - token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } } - } - - fun dump(pw: PrintWriter) { - with(pw) { - println("MediaDeviceManager state:") - entries.forEach { (key, entry) -> - println(" key=$key") - entry.dump(pw) + if (mediaControlsDeviceManagerBackgroundExecution()) { + fgExecutor.execute { + token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } } } + } else { + token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } } } } - @MainThread - private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) { - listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) } - } - interface Listener { /** Called when the route has changed for a given notification. */ fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?) @@ -260,7 +287,7 @@ constructor( override fun onAboutToConnectDeviceAdded( deviceAddress: String, deviceName: String, - deviceIcon: Drawable? + deviceIcon: Drawable?, ) { aboutToConnectDeviceOverride = AboutToConnectDevice( @@ -270,8 +297,8 @@ constructor( /* enabled */ enabled = true, /* icon */ deviceIcon, /* name */ deviceName, - /* showBroadcastButton */ showBroadcastButton = false - ) + /* showBroadcastButton */ showBroadcastButton = false, + ), ) updateCurrent() } @@ -292,7 +319,7 @@ constructor( override fun onBroadcastMetadataChanged( broadcastId: Int, - metadata: BluetoothLeBroadcastMetadata + metadata: BluetoothLeBroadcastMetadata, ) { logger.logBroadcastMetadataChanged(broadcastId, metadata.toString()) updateCurrent() @@ -352,14 +379,14 @@ constructor( // route. connectedDevice?.copy( name = it.name ?: connectedDevice.name, - icon = icon + icon = icon, ) } ?: MediaDeviceData( enabled = false, icon = MediaControlDrawables.getHomeDevices(context), name = context.getString(R.string.media_seamless_other_device), - showBroadcastButton = false + showBroadcastButton = false, ) logger.logRemoteDevice(routingSession?.name, connectedDevice) } else { @@ -398,7 +425,7 @@ constructor( device?.iconWithoutBackground, name, id = device?.id, - showBroadcastButton = false + showBroadcastButton = false, ) } } @@ -415,7 +442,7 @@ constructor( icon = iconWithoutBackground, name = name, id = id, - showBroadcastButton = false + showBroadcastButton = false, ) private fun getLeAudioBroadcastDeviceData(): MediaDeviceData { @@ -425,7 +452,7 @@ constructor( icon = MediaControlDrawables.getLeAudioSharing(context), name = context.getString(R.string.audio_sharing_description), intent = null, - showBroadcastButton = false + showBroadcastButton = false, ) } else { MediaDeviceData( @@ -433,7 +460,7 @@ constructor( icon = MediaControlDrawables.getAntenna(context), name = broadcastDescription, intent = null, - showBroadcastButton = true + showBroadcastButton = true, ) } } @@ -449,7 +476,7 @@ constructor( device, controller, routingSession?.name, - selectedRoutes?.firstOrNull()?.name + selectedRoutes?.firstOrNull()?.name, ) if (controller == null) { @@ -514,7 +541,7 @@ constructor( MediaDataUtils.getAppLabel( context, localMediaManager.packageName, - context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name) + context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name), ) val isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp) if (isCurrentBroadcastedApp) { @@ -538,5 +565,5 @@ constructor( */ private data class AboutToConnectDevice( val fullMediaDevice: MediaDevice? = null, - val backupMediaDeviceData: MediaDeviceData? = null + val backupMediaDeviceData: MediaDeviceData? = null, ) 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 684a560b0502..be4e6cc59f76 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 @@ -25,12 +25,12 @@ import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.NotificationMediaManager.isPlayingState import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.SmartspaceMediaData import com.android.systemui.media.controls.util.MediaControllerFactory import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.time.SystemClock diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt index a1f0cc33c065..8744c5c9a838 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt @@ -39,8 +39,8 @@ import androidx.lifecycle.MutableLiveData import com.android.systemui.Flags import com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.media.NotificationMediaManager import com.android.systemui.plugins.FalsingManager -import com.android.systemui.statusbar.NotificationMediaManager import com.android.systemui.util.concurrency.RepeatableExecutor import java.util.Locale import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java index c58ba377fb68..ac1672db9375 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java @@ -351,8 +351,9 @@ public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<Recycl @VisibleForTesting void showCustomEndSessionDialog(MediaDevice device) { MediaSessionReleaseDialog mediaSessionReleaseDialog = new MediaSessionReleaseDialog( - mContext, () -> transferOutput(device), mController.getColorButtonBackground(), - mController.getColorItemContent()); + mContext, () -> transferOutput(device), + mController.getColorSchemeLegacy().getColorButtonBackground(), + mController.getColorSchemeLegacy().getColorItemContent()); mediaSessionReleaseDialog.show(); } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java index 300a3578bb8f..6ab4a52dc919 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java @@ -50,9 +50,11 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.media.flags.Flags; import com.android.settingslib.media.InputMediaDevice; import com.android.settingslib.media.MediaDevice; -import com.android.settingslib.utils.ThreadUtils; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.res.R; +import java.util.concurrent.Executor; /** * A RecyclerView adapter for the legacy UI media output dialog device list. */ @@ -61,14 +63,20 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final int UNMUTE_DEFAULT_VOLUME = 2; - private static final float DEVICE_DISABLED_ALPHA = 0.5f; - private static final float DEVICE_ACTIVE_ALPHA = 1f; + @VisibleForTesting static final float DEVICE_DISABLED_ALPHA = 0.5f; + @VisibleForTesting static final float DEVICE_ACTIVE_ALPHA = 1f; + private final Executor mMainExecutor; + private final Executor mBackgroundExecutor; View mHolderView; - private boolean mIsInitVolumeFirstTime; - public MediaOutputAdapterLegacy(MediaSwitchingController controller) { + public MediaOutputAdapterLegacy( + MediaSwitchingController controller, + @Main Executor mainExecutor, + @Background Executor backgroundExecutor + ) { super(controller); - mIsInitVolumeFirstTime = true; + mMainExecutor = mainExecutor; + mBackgroundExecutor = backgroundExecutor; } @Override @@ -181,9 +189,9 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { mEndTouchArea.setVisibility(View.GONE); mEndClickIcon.setVisibility(View.GONE); mContainerLayout.setOnClickListener(null); - mTitleText.setTextColor(mController.getColorItemContent()); - mSubTitleText.setTextColor(mController.getColorItemContent()); - mVolumeValueText.setTextColor(mController.getColorItemContent()); + mTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent()); + mSubTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent()); + mVolumeValueText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent()); mIconAreaLayout.setBackground(null); updateIconAreaClickListener(null); updateSeekBarProgressColor(); @@ -193,14 +201,14 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { /** Binds a ViewHolder for a "Connect a device" item. */ void onBindPairNewDevice() { - mTitleText.setTextColor(mController.getColorItemContent()); + mTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent()); mCheckBox.setVisibility(View.GONE); updateTitle(mContext.getText(R.string.media_output_dialog_pairing_new)); updateItemBackground(ConnectionState.DISCONNECTED); final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add); mTitleIcon.setImageDrawable(addDrawable); - mTitleIcon.setImageTintList( - ColorStateList.valueOf(mController.getColorItemContent())); + mTitleIcon.setImageTintList(ColorStateList.valueOf( + mController.getColorSchemeLegacy().getColorItemContent())); mContainerLayout.setOnClickListener(mController::launchBluetoothPairing); } @@ -251,10 +259,9 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { updateSeekbarProgressBackground(); } } - boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE; mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE); if (showSeekBar) { - initSeekbar(device, isCurrentSeekbarInvisible); + initSeekbar(device); updateContainerContentA11yImportance(false /* isImportant */); mSeekBar.setContentDescription(contentDescription); } else { @@ -264,9 +271,8 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { void updateGroupSeekBar(String contentDescription) { updateSeekbarProgressBackground(); - boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE; mSeekBar.setVisibility(View.VISIBLE); - initGroupSeekbar(isCurrentSeekbarInvisible); + initGroupSeekbar(); updateContainerContentA11yImportance(false /* isImportant */); mSeekBar.setContentDescription(contentDescription); } @@ -297,8 +303,8 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { protected void updateLoadingIndicator(ConnectionState connectionState) { if (connectionState == ConnectionState.CONNECTING) { mProgressBar.setVisibility(View.VISIBLE); - mProgressBar.getIndeterminateDrawable().setTintList( - ColorStateList.valueOf(mController.getColorItemContent())); + mProgressBar.getIndeterminateDrawable().setTintList(ColorStateList.valueOf( + mController.getColorSchemeLegacy().getColorItemContent())); } else { mProgressBar.setVisibility(View.GONE); } @@ -318,8 +324,8 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { // Connected or connecting state has a darker background. int backgroundColor = isConnected || isConnecting - ? mController.getColorConnectedItemBackground() - : mController.getColorItemBackground(); + ? mController.getColorSchemeLegacy().getColorConnectedItemBackground() + : mController.getColorSchemeLegacy().getColorItemBackground(); mItemLayout.setBackgroundTintList(ColorStateList.valueOf(backgroundColor)); } @@ -332,13 +338,13 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { } private void updateSeekBarProgressColor() { - mSeekBar.setProgressTintList( - ColorStateList.valueOf(mController.getColorSeekbarProgress())); + mSeekBar.setProgressTintList(ColorStateList.valueOf( + mController.getColorSchemeLegacy().getColorSeekbarProgress())); final Drawable contrastDotDrawable = ((LayerDrawable) mSeekBar.getProgressDrawable()).findDrawableByLayerId( R.id.contrast_dot); - contrastDotDrawable.setTintList( - ColorStateList.valueOf(mController.getColorItemContent())); + contrastDotDrawable.setTintList(ColorStateList.valueOf( + mController.getColorSchemeLegacy().getColorItemContent())); } void updateSeekbarProgressBackground() { @@ -354,31 +360,21 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { mActiveRadius, 0, 0}); } - private void initializeSeekbarVolume( - @Nullable MediaDevice device, int currentVolume, - boolean isCurrentSeekbarInvisible) { + private void initializeSeekbarVolume(@Nullable MediaDevice device, int currentVolume) { if (!isDragging()) { if (mSeekBar.getVolume() != currentVolume && (mLatestUpdateVolume == -1 || currentVolume == mLatestUpdateVolume)) { // Update only if volume of device and value of volume bar doesn't match. // Check if response volume match with the latest request, to ignore obsolete // response - if (isCurrentSeekbarInvisible && !mIsInitVolumeFirstTime) { + if (!mVolumeAnimator.isStarted()) { if (currentVolume == 0) { updateMutedVolumeIcon(device); } else { updateUnmutedVolumeIcon(device); } - } else { - if (!mVolumeAnimator.isStarted()) { - if (currentVolume == 0) { - updateMutedVolumeIcon(device); - } else { - updateUnmutedVolumeIcon(device); - } - mSeekBar.setVolume(currentVolume); - mLatestUpdateVolume = -1; - } + mSeekBar.setVolume(currentVolume); + mLatestUpdateVolume = -1; } } else if (currentVolume == 0) { mSeekBar.resetVolume(); @@ -388,12 +384,9 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { mLatestUpdateVolume = -1; } } - if (mIsInitVolumeFirstTime) { - mIsInitVolumeFirstTime = false; - } } - void initSeekbar(@NonNull MediaDevice device, boolean isCurrentSeekbarInvisible) { + void initSeekbar(@NonNull MediaDevice device) { SeekBarVolumeControl volumeControl = new SeekBarVolumeControl() { @Override public int getVolume() { @@ -422,7 +415,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { } mSeekBar.setMaxVolume(device.getMaxVolume()); final int currentVolume = device.getCurrentVolume(); - initializeSeekbarVolume(device, currentVolume, isCurrentSeekbarInvisible); + initializeSeekbarVolume(device, currentVolume); mSeekBar.setOnSeekBarChangeListener(new MediaSeekBarChangedListener( device, volumeControl) { @@ -435,7 +428,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { } // Initializes the seekbar for a group of devices. - void initGroupSeekbar(boolean isCurrentSeekbarInvisible) { + void initGroupSeekbar() { SeekBarVolumeControl volumeControl = new SeekBarVolumeControl() { @Override public int getVolume() { @@ -462,7 +455,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { mSeekBar.setMaxVolume(mController.getSessionVolumeMax()); final int currentVolume = mController.getSessionVolume(); - initializeSeekbarVolume(null, currentVolume, isCurrentSeekbarInvisible); + initializeSeekbarVolume(null, currentVolume); mSeekBar.setOnSeekBarChangeListener(new MediaSeekBarChangedListener( null, volumeControl) { @Override @@ -503,9 +496,10 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { boolean isInputMediaDevice = device instanceof InputMediaDevice; int id = getDrawableId(isInputMediaDevice, isMutedVolumeIcon); mTitleIcon.setImageDrawable(mContext.getDrawable(id)); - mTitleIcon.setImageTintList(ColorStateList.valueOf(mController.getColorItemContent())); - mIconAreaLayout.setBackgroundTintList( - ColorStateList.valueOf(mController.getColorSeekbarProgress())); + mTitleIcon.setImageTintList(ColorStateList.valueOf( + mController.getColorSchemeLegacy().getColorItemContent())); + mIconAreaLayout.setBackgroundTintList(ColorStateList.valueOf( + mController.getColorSchemeLegacy().getColorSeekbarProgress())); } @VisibleForTesting @@ -534,8 +528,8 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { mStatusIcon.setVisibility(View.GONE); } else { mStatusIcon.setImageDrawable(deviceStatusIcon); - mStatusIcon.setImageTintList( - ColorStateList.valueOf(mController.getColorItemContent())); + mStatusIcon.setImageTintList(ColorStateList.valueOf( + mController.getColorSchemeLegacy().getColorItemContent())); if (deviceStatusIcon instanceof AnimatedVectorDrawable) { ((AnimatedVectorDrawable) deviceStatusIcon).start(); } @@ -585,9 +579,10 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { private void updateEndAreaWithIcon(View.OnClickListener clickListener, @DrawableRes int iconDrawableId, @StringRes int accessibilityStringId) { - updateEndAreaColor(mController.getColorSeekbarProgress()); + updateEndAreaColor(mController.getColorSchemeLegacy().getColorSeekbarProgress()); mEndClickIcon.setImageTintList( - ColorStateList.valueOf(mController.getColorItemContent())); + ColorStateList.valueOf( + mController.getColorSchemeLegacy().getColorItemContent())); mEndClickIcon.setOnClickListener(clickListener); Drawable drawable = mContext.getDrawable(iconDrawableId); mEndClickIcon.setImageDrawable(drawable); @@ -600,8 +595,9 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { private void updateEndAreaForGroupCheckBox(@NonNull MediaDevice device, @NonNull GroupStatus groupStatus) { boolean isEnabled = isGroupCheckboxEnabled(groupStatus); - updateEndAreaColor(groupStatus.selected() ? mController.getColorSeekbarProgress() - : mController.getColorItemBackground()); + updateEndAreaColor(groupStatus.selected() + ? mController.getColorSchemeLegacy().getColorSeekbarProgress() + : mController.getColorSchemeLegacy().getColorItemBackground()); mCheckBox.setContentDescription(mContext.getString( groupStatus.selected() ? R.string.accessibility_remove_device_from_group : R.string.accessibility_add_device_to_group)); @@ -611,7 +607,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { isEnabled ? (buttonView, isChecked) -> onGroupActionTriggered( !groupStatus.selected(), device) : null); mCheckBox.setEnabled(isEnabled); - setCheckBoxColor(mCheckBox, mController.getColorItemContent()); + setCheckBoxColor(mCheckBox, mController.getColorSchemeLegacy().getColorItemContent()); } private void setCheckBoxColor(CheckBox checkBox, int color) { @@ -714,15 +710,15 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { } protected void setUpDeviceIcon(@NonNull MediaDevice device) { - ThreadUtils.postOnBackgroundThread(() -> { + mBackgroundExecutor.execute(() -> { Icon icon = mController.getDeviceIconCompat(device).toIcon(mContext); - ThreadUtils.postOnMainThread(() -> { + mMainExecutor.execute(() -> { if (!TextUtils.equals(mDeviceId, device.getId())) { return; } mTitleIcon.setImageIcon(icon); - mTitleIcon.setImageTintList( - ColorStateList.valueOf(mController.getColorItemContent())); + mTitleIcon.setImageTintList(ColorStateList.valueOf( + mController.getColorSchemeLegacy().getColorItemContent())); }); }); } @@ -807,7 +803,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { } void onBind(String groupDividerTitle) { - mTitleText.setTextColor(mController.getColorItemContent()); + mTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent()); mTitleText.setText(groupDividerTitle); } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java index d791361d555f..49d09cf64c8e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java @@ -40,7 +40,6 @@ import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowInsets; import android.view.WindowManager; @@ -93,13 +92,10 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog private ImageView mAppResourceIcon; private ImageView mBroadcastIcon; private RecyclerView mDevicesRecyclerView; - private LinearLayout mDeviceListLayout; + private ViewGroup mDeviceListLayout; private LinearLayout mMediaMetadataSectionLayout; private Button mDoneButton; private Button mStopButton; - private int mListMaxHeight; - private int mItemHeight; - private int mListPaddingTop; private WallpaperColors mWallpaperColors; private boolean mShouldLaunchLeBroadcastDialog; private boolean mIsLeBroadcastCallbackRegistered; @@ -109,17 +105,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog protected Executor mExecutor; - private final ViewTreeObserver.OnGlobalLayoutListener mDeviceListLayoutListener = () -> { - ViewGroup.LayoutParams params = mDeviceListLayout.getLayoutParams(); - int totalItemsHeight = mAdapter.getItemCount() * mItemHeight - + mListPaddingTop; - int correctHeight = Math.min(totalItemsHeight, mListMaxHeight); - // Set max height for list - if (correctHeight != params.height) { - params.height = correctHeight; - mDeviceListLayout.setLayoutParams(params); - } - }; private final BluetoothLeBroadcast.Callback mBroadcastCallback = new BluetoothLeBroadcast.Callback() { @@ -220,12 +205,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog mBroadcastSender = broadcastSender; mMediaSwitchingController = mediaSwitchingController; mLayoutManager = new LayoutManagerWrapper(mContext); - mListMaxHeight = context.getResources().getDimensionPixelSize( - R.dimen.media_output_dialog_list_max_height); - mItemHeight = context.getResources().getDimensionPixelSize( - R.dimen.media_output_dialog_list_item_height); - mListPaddingTop = mContext.getResources().getDimensionPixelSize( - R.dimen.media_output_dialog_list_padding_top); mExecutor = Executors.newSingleThreadExecutor(); mIncludePlaybackAndAppMetadata = includePlaybackAndAppMetadata; } @@ -258,8 +237,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog mAppResourceIcon = mDialogView.requireViewById(R.id.app_source_icon); mBroadcastIcon = mDialogView.requireViewById(R.id.broadcast_icon); - mDeviceListLayout.getViewTreeObserver().addOnGlobalLayoutListener( - mDeviceListLayoutListener); // Init device list mLayoutManager.setAutoMeasureEnabled(true); mDevicesRecyclerView.setLayoutManager(mLayoutManager); @@ -342,7 +319,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog WallpaperColors wallpaperColors = WallpaperColors.fromBitmap(icon.getBitmap()); colorSetUpdated = !wallpaperColors.equals(mWallpaperColors); if (colorSetUpdated) { - mMediaSwitchingController.setCurrentColorScheme(wallpaperColors, isDarkThemeOn); + mMediaSwitchingController.updateCurrentColorScheme(wallpaperColors, + isDarkThemeOn); updateButtonBackgroundColorFilter(); updateDialogBackgroundColor(); } @@ -359,7 +337,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog mAppResourceIcon.setVisibility(View.GONE); } else if (appSourceIcon != null) { Icon appIcon = appSourceIcon.toIcon(mContext); - mAppResourceIcon.setColorFilter(mMediaSwitchingController.getColorItemContent()); + mAppResourceIcon.setColorFilter( + mMediaSwitchingController.getColorSchemeLegacy().getColorItemContent()); mAppResourceIcon.setImageIcon(appIcon); } else { Drawable appIconDrawable = mMediaSwitchingController.getAppSourceIconFromPackage(); @@ -369,12 +348,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog mAppResourceIcon.setVisibility(View.GONE); } } - if (mHeaderIcon.getVisibility() == View.VISIBLE) { - final int size = getHeaderIconSize(); - final int padding = mContext.getResources().getDimensionPixelSize( - R.dimen.media_output_dialog_header_icon_padding); - mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size + padding, size)); - } if (!mIncludePlaybackAndAppMetadata) { mHeaderTitle.setVisibility(View.GONE); @@ -419,18 +392,19 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog private void updateButtonBackgroundColorFilter() { ColorFilter buttonColorFilter = new PorterDuffColorFilter( - mMediaSwitchingController.getColorButtonBackground(), + mMediaSwitchingController.getColorSchemeLegacy().getColorButtonBackground(), PorterDuff.Mode.SRC_IN); mDoneButton.getBackground().setColorFilter(buttonColorFilter); mStopButton.getBackground().setColorFilter(buttonColorFilter); - mDoneButton.setTextColor(mMediaSwitchingController.getColorPositiveButtonText()); + mDoneButton.setTextColor( + mMediaSwitchingController.getColorSchemeLegacy().getColorPositiveButtonText()); } private void updateDialogBackgroundColor() { - getDialogView() - .getBackground() - .setTint(mMediaSwitchingController.getColorDialogBackground()); - mDeviceListLayout.setBackgroundColor(mMediaSwitchingController.getColorDialogBackground()); + getDialogView().getBackground().setTint( + mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground()); + mDeviceListLayout.setBackgroundColor( + mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground()); } public void handleLeBroadcastStarted() { @@ -520,8 +494,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog abstract IconCompat getHeaderIcon(); - abstract int getHeaderIconSize(); - abstract CharSequence getHeaderText(); abstract CharSequence getHeaderSubtitle(); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java index 9ade9e275ca1..791a61cc73ec 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java @@ -52,6 +52,8 @@ import com.android.systemui.statusbar.phone.SystemUIDialog; import com.google.zxing.WriterException; +import java.util.concurrent.Executor; + /** * Dialog for media output broadcast. */ @@ -239,13 +241,16 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { Context context, boolean aboveStatusbar, BroadcastSender broadcastSender, - MediaSwitchingController mediaSwitchingController) { + MediaSwitchingController mediaSwitchingController, + Executor mainExecutor, + Executor backgroundExecutor) { super( context, broadcastSender, mediaSwitchingController, /* includePlaybackAndAppMetadata */ true); - mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); + mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mainExecutor, + backgroundExecutor); // TODO(b/226710953): Move the part to MediaOutputBaseDialog for every class // that extends MediaOutputBaseDialog if (!aboveStatusbar) { @@ -295,12 +300,6 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { } @Override - int getHeaderIconSize() { - return mContext.getResources().getDimensionPixelSize( - R.dimen.media_output_dialog_header_album_icon_size); - } - - @Override CharSequence getHeaderText() { return mMediaSwitchingController.getHeaderTitle(); } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt index 2e7e66f5b384..81c85a6ad22d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt @@ -20,6 +20,9 @@ import android.content.Context import android.view.View import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import java.util.concurrent.Executor import javax.inject.Inject /** Manager to create and show a [MediaOutputBroadcastDialog]. */ @@ -29,7 +32,9 @@ constructor( private val context: Context, private val broadcastSender: BroadcastSender, private val dialogTransitionAnimator: DialogTransitionAnimator, - private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory + private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory, + @Main private val mainExecutor: Executor, + @Background private val backgroundExecutor: Executor, ) { var mediaOutputBroadcastDialog: MediaOutputBroadcastDialog? = null @@ -47,7 +52,14 @@ constructor( /* token */ null, ) val dialog = - MediaOutputBroadcastDialog(context, aboveStatusBar, broadcastSender, controller) + MediaOutputBroadcastDialog( + context, + aboveStatusBar, + broadcastSender, + controller, + mainExecutor, + backgroundExecutor, + ) mediaOutputBroadcastDialog = dialog // Show the dialog. diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorSchemeLegacy.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorSchemeLegacy.kt new file mode 100644 index 000000000000..7f0fa463811b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorSchemeLegacy.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.dialog + +import android.content.Context +import com.android.settingslib.Utils +import com.android.systemui.monet.ColorScheme +import com.android.systemui.res.R + +abstract class MediaOutputColorSchemeLegacy { + companion object Factory { + @JvmStatic + fun fromSystemColors(context: Context): MediaOutputColorSchemeLegacy { + return MediaOutputColorSchemeLegacySystem(context) + } + + @JvmStatic + fun fromDynamicColors( + colorScheme: ColorScheme, + isDarkTheme: Boolean, + ): MediaOutputColorSchemeLegacy { + return MediaOutputColorSchemeLegacyDynamic(colorScheme, isDarkTheme) + } + } + + abstract fun getColorConnectedItemBackground(): Int + + abstract fun getColorPositiveButtonText(): Int + + abstract fun getColorDialogBackground(): Int + + abstract fun getColorItemContent(): Int + + abstract fun getColorSeekbarProgress(): Int + + abstract fun getColorButtonBackground(): Int + + abstract fun getColorItemBackground(): Int +} + +class MediaOutputColorSchemeLegacySystem(private val mContext: Context) : + MediaOutputColorSchemeLegacy() { + + override fun getColorConnectedItemBackground() = + Utils.getColorStateListDefaultColor( + mContext, + R.color.media_dialog_connected_item_background, + ) + + override fun getColorPositiveButtonText() = + Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_solid_button_text) + + override fun getColorDialogBackground() = + Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_background) + + override fun getColorItemContent() = + Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_item_main_content) + + override fun getColorSeekbarProgress() = + Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_seekbar_progress) + + override fun getColorButtonBackground() = + Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_button_background) + + override fun getColorItemBackground() = + Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_item_background) +} + +class MediaOutputColorSchemeLegacyDynamic(colorScheme: ColorScheme, isDarkTheme: Boolean) : + MediaOutputColorSchemeLegacy() { + private var mColorItemContent: Int + private var mColorSeekbarProgress: Int + private var mColorButtonBackground: Int + private var mColorItemBackground: Int + private var mColorConnectedItemBackground: Int + private var mColorPositiveButtonText: Int + private var mColorDialogBackground: Int + + init { + if (isDarkTheme) { + mColorItemContent = colorScheme.accent1.s100 // A1-100 + mColorSeekbarProgress = colorScheme.accent2.s600 // A2-600 + mColorButtonBackground = colorScheme.accent1.s300 // A1-300 + mColorItemBackground = colorScheme.neutral2.s800 // N2-800 + mColorConnectedItemBackground = colorScheme.accent2.s800 // A2-800 + mColorPositiveButtonText = colorScheme.accent2.s800 // A2-800 + mColorDialogBackground = colorScheme.neutral1.s900 // N1-900 + } else { + mColorItemContent = colorScheme.accent1.s800 // A1-800 + mColorSeekbarProgress = colorScheme.accent1.s300 // A1-300 + mColorButtonBackground = colorScheme.accent1.s600 // A1-600 + mColorItemBackground = colorScheme.accent2.s50 // A2-50 + mColorConnectedItemBackground = colorScheme.accent1.s100 // A1-100 + mColorPositiveButtonText = colorScheme.neutral1.s50 // N1-50 + mColorDialogBackground = colorScheme.backgroundColor + } + } + + override fun getColorConnectedItemBackground() = mColorConnectedItemBackground + + override fun getColorPositiveButtonText() = mColorPositiveButtonText + + override fun getColorDialogBackground() = mColorDialogBackground + + override fun getColorItemContent() = mColorItemContent + + override fun getColorSeekbarProgress() = mColorSeekbarProgress + + override fun getColorButtonBackground() = mColorButtonBackground + + override fun getColorItemBackground() = mColorItemBackground +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java index 2e602be4556e..163ff248b9df 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java @@ -34,6 +34,8 @@ import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.res.R; +import java.util.concurrent.Executor; + /** * Dialog for media output transferring. */ @@ -49,11 +51,14 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { MediaSwitchingController mediaSwitchingController, DialogTransitionAnimator dialogTransitionAnimator, UiEventLogger uiEventLogger, + Executor mainExecutor, + Executor backgroundExecutor, boolean includePlaybackAndAppMetadata) { super(context, broadcastSender, mediaSwitchingController, includePlaybackAndAppMetadata); mDialogTransitionAnimator = dialogTransitionAnimator; mUiEventLogger = uiEventLogger; - mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); + mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mainExecutor, + backgroundExecutor); if (!aboveStatusbar) { getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); } @@ -76,12 +81,6 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { } @Override - int getHeaderIconSize() { - return mContext.getResources().getDimensionPixelSize( - R.dimen.media_output_dialog_header_album_icon_size); - } - - @Override CharSequence getHeaderText() { return mMediaSwitchingController.getHeaderTitle(); } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt index 4e9451a838ad..d3a81a53b6d3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt @@ -25,6 +25,9 @@ import com.android.internal.logging.UiEventLogger import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import java.util.concurrent.Executor import javax.inject.Inject /** Manager to create and show a [MediaOutputDialog]. */ @@ -37,6 +40,9 @@ constructor( private val dialogTransitionAnimator: DialogTransitionAnimator, private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory, ) { + @Inject @Main lateinit var mainExecutor: Executor + @Inject @Background lateinit var backgroundExecutor: Executor + companion object { const val INTERACTION_JANK_TAG = "media_output" var mediaOutputDialog: MediaOutputDialog? = null @@ -51,7 +57,7 @@ constructor( aboveStatusBar: Boolean, view: View? = null, userHandle: UserHandle? = null, - token: MediaSession.Token? = null + token: MediaSession.Token? = null, ) { createAndShowWithController( packageName, @@ -62,8 +68,8 @@ constructor( it, DialogCuj( InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG - ) + INTERACTION_JANK_TAG, + ), ) }, userHandle = userHandle, @@ -128,15 +134,14 @@ constructor( controller, dialogTransitionAnimator, uiEventLogger, - includePlaybackAndAppMetadata + mainExecutor, + backgroundExecutor, + includePlaybackAndAppMetadata, ) // Show the dialog. if (dialogTransitionAnimatorController != null) { - dialogTransitionAnimator.show( - mediaOutputDialog, - dialogTransitionAnimatorController, - ) + dialogTransitionAnimator.show(mediaOutputDialog, dialogTransitionAnimatorController) } else { mediaOutputDialog.show() } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java index 9d375809786a..f79693138e24 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java @@ -83,6 +83,8 @@ import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.animation.ActivityTransitionAnimator; import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.broadcast.BroadcastSender; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.media.dialog.MediaItem.MediaItemType; import com.android.systemui.media.nearby.NearbyMediaDevicesManager; @@ -114,6 +116,8 @@ import java.util.concurrent.Executor; import java.util.function.Function; import java.util.stream.Collectors; +import javax.inject.Inject; + /** * Controller for a dialog that allows users to switch media output and input devices, control * volume, connect to new devices, etc. @@ -141,7 +145,7 @@ public class MediaSwitchingController @VisibleForTesting final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>(); final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>(); - private final List<MediaItem> mOutputMediaItemList = new CopyOnWriteArrayList<>(); + private final OutputMediaItemListProxy mOutputMediaItemListProxy; private final List<MediaItem> mInputMediaItemList = new CopyOnWriteArrayList<>(); private final AudioManager mAudioManager; private final PowerExemptionManager mPowerExemptionManager; @@ -149,7 +153,8 @@ public class MediaSwitchingController private final NearbyMediaDevicesManager mNearbyMediaDevicesManager; private final Map<String, Integer> mNearbyDeviceInfoMap = new ConcurrentHashMap<>(); private final MediaSession.Token mToken; - + @Inject @Main Executor mMainExecutor; + @Inject @Background Executor mBackgroundExecutor; @VisibleForTesting boolean mIsRefreshing = false; @VisibleForTesting @@ -163,17 +168,10 @@ public class MediaSwitchingController @VisibleForTesting MediaOutputMetricLogger mMetricLogger; private int mCurrentState; - - private int mColorItemContent; - private int mColorSeekbarProgress; - private int mColorButtonBackground; - private int mColorItemBackground; - private int mColorConnectedItemBackground; - private int mColorPositiveButtonText; - private int mColorDialogBackground; private FeatureFlags mFeatureFlags; private UserTracker mUserTracker; private VolumePanelGlobalStateInteractor mVolumePanelGlobalStateInteractor; + @NonNull private MediaOutputColorSchemeLegacy mMediaOutputColorSchemeLegacy; public enum BroadcastNotifyDialog { ACTION_FIRST_LAUNCH, @@ -228,22 +226,10 @@ public class MediaSwitchingController InfoMediaManager.createInstance(mContext, packageName, userHandle, lbm, token); mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName); mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName); + mOutputMediaItemListProxy = new OutputMediaItemListProxy(); mDialogTransitionAnimator = dialogTransitionAnimator; mNearbyMediaDevicesManager = nearbyMediaDevicesManager; - mColorItemContent = Utils.getColorStateListDefaultColor(mContext, - R.color.media_dialog_item_main_content); - mColorSeekbarProgress = Utils.getColorStateListDefaultColor(mContext, - R.color.media_dialog_seekbar_progress); - mColorButtonBackground = Utils.getColorStateListDefaultColor(mContext, - R.color.media_dialog_button_background); - mColorItemBackground = Utils.getColorStateListDefaultColor(mContext, - R.color.media_dialog_item_background); - mColorConnectedItemBackground = Utils.getColorStateListDefaultColor(mContext, - R.color.media_dialog_connected_item_background); - mColorPositiveButtonText = Utils.getColorStateListDefaultColor(mContext, - R.color.media_dialog_solid_button_text); - mColorDialogBackground = Utils.getColorStateListDefaultColor(mContext, - R.color.media_dialog_background); + mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromSystemColors(mContext); if (enableInputRouting()) { mInputRouteManager = new InputRouteManager(mContext, audioManager); @@ -260,7 +246,7 @@ public class MediaSwitchingController protected void start(@NonNull Callback cb) { synchronized (mMediaDevicesLock) { mCachedMediaDevices.clear(); - mOutputMediaItemList.clear(); + mOutputMediaItemListProxy.clear(); } mNearbyDeviceInfoMap.clear(); if (mNearbyMediaDevicesManager != null) { @@ -306,7 +292,7 @@ public class MediaSwitchingController mLocalMediaManager.stopScan(); synchronized (mMediaDevicesLock) { mCachedMediaDevices.clear(); - mOutputMediaItemList.clear(); + mOutputMediaItemListProxy.clear(); } if (mNearbyMediaDevicesManager != null) { mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this); @@ -348,7 +334,7 @@ public class MediaSwitchingController @Override public void onDeviceListUpdate(List<MediaDevice> devices) { - boolean isListEmpty = mOutputMediaItemList.isEmpty(); + boolean isListEmpty = mOutputMediaItemListProxy.isEmpty(); if (isListEmpty || !mIsRefreshing) { buildMediaItems(devices); mCallback.onDeviceListChanged(); @@ -362,11 +348,12 @@ public class MediaSwitchingController } @Override - public void onSelectedDeviceStateChanged(MediaDevice device, - @LocalMediaManager.MediaDeviceState int state) { + public void onSelectedDeviceStateChanged( + MediaDevice device, @LocalMediaManager.MediaDeviceState int state) { mCallback.onRouteChanged(); mMetricLogger.logOutputItemSuccess( - device.toString(), new ArrayList<>(mOutputMediaItemList)); + device.toString(), + new ArrayList<>(mOutputMediaItemListProxy.getOutputMediaItemList())); } @Override @@ -377,7 +364,8 @@ public class MediaSwitchingController @Override public void onRequestFailed(int reason) { mCallback.onRouteChanged(); - mMetricLogger.logOutputItemFailure(new ArrayList<>(mOutputMediaItemList), reason); + mMetricLogger.logOutputItemFailure( + new ArrayList<>(mOutputMediaItemListProxy.getOutputMediaItemList()), reason); } /** @@ -396,7 +384,7 @@ public class MediaSwitchingController } try { synchronized (mMediaDevicesLock) { - mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice)); + mOutputMediaItemListProxy.removeMutingExpectedDevices(); } mAudioManager.cancelMuteAwaitConnection(mAudioManager.getMutingExpectedDevice()); } catch (Exception e) { @@ -568,26 +556,15 @@ public class MediaSwitchingController return null; } - void setCurrentColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) { - ColorScheme mCurrentColorScheme = new ColorScheme(wallpaperColors, + void updateCurrentColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) { + ColorScheme currentColorScheme = new ColorScheme(wallpaperColors, isDarkTheme); - if (isDarkTheme) { - mColorItemContent = mCurrentColorScheme.getAccent1().getS100(); // A1-100 - mColorSeekbarProgress = mCurrentColorScheme.getAccent2().getS600(); // A2-600 - mColorButtonBackground = mCurrentColorScheme.getAccent1().getS300(); // A1-300 - mColorItemBackground = mCurrentColorScheme.getNeutral2().getS800(); // N2-800 - mColorConnectedItemBackground = mCurrentColorScheme.getAccent2().getS800(); // A2-800 - mColorPositiveButtonText = mCurrentColorScheme.getAccent2().getS800(); // A2-800 - mColorDialogBackground = mCurrentColorScheme.getNeutral1().getS900(); // N1-900 - } else { - mColorItemContent = mCurrentColorScheme.getAccent1().getS800(); // A1-800 - mColorSeekbarProgress = mCurrentColorScheme.getAccent1().getS300(); // A1-300 - mColorButtonBackground = mCurrentColorScheme.getAccent1().getS600(); // A1-600 - mColorItemBackground = mCurrentColorScheme.getAccent2().getS50(); // A2-50 - mColorConnectedItemBackground = mCurrentColorScheme.getAccent1().getS100(); // A1-100 - mColorPositiveButtonText = mCurrentColorScheme.getNeutral1().getS50(); // N1-50 - mColorDialogBackground = mCurrentColorScheme.getBackgroundColor(); - } + mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromDynamicColors( + currentColorScheme, isDarkTheme); + } + + MediaOutputColorSchemeLegacy getColorSchemeLegacy() { + return mMediaOutputColorSchemeLegacy; } public void refreshDataSetIfNeeded() { @@ -598,44 +575,16 @@ public class MediaSwitchingController } } - public int getColorConnectedItemBackground() { - return mColorConnectedItemBackground; - } - - public int getColorPositiveButtonText() { - return mColorPositiveButtonText; - } - - public int getColorDialogBackground() { - return mColorDialogBackground; - } - - public int getColorItemContent() { - return mColorItemContent; - } - - public int getColorSeekbarProgress() { - return mColorSeekbarProgress; - } - - public int getColorButtonBackground() { - return mColorButtonBackground; - } - - public int getColorItemBackground() { - return mColorItemBackground; - } - private void buildMediaItems(List<MediaDevice> devices) { synchronized (mMediaDevicesLock) { - List<MediaItem> updatedMediaItems = buildMediaItems(mOutputMediaItemList, devices); - mOutputMediaItemList.clear(); - mOutputMediaItemList.addAll(updatedMediaItems); + List<MediaItem> updatedMediaItems = + buildMediaItems(mOutputMediaItemListProxy.getOutputMediaItemList(), devices); + mOutputMediaItemListProxy.clearAndAddAll(updatedMediaItems); } } - protected List<MediaItem> buildMediaItems(List<MediaItem> oldMediaItems, - List<MediaDevice> devices) { + protected List<MediaItem> buildMediaItems( + List<MediaItem> oldMediaItems, List<MediaDevice> devices) { synchronized (mMediaDevicesLock) { if (!mLocalMediaManager.isPreferenceRouteListingExist()) { attachRangeInfo(devices); @@ -698,6 +647,27 @@ public class MediaSwitchingController List<MediaItem> finalMediaItems = targetMediaDevices.stream() .map(MediaItem::createDeviceMediaItem) .collect(Collectors.toList()); + + boolean shouldAddFirstSeenSelectedDevice = + com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping(); + + if (shouldAddFirstSeenSelectedDevice) { + finalMediaItems.clear(); + Set<String> selectedDevicesIds = getSelectedMediaDevice().stream() + .map(MediaDevice::getId) + .collect(Collectors.toSet()); + for (MediaDevice targetMediaDevice : targetMediaDevices) { + if (shouldAddFirstSeenSelectedDevice + && selectedDevicesIds.contains(targetMediaDevice.getId())) { + finalMediaItems.add(MediaItem.createDeviceMediaItem( + targetMediaDevice, /* isFirstDeviceInGroup */ true)); + shouldAddFirstSeenSelectedDevice = false; + } else { + finalMediaItems.add(MediaItem.createDeviceMediaItem( + targetMediaDevice, /* isFirstDeviceInGroup */ false)); + } + } + } dividerItems.forEach(finalMediaItems::add); attachConnectNewDeviceItemIfNeeded(finalMediaItems); return finalMediaItems; @@ -722,7 +692,8 @@ public class MediaSwitchingController * list. */ @GuardedBy("mMediaDevicesLock") - private List<MediaItem> categorizeMediaItemsLocked(MediaDevice connectedMediaDevice, + private List<MediaItem> categorizeMediaItemsLocked( + MediaDevice connectedMediaDevice, List<MediaDevice> devices, boolean needToHandleMutingExpectedDevice) { List<MediaItem> finalMediaItems = new ArrayList<>(); @@ -781,6 +752,14 @@ public class MediaSwitchingController } private void attachConnectNewDeviceItemIfNeeded(List<MediaItem> mediaItems) { + MediaItem connectNewDeviceItem = getConnectNewDeviceItem(); + if (connectNewDeviceItem != null) { + mediaItems.add(connectNewDeviceItem); + } + } + + @Nullable + private MediaItem getConnectNewDeviceItem() { boolean isSelectedDeviceNotAGroup = getSelectedMediaDevice().size() == 1; if (enableInputRouting()) { // When input routing is enabled, there are expected to be at least 2 total selected @@ -789,9 +768,9 @@ public class MediaSwitchingController } // Attach "Connect a device" item only when current output is not remote and not a group - if (!isCurrentConnectedDeviceRemote() && isSelectedDeviceNotAGroup) { - mediaItems.add(MediaItem.createPairNewDeviceMediaItem()); - } + return (!isCurrentConnectedDeviceRemote() && isSelectedDeviceNotAGroup) + ? MediaItem.createPairNewDeviceMediaItem() + : null; } private void attachRangeInfo(List<MediaDevice> devices) { @@ -880,13 +859,13 @@ public class MediaSwitchingController mediaItems.add( MediaItem.createGroupDividerMediaItem( mContext.getString(R.string.media_output_group_title))); - mediaItems.addAll(mOutputMediaItemList); + mediaItems.addAll(mOutputMediaItemListProxy.getOutputMediaItemList()); } public List<MediaItem> getMediaItemList() { // If input routing is not enabled, only return output media items. if (!enableInputRouting()) { - return mOutputMediaItemList; + return mOutputMediaItemListProxy.getOutputMediaItemList(); } // If input routing is enabled, return both output and input media items. @@ -992,7 +971,7 @@ public class MediaSwitchingController public boolean isAnyDeviceTransferring() { synchronized (mMediaDevicesLock) { - for (MediaItem mediaItem : mOutputMediaItemList) { + for (MediaItem mediaItem : mOutputMediaItemListProxy.getOutputMediaItemList()) { if (mediaItem.getMediaDevice().isPresent() && mediaItem.getMediaDevice().get().getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) { @@ -1032,8 +1011,11 @@ public class MediaSwitchingController startActivity(launchIntent, controller); } - void launchLeBroadcastNotifyDialog(View mediaOutputDialog, BroadcastSender broadcastSender, - BroadcastNotifyDialog action, final DialogInterface.OnClickListener listener) { + void launchLeBroadcastNotifyDialog( + View mediaOutputDialog, + BroadcastSender broadcastSender, + BroadcastNotifyDialog action, + final DialogInterface.OnClickListener listener) { final AlertDialog.Builder builder = new AlertDialog.Builder(mContext); switch (action) { case ACTION_FIRST_LAUNCH: @@ -1076,7 +1058,7 @@ public class MediaSwitchingController mVolumePanelGlobalStateInteractor, mUserTracker); MediaOutputBroadcastDialog dialog = new MediaOutputBroadcastDialog(mContext, true, - broadcastSender, controller); + broadcastSender, controller, mMainExecutor, mBackgroundExecutor); dialog.show(); } @@ -1263,8 +1245,8 @@ public class MediaSwitchingController return !sourceList.isEmpty(); } - boolean addSourceIntoSinkDeviceWithBluetoothLeAssistant(BluetoothDevice sink, - BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) { + boolean addSourceIntoSinkDeviceWithBluetoothLeAssistant( + BluetoothDevice sink, BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) { LocalBluetoothLeBroadcastAssistant assistant = mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); if (assistant == null) { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java new file mode 100644 index 000000000000..1c9c0b102cb7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java @@ -0,0 +1,55 @@ +/* + * 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.systemui.media.dialog; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** A proxy of holding the list of Output Switcher's output media items. */ +public class OutputMediaItemListProxy { + private final List<MediaItem> mOutputMediaItemList; + + public OutputMediaItemListProxy() { + mOutputMediaItemList = new CopyOnWriteArrayList<>(); + } + + /** Returns the list of output media items. */ + public List<MediaItem> getOutputMediaItemList() { + return mOutputMediaItemList; + } + + /** Updates the list of output media items with the given list. */ + public void clearAndAddAll(List<MediaItem> updatedMediaItems) { + mOutputMediaItemList.clear(); + mOutputMediaItemList.addAll(updatedMediaItems); + } + + /** Removes the media items with muting expected devices. */ + public void removeMutingExpectedDevices() { + mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice)); + } + + /** Clears the output media item list. */ + public void clear() { + mOutputMediaItemList.clear(); + } + + /** Returns whether the output media item list is empty. */ + public boolean isEmpty() { + return mOutputMediaItemList.isEmpty(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/domain/interactor/MediaInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/interactor/MediaInteractor.kt new file mode 100644 index 000000000000..afed141644ff --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/interactor/MediaInteractor.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.remedia.domain.interactor + +import com.android.systemui.media.remedia.domain.model.MediaSessionModel + +/** + * Defines interface for classes that can provide business logic in the domain of the media controls + * element. + */ +interface MediaInteractor { + + /** The list of sessions. Needs to be backed by a compose snapshot state. */ + val sessions: List<MediaSessionModel> + + /** Seek to [to], in milliseconds on the media session with the given [sessionKey]. */ + fun seek(sessionKey: Any, to: Long) + + /** Hide the representation of the media session with the given [sessionKey]. */ + fun hide(sessionKey: Any) + + /** Open media settings. */ + fun openMediaSettings() +} diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaActionModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaActionModel.kt new file mode 100644 index 000000000000..02e4d7a966c2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaActionModel.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.remedia.domain.model + +import com.android.systemui.common.shared.model.Icon + +sealed interface MediaActionModel { + data class Action(val icon: Icon, val onClick: (() -> Unit)?) : MediaActionModel + + data object ReserveSpace : MediaActionModel + + data object None : MediaActionModel +} diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaOutputDeviceModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaOutputDeviceModel.kt new file mode 100644 index 000000000000..d581ae3e4e40 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaOutputDeviceModel.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.remedia.domain.model + +import com.android.systemui.common.shared.model.Icon + +data class MediaOutputDeviceModel(val name: String, val icon: Icon, val isInProgress: Boolean) diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaSessionModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaSessionModel.kt new file mode 100644 index 000000000000..e64ce73226f2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaSessionModel.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.remedia.domain.model + +import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.ImageBitmap +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.media.remedia.shared.model.MediaCardActionButtonLayout +import com.android.systemui.media.remedia.shared.model.MediaColorScheme +import com.android.systemui.media.remedia.shared.model.MediaSessionState + +/** Data model representing a media session. */ +@Stable +interface MediaSessionModel { + /** Unique identifier. */ + val key: Any + + val appName: String + + val appIcon: Icon + + val background: ImageBitmap? + + val colorScheme: MediaColorScheme + + val title: String + + val subtitle: String + + val onClick: () -> Unit + + /** + * Whether the session is currently active. Under some UIs, only currently active session should + * be shown. + */ + val isActive: Boolean + + /** Whether the session can be hidden/dismissed by the user. */ + val canBeHidden: Boolean + + /** + * Whether the session currently supports scrubbing (e.g. moving to a different position iin the + * playback. + */ + val canBeScrubbed: Boolean + + val state: MediaSessionState + + /** The position of the playback within the current track. */ + val positionMs: Long + + /** The total duration of the current track. */ + val durationMs: Long + + val outputDevice: MediaOutputDeviceModel + + /** How to lay out the action buttons. */ + val actionButtonLayout: MediaCardActionButtonLayout + val playPauseAction: MediaActionModel + val leftAction: MediaActionModel + val rightAction: MediaActionModel + val additionalActions: List<MediaActionModel.Action> +} diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaActionState.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaActionState.kt new file mode 100644 index 000000000000..c3ce5035accc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaActionState.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.remedia.shared.model + +enum class MediaActionState { + Enabled, + Disabled, + Hidden, +} diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaCardActionButtonLayout.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaCardActionButtonLayout.kt new file mode 100644 index 000000000000..554fb1f2fc43 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaCardActionButtonLayout.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.remedia.shared.model + +enum class MediaCardActionButtonLayout { + /** Shows the play/pause button and left/right buttons in privileged positions on the card */ + WithPlayPause, + /** Shows all action buttons along the bottom row. */ + SecondaryActionsOnly, +} diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaColorScheme.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaColorScheme.kt new file mode 100644 index 000000000000..8dba170f0928 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaColorScheme.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.remedia.shared.model + +import androidx.compose.ui.graphics.Color + +data class MediaColorScheme(val primary: Color, val onPrimary: Color) diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt new file mode 100644 index 000000000000..fea5b3267562 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.remedia.ui.compose + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerScope +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp +import com.android.compose.modifiers.thenIf +import kotlinx.coroutines.launch + +/** State for a [DismissibleHorizontalPager] */ +class DismissibleHorizontalPagerState( + val isDismissible: Boolean, + val isScrollingEnabled: Boolean, + val pagerState: PagerState, + val offset: Animatable<Float, AnimationVector1D>, +) + +/** + * Returns a remembered [DismissibleHorizontalPagerState] that starts at [initialPage] and has + * [pageCount] total pages. + */ +@Composable +fun rememberDismissibleHorizontalPagerState( + isDismissible: Boolean = true, + isScrollingEnabled: Boolean = true, + initialPage: Int = 0, + pageCount: () -> Int, +): DismissibleHorizontalPagerState { + val pagerState = rememberPagerState(initialPage = initialPage, pageCount = pageCount) + val offset = remember { Animatable(0f) } + + return remember(isDismissible, isScrollingEnabled, pagerState, offset) { + DismissibleHorizontalPagerState( + isDismissible = isDismissible, + isScrollingEnabled = isScrollingEnabled, + pagerState = pagerState, + offset = offset, + ) + } +} + +/** + * A [HorizontalPager] that can be swiped-away to dismiss by the user when swiped farther left or + * right once fully scrolled to the left-most or right-most page, respectively. + */ +@Composable +fun DismissibleHorizontalPager( + state: DismissibleHorizontalPagerState, + onDismissed: () -> Unit, + modifier: Modifier = Modifier, + key: ((Int) -> Any)? = null, + pageSpacing: Dp = 0.dp, + indicator: @Composable BoxScope.() -> Unit, + pageContent: @Composable PagerScope.(page: Int) -> Unit, +) { + val scope = rememberCoroutineScope() + + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + return if (state.offset.value > 0f && available.x < 0f) { + scope.launch { state.offset.snapTo(state.offset.value + available.x) } + Offset(available.x, 0f) + } else if (state.offset.value < 0f && available.x > 0f) { + scope.launch { state.offset.snapTo(state.offset.value + available.x) } + Offset(available.x, 0f) + } else { + Offset.Zero + } + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + return if (available.x > 0f) { + scope.launch { state.offset.snapTo(state.offset.value + available.x) } + Offset(available.x, 0f) + } else if (available.x < 0f) { + scope.launch { state.offset.snapTo(state.offset.value + available.x) } + Offset(available.x, 0f) + } else { + Offset.Zero + } + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + scope.launch { + state.offset.animateTo( + if (state.offset.value >= state.pagerState.layoutInfo.pageSize / 2f) { + state.pagerState.layoutInfo.pageSize * 2f + } else if ( + state.offset.value <= -state.pagerState.layoutInfo.pageSize / 2f + ) { + -state.pagerState.layoutInfo.pageSize * 2f + } else { + 0f + } + ) + if (state.offset.value != 0f) { + onDismissed() + } + } + return super.onPostFling(consumed, available) + } + } + } + + Box(modifier = modifier) { + HorizontalPager( + state = state.pagerState, + userScrollEnabled = state.isScrollingEnabled, + key = key, + pageSpacing = pageSpacing, + pageContent = pageContent, + modifier = + Modifier.thenIf(state.isDismissible) { + Modifier.nestedScroll(nestedScrollConnection).graphicsLayer { + translationX = state.offset.value + } + }, + ) + + indicator() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt index c9fb8e877009..9eb55a8eff2e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt @@ -20,6 +20,7 @@ package com.android.systemui.media.remedia.ui.compose import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode @@ -36,6 +37,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.hoverable +import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.MutableInteractionSource @@ -43,9 +45,9 @@ import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -63,14 +65,14 @@ import androidx.compose.material3.SliderColors import androidx.compose.material3.SliderDefaults.colors import androidx.compose.material3.SliderState import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -90,6 +92,7 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.Layout +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp @@ -105,20 +108,137 @@ import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneTransitionLayout import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState import com.android.compose.animation.scene.transitions -import com.android.compose.theme.LocalAndroidColorScheme +import com.android.compose.ui.graphics.painter.rememberDrawablePainter import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.common.ui.compose.PagerDots import com.android.systemui.common.ui.compose.load import com.android.systemui.communal.ui.compose.extensions.detectLongPressGesture +import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.media.remedia.shared.model.MediaCardActionButtonLayout +import com.android.systemui.media.remedia.shared.model.MediaColorScheme import com.android.systemui.media.remedia.shared.model.MediaSessionState import com.android.systemui.media.remedia.ui.viewmodel.MediaCardGutsViewModel import com.android.systemui.media.remedia.ui.viewmodel.MediaCardViewModel +import com.android.systemui.media.remedia.ui.viewmodel.MediaCarouselVisibility +import com.android.systemui.media.remedia.ui.viewmodel.MediaNavigationViewModel import com.android.systemui.media.remedia.ui.viewmodel.MediaOutputSwitcherChipViewModel import com.android.systemui.media.remedia.ui.viewmodel.MediaPlayPauseActionViewModel import com.android.systemui.media.remedia.ui.viewmodel.MediaSecondaryActionViewModel -import com.android.systemui.media.remedia.ui.viewmodel.MediaSeekBarViewModel +import com.android.systemui.media.remedia.ui.viewmodel.MediaViewModel import kotlin.math.max +/** + * Renders a media controls UI element. + * + * This composable supports a multitude of presentation styles/layouts controlled by the + * [presentationStyle] parameter. If the card carousel can be swiped away to dismiss by the user, + * the [onDismissed] callback will be invoked when/if that happens. + */ +@Composable +fun Media( + viewModelFactory: MediaViewModel.Factory, + presentationStyle: MediaPresentationStyle, + behavior: MediaUiBehavior, + onDismissed: () -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + val viewModel: MediaViewModel = + rememberViewModel("Media.viewModel") { + viewModelFactory.create( + context = context, + carouselVisibility = behavior.carouselVisibility, + ) + } + + CardCarousel( + viewModel = viewModel, + presentationStyle = presentationStyle, + behavior = behavior, + onDismissed = onDismissed, + modifier = modifier, + ) +} + +/** + * Renders a media controls carousel of cards. + * + * This composable supports a multitude of presentation styles/layouts controlled by the + * [presentationStyle] parameter. The behavior is controlled by [behavior]. If + * [MediaUiBehavior.isCarouselDismissible] is `true`, the [onDismissed] callback will be invoked + * when/if that happens. + */ +@Composable +private fun CardCarousel( + viewModel: MediaViewModel, + presentationStyle: MediaPresentationStyle, + behavior: MediaUiBehavior, + onDismissed: () -> Unit, + modifier: Modifier = Modifier, +) { + AnimatedVisibility(visible = viewModel.isCarouselVisible, modifier = modifier) { + CardCarouselContent( + viewModel = viewModel, + presentationStyle = presentationStyle, + behavior = behavior, + onDismissed = onDismissed, + ) + } +} + +@Composable +private fun CardCarouselContent( + viewModel: MediaViewModel, + presentationStyle: MediaPresentationStyle, + behavior: MediaUiBehavior, + onDismissed: () -> Unit, + modifier: Modifier = Modifier, +) { + val pagerState = + rememberDismissibleHorizontalPagerState( + isDismissible = behavior.isCarouselDismissible, + isScrollingEnabled = behavior.isCarouselScrollingEnabled, + ) { + viewModel.cards.size + } + + val roundedCornerShape = RoundedCornerShape(32.dp) + + LaunchedEffect(pagerState.pagerState.currentPage) { + viewModel.onCardSelected(pagerState.pagerState.currentPage) + } + + DismissibleHorizontalPager( + state = pagerState, + onDismissed = onDismissed, + pageSpacing = 8.dp, + key = { index -> viewModel.cards[index].key }, + indicator = { + if (pagerState.pagerState.pageCount > 1) { + PagerDots( + pagerState = pagerState.pagerState, + activeColor = Color(0xffdee0ff), + nonActiveColor = Color(0xffa7a9ca), + dotSize = 6.dp, + spaceSize = 6.dp, + modifier = + Modifier.align(Alignment.BottomCenter).padding(8.dp).graphicsLayer { + translationX = pagerState.offset.value + }, + ) + } + }, + modifier = modifier.padding(8.dp).clip(roundedCornerShape), + ) { index -> + Card( + viewModel = viewModel.cards[index], + presentationStyle = presentationStyle, + modifier = Modifier.clip(roundedCornerShape), + ) + } +} + /** Renders the UI of a single media card. */ @Composable private fun Card( @@ -139,7 +259,7 @@ private fun Card( Box(modifier) { if (stlState.currentScene != Media.Scenes.Compact) { - CardBackground(imageLoader = viewModel.artLoader, modifier = Modifier.matchParentSize()) + CardBackground(image = viewModel.background, modifier = Modifier.matchParentSize()) } key(stlState) { @@ -158,6 +278,22 @@ private fun Card( } } +@Composable +private fun rememberAnimatedColorScheme(colorScheme: MediaColorScheme): AnimatedColorScheme { + val animatedPrimary by animateColorAsState(targetValue = colorScheme.primary) + val animatedOnPrimary by animateColorAsState(targetValue = colorScheme.onPrimary) + + return remember { + object : AnimatedColorScheme { + override val primary: Color + get() = animatedPrimary + + override val onPrimary: Color + get() = animatedOnPrimary + } + } +} + /** * Renders the foreground of a card, including all UI content and the internal "guts". * @@ -180,6 +316,8 @@ private fun ContentScope.CardForeground( val isGutsVisible = viewModel.guts.isVisible LaunchedEffect(isGutsVisible) { gutsAlphaAnimatable.animateTo(if (isGutsVisible) 1f else 0f) } + val colorScheme = rememberAnimatedColorScheme(viewModel.colorScheme) + // Use a custom layout to measure the content even if the content is being hidden because the // internal guts are showing. This is needed because only the content knows the size the of the // card and the guts are set to be the same size of the content. @@ -189,6 +327,7 @@ private fun ContentScope.CardForeground( viewModel = viewModel, threeRows = threeRows, fillHeight = fillHeight, + colorScheme = colorScheme, modifier = Modifier.graphicsLayer { compositingStrategy = CompositingStrategy.ModulateAlpha @@ -198,6 +337,7 @@ private fun ContentScope.CardForeground( CardGuts( viewModel = viewModel.guts, + colorScheme = colorScheme, modifier = Modifier.graphicsLayer { compositingStrategy = CompositingStrategy.ModulateAlpha @@ -232,29 +372,37 @@ private fun ContentScope.CardForegroundContent( viewModel: MediaCardViewModel, threeRows: Boolean, fillHeight: Boolean, + colorScheme: AnimatedColorScheme, modifier: Modifier = Modifier, ) { Column( modifier = - modifier - .combinedClickable( - onClick = viewModel.onClick, - onLongClick = viewModel.onLongClick, - onClickLabel = viewModel.onClickLabel, - ) - .padding(16.dp) + modifier.combinedClickable( + onClick = viewModel.onClick, + onLongClick = viewModel.onLongClick, + onClickLabel = viewModel.onClickLabel, + ) ) { // Always add the first/top row, regardless of presentation style. - Row(verticalAlignment = Alignment.CenterVertically) { + Box(modifier = Modifier.fillMaxWidth()) { // Icon. Icon( icon = viewModel.icon, - tint = LocalAndroidColorScheme.current.primaryFixed, - modifier = Modifier.size(24.dp).clip(CircleShape), + tint = colorScheme.primary, + modifier = + Modifier.align(Alignment.TopStart) + .padding(top = 16.dp, start = 16.dp) + .size(24.dp) + .clip(CircleShape), ) - Spacer(modifier = Modifier.weight(1f)) - viewModel.outputSwitcherChips.fastForEach { chip -> - OutputSwitcherChip(viewModel = chip, modifier = Modifier.padding(start = 8.dp)) + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.align(Alignment.TopEnd), + ) { + viewModel.outputSwitcherChips.fastForEach { chip -> + OutputSwitcherChip(viewModel = chip, colorScheme = colorScheme) + } } } @@ -271,7 +419,7 @@ private fun ContentScope.CardForegroundContent( // Second row. Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(top = 16.dp), + modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp), ) { Metadata( title = viewModel.title, @@ -280,14 +428,16 @@ private fun ContentScope.CardForegroundContent( modifier = Modifier.weight(1f).padding(end = 8.dp), ) - AnimatedVisibility(visible = viewModel.playPauseAction.isVisible) { - PlayPauseAction( - viewModel = viewModel.playPauseAction, - buttonWidth = 48.dp, - buttonColor = LocalAndroidColorScheme.current.primaryFixed, - iconColor = LocalAndroidColorScheme.current.onPrimaryFixed, - buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp }, - ) + if (viewModel.actionButtonLayout == MediaCardActionButtonLayout.WithPlayPause) { + AnimatedVisibility(visible = viewModel.playPauseAction != null) { + PlayPauseAction( + viewModel = checkNotNull(viewModel.playPauseAction), + buttonWidth = 48.dp, + buttonColor = colorScheme.primary, + iconColor = colorScheme.onPrimary, + buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp }, + ) + } } } @@ -295,9 +445,16 @@ private fun ContentScope.CardForegroundContent( Row( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(top = 24.dp), + modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp, bottom = 16.dp), ) { - Navigation(viewModel = viewModel.seekBar, isSeekBarVisible = true) + Navigation( + viewModel = viewModel.navigation, + isSeekBarVisible = true, + areActionsVisible = + viewModel.actionButtonLayout == MediaCardActionButtonLayout.WithPlayPause, + modifier = Modifier.weight(1f), + ) + viewModel.additionalActions.fastForEachIndexed { index, action -> SecondaryAction( viewModel = action, @@ -311,7 +468,7 @@ private fun ContentScope.CardForegroundContent( // Bottom row. Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(top = 36.dp), + modifier = Modifier.padding(start = 16.dp, top = 36.dp, end = 16.dp, bottom = 16.dp), ) { Metadata( title = viewModel.title, @@ -321,18 +478,34 @@ private fun ContentScope.CardForegroundContent( ) Navigation( - viewModel = viewModel.seekBar, + viewModel = viewModel.navigation, isSeekBarVisible = false, + areActionsVisible = + viewModel.actionButtonLayout == MediaCardActionButtonLayout.WithPlayPause, modifier = Modifier.padding(end = 8.dp), ) - PlayPauseAction( - viewModel = viewModel.playPauseAction, - buttonWidth = 48.dp, - buttonColor = LocalAndroidColorScheme.current.primaryFixed, - iconColor = LocalAndroidColorScheme.current.onPrimaryFixed, - buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp }, - ) + if ( + viewModel.actionButtonLayout == MediaCardActionButtonLayout.SecondaryActionsOnly + ) { + viewModel.additionalActions.fastForEachIndexed { index, action -> + SecondaryAction( + viewModel = action, + element = Media.Elements.additionalActionButton(index), + modifier = Modifier.padding(end = 8.dp), + ) + } + } + + AnimatedVisibility(visible = viewModel.playPauseAction != null) { + PlayPauseAction( + viewModel = checkNotNull(viewModel.playPauseAction), + buttonWidth = 48.dp, + buttonColor = colorScheme.primary, + iconColor = colorScheme.onPrimary, + buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp }, + ) + } } } } @@ -375,18 +548,18 @@ private fun ContentScope.CompactCardForeground( iconColor = MaterialTheme.colorScheme.onSurface, ) - val nextAction = (viewModel.seekBar as? MediaSeekBarViewModel.Showing)?.next - if (nextAction != null) { + val rightAction = (viewModel.navigation as? MediaNavigationViewModel.Showing)?.right + if (rightAction != null) { SecondaryAction( - viewModel = nextAction, + viewModel = rightAction, element = Media.Elements.NextButton, iconColor = MaterialTheme.colorScheme.onSurface, ) } - AnimatedVisibility(visible = viewModel.playPauseAction.isVisible) { + AnimatedVisibility(visible = viewModel.playPauseAction != null) { PlayPauseAction( - viewModel = viewModel.playPauseAction, + viewModel = checkNotNull(viewModel.playPauseAction), buttonWidth = 72.dp, buttonColor = MaterialTheme.colorScheme.primaryContainer, iconColor = MaterialTheme.colorScheme.onPrimaryContainer, @@ -398,47 +571,38 @@ private fun ContentScope.CompactCardForeground( /** Renders the background of a card, loading the artwork and showing an overlay on top of it. */ @Composable -private fun CardBackground(imageLoader: suspend () -> ImageBitmap, modifier: Modifier = Modifier) { - var image: ImageBitmap? by remember { mutableStateOf(null) } - LaunchedEffect(imageLoader) { - image = null - image = imageLoader() - } - - val gradientBaseColor = MaterialTheme.colorScheme.onSurface - Box( - modifier = - modifier.drawWithContent { - // Draw the content of the box (loaded art or placeholder). - drawContent() - - if (image != null) { - // Then draw the overlay. - drawRect( - brush = - Brush.radialGradient( - 0f to gradientBaseColor.copy(alpha = 0.65f), - 1f to gradientBaseColor.copy(alpha = 0.75f), - center = size.center, - radius = max(size.width, size.height) / 2, - ) - ) - } - } - ) { - image?.let { loadedImage -> +private fun CardBackground(image: ImageBitmap?, modifier: Modifier = Modifier) { + Crossfade(targetState = image, modifier = modifier) { imageOrNull -> + if (imageOrNull != null) { // Loaded art. + val gradientBaseColor = MaterialTheme.colorScheme.onSurface Image( - bitmap = loadedImage, + bitmap = imageOrNull, contentDescription = null, contentScale = ContentScale.Crop, - modifier = Modifier.matchParentSize(), + modifier = + Modifier.fillMaxSize().drawWithContent { + // Draw the content (loaded art). + drawContent() + + if (image != null) { + // Then draw the overlay. + drawRect( + brush = + Brush.radialGradient( + 0f to gradientBaseColor.copy(alpha = 0.65f), + 1f to gradientBaseColor.copy(alpha = 0.75f), + center = size.center, + radius = max(size.width, size.height) / 2, + ) + ) + } + }, ) + } else { + // Placeholder. + Box(Modifier.background(MaterialTheme.colorScheme.onSurface).fillMaxSize()) } - ?: run { - // Placeholder. - Box(Modifier.background(MaterialTheme.colorScheme.onSurface).matchParentSize()) - } } } @@ -449,22 +613,26 @@ private fun CardBackground(imageLoader: suspend () -> ImageBitmap, modifier: Mod * would otherwise be showing based on the view-model alone. This is meant for callers to decide * whether they'd like to show the seek bar in addition to the prev/next buttons or just show the * buttons. + * + * If [areActionsVisible] is `false`, the left/right buttons to the left and right of the seek bar + * will not be included in the layout. */ @Composable private fun ContentScope.Navigation( - viewModel: MediaSeekBarViewModel, + viewModel: MediaNavigationViewModel, isSeekBarVisible: Boolean, + areActionsVisible: Boolean, modifier: Modifier = Modifier, ) { when (viewModel) { - is MediaSeekBarViewModel.Showing -> { + is MediaNavigationViewModel.Showing -> { Row( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, modifier = modifier, ) { - viewModel.previous?.let { - SecondaryAction(viewModel = it, element = Media.Elements.PrevButton) + if (areActionsVisible) { + SecondaryAction(viewModel = viewModel.left, element = Media.Elements.PrevButton) } val interactionSource = remember { MutableInteractionSource() } @@ -499,13 +667,16 @@ private fun ContentScope.Navigation( } } - viewModel.next?.let { - SecondaryAction(viewModel = it, element = Media.Elements.NextButton) + if (areActionsVisible) { + SecondaryAction( + viewModel = viewModel.right, + element = Media.Elements.NextButton, + ) } } } - is MediaSeekBarViewModel.Hidden -> Unit + is MediaNavigationViewModel.Hidden -> Unit } } @@ -647,7 +818,11 @@ private fun SeekBarTrack( /** Renders the internal "guts" of a card. */ @Composable -private fun CardGuts(viewModel: MediaCardGutsViewModel, modifier: Modifier = Modifier) { +private fun CardGuts( + viewModel: MediaCardGutsViewModel, + colorScheme: AnimatedColorScheme, + modifier: Modifier = Modifier, +) { Box( modifier = modifier.pointerInput(Unit) { detectLongPressGesture { viewModel.onLongClick() } } @@ -682,7 +857,7 @@ private fun CardGuts(viewModel: MediaCardGutsViewModel, modifier: Modifier = Mod ) { Text( text = checkNotNull(viewModel.primaryAction.text), - color = LocalAndroidColorScheme.current.onPrimaryFixed, + color = colorScheme.onPrimary, ) } @@ -740,29 +915,51 @@ private fun ContentScope.Metadata( @Composable private fun OutputSwitcherChip( viewModel: MediaOutputSwitcherChipViewModel, + colorScheme: AnimatedColorScheme, modifier: Modifier = Modifier, ) { - PlatformButton( - onClick = viewModel.onClick, - colors = - ButtonDefaults.buttonColors( - containerColor = LocalAndroidColorScheme.current.primaryFixed - ), - contentPadding = PaddingValues(start = 8.dp, end = 12.dp, top = 4.dp, bottom = 4.dp), - modifier = modifier.height(24.dp), + // For accessibility reasons, the touch area for the chip needs to be at least 48dp in height. + // At the same time, the rounded corner chip should only be as tall as it needs to be to contain + // its contents and look like a nice design; also, the ripple effect should only be shown within + // the bounds of the chip. + // + // This is achieved by sharing this InteractionSource between the outer and inner composables. + // + // The outer composable hosts that clickable that writes user events into the InteractionSource. + // The inner composable consumes the user events from the InteractionSource and feeds them into + // its indication. + val clickInteractionSource = remember { MutableInteractionSource() } + Box( + modifier = + modifier + .height(48.dp) + .clickable(interactionSource = clickInteractionSource, indication = null) { + viewModel.onClick() + } + .padding(top = 16.dp, end = 16.dp, bottom = 8.dp) ) { - Icon( - icon = viewModel.icon, - tint = LocalAndroidColorScheme.current.onPrimaryFixed, - modifier = Modifier.size(16.dp), - ) - viewModel.text?.let { - Spacer(Modifier.size(4.dp)) - Text( - text = viewModel.text, - style = MaterialTheme.typography.bodySmall, - color = LocalAndroidColorScheme.current.onPrimaryFixed, + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.clip(RoundedCornerShape(12.dp)) + .background(colorScheme.primary) + .indication(clickInteractionSource, ripple()) + .padding(start = 8.dp, end = 12.dp, top = 4.dp, bottom = 4.dp), + ) { + Icon( + icon = viewModel.icon, + tint = colorScheme.onPrimary, + modifier = Modifier.size(16.dp), ) + + viewModel.text?.let { + Text( + text = viewModel.text, + style = MaterialTheme.typography.bodySmall, + color = colorScheme.onPrimary, + ) + } } } } @@ -785,7 +982,8 @@ private fun ContentScope.PlayPauseAction( // This element can be animated when switching between scenes inside a media card. Element(key = Media.Elements.PlayPauseButton, modifier = modifier) { PlatformButton( - onClick = viewModel.onClick, + onClick = viewModel.onClick ?: {}, + enabled = viewModel.onClick != null, colors = ButtonDefaults.buttonColors(containerColor = buttonColor), shape = RoundedCornerShape(cornerRadius), modifier = Modifier.size(width = buttonWidth, height = 48.dp), @@ -793,22 +991,29 @@ private fun ContentScope.PlayPauseAction( when (viewModel.state) { is MediaSessionState.Playing, is MediaSessionState.Paused -> { - // TODO(b/399860531): load this expensive-to-load animated vector drawable off - // the main thread. - val iconResource = checkNotNull(viewModel.icon) - Icon( - painter = - rememberAnimatedVectorPainter( - animatedImageVector = - AnimatedImageVector.animatedVectorResource( - id = iconResource.res - ), - atEnd = viewModel.state == MediaSessionState.Playing, - ), - contentDescription = iconResource.contentDescription?.load(), - tint = iconColor, - modifier = Modifier.size(24.dp), - ) + val painterOrNull = + when (viewModel.icon) { + // TODO(b/399860531): load this expensive-to-load animated vector + // drawable off the main thread. + is Icon.Resource -> + rememberAnimatedVectorPainter( + animatedImageVector = + AnimatedImageVector.animatedVectorResource( + id = viewModel.icon.res + ), + atEnd = viewModel.state == MediaSessionState.Playing, + ) + is Icon.Loaded -> rememberDrawablePainter(viewModel.icon.drawable) + null -> null + } + painterOrNull?.let { painter -> + Icon( + painter = painter, + contentDescription = viewModel.icon?.contentDescription?.load(), + tint = iconColor, + modifier = Modifier.size(24.dp), + ) + } } is MediaSessionState.Buffering -> { CircularProgressIndicator(color = iconColor, modifier = Modifier.size(24.dp)) @@ -831,7 +1036,7 @@ private fun ContentScope.SecondaryAction( element: ElementKey? = null, iconColor: Color = Color.White, ) { - if (element != null) { + if (viewModel !is MediaSecondaryActionViewModel.None && element != null) { Element(key = element, modifier = modifier) { SecondaryActionContent(viewModel = viewModel, iconColor = iconColor) } @@ -847,14 +1052,22 @@ private fun SecondaryActionContent( iconColor: Color, modifier: Modifier = Modifier, ) { - PlatformIconButton( - onClick = viewModel.onClick, - iconResource = (viewModel.icon as Icon.Resource).res, - contentDescription = viewModel.icon.contentDescription?.load(), - colors = IconButtonDefaults.iconButtonColors(contentColor = iconColor), - enabled = viewModel.isEnabled, - modifier = modifier.size(48.dp).padding(13.dp), - ) + val sharedModifier = modifier.size(48.dp).padding(13.dp) + when (viewModel) { + is MediaSecondaryActionViewModel.Action -> + PlatformIconButton( + onClick = viewModel.onClick ?: {}, + iconResource = (viewModel.icon as Icon.Resource).res, + contentDescription = viewModel.icon.contentDescription?.load(), + colors = IconButtonDefaults.iconButtonColors(contentColor = iconColor), + enabled = viewModel.onClick != null, + modifier = sharedModifier, + ) + + is MediaSecondaryActionViewModel.ReserveSpace -> Spacer(modifier = sharedModifier) + + is MediaSecondaryActionViewModel.None -> Unit + } } /** Enumerates all supported media presentation styles. */ @@ -867,6 +1080,19 @@ enum class MediaPresentationStyle { Compact, } +data class MediaUiBehavior( + val isCarouselDismissible: Boolean = true, + val isCarouselScrollingEnabled: Boolean = true, + val carouselVisibility: MediaCarouselVisibility = MediaCarouselVisibility.WhenNotEmpty, + val isFalsingProtectionNeeded: Boolean = false, +) + +@Stable +private interface AnimatedColorScheme { + val primary: Color + val onPrimary: Color +} + private object Media { /** diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt index ecd6e6d094d9..833a04ddcb55 100644 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt @@ -19,6 +19,8 @@ package com.android.systemui.media.remedia.ui.viewmodel import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.ImageBitmap import com.android.systemui.common.shared.model.Icon +import com.android.systemui.media.remedia.shared.model.MediaCardActionButtonLayout +import com.android.systemui.media.remedia.shared.model.MediaColorScheme /** Models UI state for a media card. */ @Stable @@ -31,20 +33,19 @@ interface MediaCardViewModel { val icon: Icon - /** - * A callback to load the artwork for the media shown on this card. This callback will be - * invoked on the main thread, it's up to the implementation to move the loading off the main - * thread. - */ - val artLoader: suspend () -> ImageBitmap + val background: ImageBitmap? + + val colorScheme: MediaColorScheme val title: String val subtitle: String - val playPauseAction: MediaPlayPauseActionViewModel + val actionButtonLayout: MediaCardActionButtonLayout + + val playPauseAction: MediaPlayPauseActionViewModel? - val seekBar: MediaSeekBarViewModel + val navigation: MediaNavigationViewModel val additionalActions: List<MediaSecondaryActionViewModel> @@ -53,7 +54,7 @@ interface MediaCardViewModel { val outputSwitcherChips: List<MediaOutputSwitcherChipViewModel> /** Simple icon-only version of the output switcher for use in compact UIs. */ - val outputSwitcherChipButton: MediaSecondaryActionViewModel + val outputSwitcherChipButton: MediaSecondaryActionViewModel.Action val onClick: () -> Unit diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCarouselVisibility.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCarouselVisibility.kt new file mode 100644 index 000000000000..53aa87ce0843 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCarouselVisibility.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.remedia.ui.viewmodel + +/** Enumerates the known rules for media carousel visibility. */ +enum class MediaCarouselVisibility { + + /** The carousel should be shown as long as it has at least one card. */ + WhenNotEmpty, + + /** + * The carousel should be shown as long as it has at least one card that represents an active + * media session. In other words: if all cards in the carousel represent _inactive_ sessions, + * the carousel should _not_ be visible. + */ + WhenAnyCardIsActive, +} diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaNavigationViewModel.kt index f1ced6bf908d..c34c7337290f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSeekBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaNavigationViewModel.kt @@ -18,17 +18,26 @@ package com.android.systemui.media.remedia.ui.viewmodel import androidx.annotation.FloatRange -/** Models UI state for the seek bar. */ -sealed interface MediaSeekBarViewModel { +/** + * Models UI state for the navigation component of the UI (potentially containing the seek bar and + * the buttons to its left and right). + */ +sealed interface MediaNavigationViewModel { /** The seek bar should be showing. */ data class Showing( /** The progress to show on the seek bar, between `0` and `1`. */ @FloatRange(from = 0.0, to = 1.0) val progress: Float, - /** The previous button; or `null` if it should be absent in the UI. */ - val previous: MediaSecondaryActionViewModel?, - /** The next button; or `null` if it should be absent in the UI. */ - val next: MediaSecondaryActionViewModel?, + /** + * The action button to the left of the seek bar; or `null` if it should be absent in the + * UI. + */ + val left: MediaSecondaryActionViewModel, + /** + * The action button to the right of the seek bar; or `null` if it should be absent in the + * UI. + */ + val right: MediaSecondaryActionViewModel, /** * Whether the portion of the seek bar track before the thumb should show the squiggle * animation. @@ -50,8 +59,8 @@ sealed interface MediaSeekBarViewModel { * the seek bar). The position/progress should be committed. */ val onScrubFinished: () -> Unit, - ) : MediaSeekBarViewModel + ) : MediaNavigationViewModel /** The seek bar should be hidden. */ - data object Hidden : MediaSeekBarViewModel + data object Hidden : MediaNavigationViewModel } diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt index 4cb11bc0b8d0..ecc92d778f8e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt @@ -21,8 +21,7 @@ import com.android.systemui.media.remedia.shared.model.MediaSessionState /** Models UI state for the play/pause action button within media controls. */ data class MediaPlayPauseActionViewModel( - val isVisible: Boolean, val state: MediaSessionState, - val icon: Icon.Resource?, - val onClick: () -> Unit, + val icon: Icon?, + val onClick: (() -> Unit)?, ) diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt index a4806800a7b1..d28ca7ab7121 100644 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt @@ -19,8 +19,10 @@ package com.android.systemui.media.remedia.ui.viewmodel import com.android.systemui.common.shared.model.Icon /** Models UI state for a secondary action button within media controls. */ -data class MediaSecondaryActionViewModel( - val icon: Icon, - val isEnabled: Boolean, - val onClick: () -> Unit, -) +sealed interface MediaSecondaryActionViewModel { + data class Action(val icon: Icon, val onClick: (() -> Unit)?) : MediaSecondaryActionViewModel + + data object ReserveSpace : MediaSecondaryActionViewModel + + data object None : MediaSecondaryActionViewModel +} diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt new file mode 100644 index 000000000000..b4f3d2724e75 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.remedia.ui.viewmodel + +import android.content.Context +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.ImageBitmap +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.media.remedia.domain.interactor.MediaInteractor +import com.android.systemui.media.remedia.domain.model.MediaActionModel +import com.android.systemui.media.remedia.shared.model.MediaColorScheme +import com.android.systemui.media.remedia.shared.model.MediaSessionState +import com.android.systemui.res.R +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlin.math.roundToLong +import kotlinx.coroutines.awaitCancellation + +/** Models UI state for a media element. */ +class MediaViewModel +@AssistedInject +constructor( + private val interactor: MediaInteractor, + @Assisted private val context: Context, + @Assisted private val carouselVisibility: MediaCarouselVisibility, +) : ExclusiveActivatable() { + + /** Whether the user is actively moving the thumb of the seek bar. */ + private var isScrubbing: Boolean by mutableStateOf(false) + /** The position of the thumb of the seek bar as the user is scrubbing it. */ + private var seekProgress: Float by mutableFloatStateOf(0f) + /** Whether the internal "guts" are visible. */ + private var isGutsVisible: Boolean by mutableStateOf(false) + /** The index of the currently-selected card. */ + private var selectedCardIndex: Int by mutableIntStateOf(0) + private set + + /** The current list of cards to show in the UI. */ + val cards: List<MediaCardViewModel> by derivedStateOf { + interactor.sessions.mapIndexed { sessionIndex, session -> + val isCurrentSessionAndScrubbing = isScrubbing && sessionIndex == selectedCardIndex + object : MediaCardViewModel { + override val key = session.key + override val icon = session.appIcon + override val background: ImageBitmap? + get() = session.background + + override val colorScheme: MediaColorScheme + get() = session.colorScheme + + override val title = session.title + override val subtitle = session.subtitle + override val actionButtonLayout = session.actionButtonLayout + override val playPauseAction = + session.playPauseAction.toPlayPauseActionViewModel(session.state) + override val additionalActions: List<MediaSecondaryActionViewModel> + get() { + return session.additionalActions.map { action -> + action.toSecondaryActionViewModel() + } + } + + override val navigation: MediaNavigationViewModel + get() { + return if (session.canBeScrubbed) { + MediaNavigationViewModel.Showing( + progress = + if (!isCurrentSessionAndScrubbing) { + session.positionMs.toFloat() / session.durationMs + } else { + seekProgress + }, + left = session.leftAction.toSecondaryActionViewModel(), + right = session.rightAction.toSecondaryActionViewModel(), + isSquiggly = + session.state != MediaSessionState.Paused && + !isCurrentSessionAndScrubbing, + isScrubbing = isCurrentSessionAndScrubbing, + onScrubChange = { progress -> + check(selectedCardIndex == sessionIndex) { + "Can't seek on a card that's not the selected card!" + } + isScrubbing = true + seekProgress = progress + }, + onScrubFinished = { + interactor.seek( + sessionKey = session.key, + to = (seekProgress * session.durationMs).roundToLong(), + ) + isScrubbing = false + }, + ) + } else { + MediaNavigationViewModel.Hidden + } + } + + override val guts: MediaCardGutsViewModel + get() { + return MediaCardGutsViewModel( + isVisible = isGutsVisible, + text = + if (session.canBeHidden) { + context.getString( + R.string.controls_media_close_session, + session.appName, + ) + } else { + context.getString(R.string.controls_media_active_session) + }, + primaryAction = + if (session.canBeHidden) { + MediaGutsButtonViewModel( + text = + context.getString( + R.string.controls_media_dismiss_button + ), + onClick = { + interactor.hide(session.key) + isGutsVisible = false + }, + ) + } else { + MediaGutsButtonViewModel( + text = context.getString(R.string.cancel), + onClick = { isGutsVisible = false }, + ) + }, + secondaryAction = + MediaGutsButtonViewModel( + text = context.getString(R.string.cancel), + onClick = { isGutsVisible = false }, + ) + .takeIf { session.canBeHidden }, + settingsButton = + MediaGutsSettingsButtonViewModel( + icon = + Icon.Resource( + res = R.drawable.ic_settings, + contentDescription = + ContentDescription.Resource( + res = R.string.controls_media_settings_button + ), + ), + onClick = { interactor.openMediaSettings() }, + ), + onLongClick = { isGutsVisible = false }, + ) + } + + override val outputSwitcherChips: List<MediaOutputSwitcherChipViewModel> + get() { + return listOf( + MediaOutputSwitcherChipViewModel( + icon = session.outputDevice.icon, + text = session.outputDevice.name, + onClick = { + // TODO(b/397989775): tell the UI to show the output switcher. + }, + ) + ) + } + + override val outputSwitcherChipButton: MediaSecondaryActionViewModel.Action + get() { + return MediaSecondaryActionViewModel.Action( + icon = session.outputDevice.icon, + onClick = { + // TODO(b/397989775): tell the UI to show the output switcher. + }, + ) + } + + override val onClick = session.onClick + override val onClickLabel = + context.getString(R.string.controls_media_playing_item_description) + override val onLongClick = { isGutsVisible = true } + } + } + } + + /** Whether the carousel should be visible. */ + val isCarouselVisible: Boolean + get() = + when (carouselVisibility) { + MediaCarouselVisibility.WhenNotEmpty -> interactor.sessions.isNotEmpty() + + MediaCarouselVisibility.WhenAnyCardIsActive -> + interactor.sessions.any { session -> session.isActive } + } + + /** Notifies that the card at [cardIndex] has been selected in the UI. */ + fun onCardSelected(cardIndex: Int) { + check(cardIndex >= 0 && cardIndex < cards.size) + selectedCardIndex = cardIndex + } + + override suspend fun onActivated(): Nothing { + awaitCancellation() + } + + private fun MediaActionModel.toPlayPauseActionViewModel( + mediaSessionState: MediaSessionState + ): MediaPlayPauseActionViewModel? { + return when (this) { + is MediaActionModel.Action -> + MediaPlayPauseActionViewModel( + state = mediaSessionState, + icon = icon, + onClick = onClick ?: {}, + ) + is MediaActionModel.None, + is MediaActionModel.ReserveSpace -> null + } + } + + private fun MediaActionModel.toSecondaryActionViewModel(): MediaSecondaryActionViewModel { + return when (this) { + is MediaActionModel.Action -> + MediaSecondaryActionViewModel.Action(icon = icon, onClick = onClick) + is MediaActionModel.ReserveSpace -> MediaSecondaryActionViewModel.ReserveSpace + is MediaActionModel.None -> MediaSecondaryActionViewModel.None + } + } + + @AssistedFactory + interface Factory { + fun create(context: Context, carouselVisibility: MediaCarouselVisibility): MediaViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt index d8fc52bcc55a..8dc27bf4ac3e 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt @@ -162,10 +162,6 @@ constructor( ): Boolean { return this@NoteTaskInitializer.handleKeyGestureEvent(event) } - - override fun isKeyGestureSupported(gestureType: Int): Boolean { - return this@NoteTaskInitializer.isKeyGestureSupported(gestureType) - } } /** @@ -225,10 +221,6 @@ constructor( return true } - private fun isKeyGestureSupported(gestureType: Int): Boolean { - return gestureType == KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES - } - companion object { val MULTI_PRESS_TIMEOUT = ViewConfiguration.getMultiPressTimeout().toLong() val LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout().toLong() 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 f1f5b267f9c1..8920c86282da 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -18,6 +18,7 @@ package com.android.systemui.qs.composefragment import android.annotation.SuppressLint import android.content.Context +import android.content.res.Configuration import android.graphics.PointF import android.graphics.Rect import android.os.Bundle @@ -48,6 +49,7 @@ import androidx.compose.foundation.layout.requiredHeightIn import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -58,6 +60,7 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerInputChange @@ -69,6 +72,8 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.layout.positionOnScreen import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.CustomAccessibilityAction @@ -102,6 +107,7 @@ import com.android.mechanics.GestureContext import com.android.systemui.Dumpable import com.android.systemui.Flags import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer +import com.android.systemui.brightness.ui.compose.ContainerColors import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dump.DumpManager import com.android.systemui.keyboard.shortcut.ui.composable.InteractionsConfig @@ -249,7 +255,7 @@ constructor( @Composable private fun Content() { - PlatformTheme(isDarkTheme = true) { + PlatformTheme(isDarkTheme = true /* Delete AlwaysDarkMode when removing this */) { ProvideShortcutHelperIndication(interactionsConfig = interactionsConfig()) { // TODO(b/389985793): Make sure that there is no coroutine work or recompositions // happening when alwaysCompose is true but isQsVisibleAndAnyShadeExpanded is false. @@ -740,17 +746,26 @@ constructor( ) val BrightnessSlider = @Composable { - BrightnessSliderContainer( - viewModel = containerViewModel.brightnessSliderViewModel, - modifier = + AlwaysDarkMode { + Box( Modifier.systemGestureExclusionInShade( - enabled = { - layoutState.transitionState is - TransitionState.Idle - } - ) - .fillMaxWidth(), - ) + enabled = { + layoutState.transitionState is TransitionState.Idle + } + ) + ) { + BrightnessSliderContainer( + viewModel = + containerViewModel.brightnessSliderViewModel, + containerColors = + ContainerColors( + Color.Transparent, + ContainerColors.defaultContainerColor, + ), + modifier = Modifier.fillMaxWidth(), + ) + } + } } val TileGrid = @Composable { @@ -1226,3 +1241,28 @@ private fun interactionsConfig() = private inline val alwaysCompose get() = Flags.alwaysComposeQsUiFragment() + +/** + * Forces the configuration and themes to be dark theme. This is needed in order to have + * [colorResource] retrieve the dark mode colors. + * + * This should be removed when we remove the force dark mode in [PlatformTheme] at the root of the + * compose hierarchy. + */ +@Composable +private fun AlwaysDarkMode(content: @Composable () -> Unit) { + val currentConfig = LocalConfiguration.current + val darkConfig = + Configuration(currentConfig).apply { + uiMode = + (uiMode and (Configuration.UI_MODE_NIGHT_MASK.inv())) or + Configuration.UI_MODE_NIGHT_YES + } + val newContext = LocalContext.current.createConfigurationContext(darkConfig) + CompositionLocalProvider( + LocalConfiguration provides darkConfig, + LocalContext provides newContext, + ) { + content() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegate.kt index 446be9b9ebcb..59844c7ae664 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegate.kt @@ -85,6 +85,7 @@ constructor( LargeStaticTile( uiState = viewModel.uiState, + iconProvider = viewModel.iconProvider, modifier = Modifier.width( dimensionResource( diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModel.kt index c756adc07ba4..dd281ccc9c50 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModel.kt @@ -26,6 +26,7 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.external.TileData +import com.android.systemui.qs.panels.ui.viewmodel.toIconProvider import com.android.systemui.qs.panels.ui.viewmodel.toUiState import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon @@ -58,6 +59,8 @@ constructor( val uiState by derivedStateOf { state.toUiState(dialogContext.resources) } + val iconProvider by derivedStateOf { state.toIconProvider() } + override suspend fun onActivated(): Nothing { withContext(backgroundDispatcher) { tileData.icon diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt index 701f44e9981c..d40ecc9565ae 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt @@ -61,6 +61,9 @@ fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewMode DisposableEffect(Unit) { onDispose { detailsViewModel.closeDetailedView() } } + val title = tileDetailedViewModel.title + val subTitle = tileDetailedViewModel.subTitle + Column( modifier = modifier @@ -90,7 +93,7 @@ fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewMode ) } Text( - text = tileDetailedViewModel.getTitle(), + text = title, modifier = Modifier.align(Alignment.CenterVertically), textAlign = TextAlign.Center, style = MaterialTheme.typography.titleLarge, @@ -110,7 +113,7 @@ fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewMode } } Text( - text = tileDetailedViewModel.getSubTitle(), + text = subTitle, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, style = MaterialTheme.typography.titleSmall, 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 a56fabcc7dc3..bf63c3858542 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 @@ -20,6 +20,7 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid import android.content.Context import android.content.res.Resources +import android.os.Trace import android.service.quicksettings.Tile.STATE_ACTIVE import android.service.quicksettings.Tile.STATE_INACTIVE import androidx.compose.animation.animateColorAsState @@ -47,8 +48,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState @@ -59,7 +60,7 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role @@ -69,6 +70,7 @@ import androidx.compose.ui.semantics.toggleableState import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.trace import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.Expandable import com.android.compose.animation.bounceable @@ -80,7 +82,6 @@ import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.haptics.msdl.qs.TileHapticsViewModel import com.android.systemui.haptics.msdl.qs.TileHapticsViewModelFactoryProvider import com.android.systemui.lifecycle.rememberViewModel -import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.flags.QsDetailedView import com.android.systemui.qs.panels.ui.compose.BounceableInfo import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius @@ -88,9 +89,12 @@ import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileHeight import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileStartPadding import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel +import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState import com.android.systemui.qs.panels.ui.viewmodel.DetailsViewModel +import com.android.systemui.qs.panels.ui.viewmodel.IconProvider import com.android.systemui.qs.panels.ui.viewmodel.TileUiState import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel +import com.android.systemui.qs.panels.ui.viewmodel.toIconProvider import com.android.systemui.qs.panels.ui.viewmodel.toUiState import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.qs.ui.compose.borderOnFocus @@ -119,6 +123,9 @@ fun TileLazyGrid( ) } +private val TileViewModel.traceName + get() = spec.toString().takeLast(Trace.MAX_SECTION_NAME_LEN) + @Composable fun Tile( tile: TileViewModel, @@ -130,105 +137,114 @@ fun Tile( modifier: Modifier = Modifier, detailsViewModel: DetailsViewModel?, ) { - val currentBounceableInfo by rememberUpdatedState(bounceableInfo) - val resources = resources() - - /* - * Use produce state because [QSTile.State] doesn't have well defined equals (due to - * inheritance). This way, even if tile.state changes, uiState may not change and lead to - * recomposition. - */ - val uiState by - produceState(tile.currentState.toUiState(resources), tile, resources) { - tile.state.collect { value = it.toUiState(resources) } - } + trace(tile.traceName) { + val currentBounceableInfo by rememberUpdatedState(bounceableInfo) + val resources = resources() + + /* + * Use produce state because [QSTile.State] doesn't have well defined equals (due to + * inheritance). This way, even if tile.state changes, uiState may not change and lead to + * recomposition. + */ + val uiState by + produceState(tile.currentState.toUiState(resources), tile, resources) { + tile.state.collect { value = it.toUiState(resources) } + } - val colors = TileDefaults.getColorForState(uiState, iconOnly) - val hapticsViewModel: TileHapticsViewModel? = - rememberViewModel(traceName = "TileHapticsViewModel") { - tileHapticsViewModelFactoryProvider.getHapticsViewModelFactory()?.create(tile) - } + val icon by + produceState(tile.currentState.toIconProvider(), tile) { + tile.state.collect { value = it.toIconProvider() } + } - // TODO(b/361789146): Draw the shapes instead of clipping - val tileShape by TileDefaults.animateTileShapeAsState(uiState.state) - val animatedColor by animateColorAsState(colors.background, label = "QSTileBackgroundColor") + val colors = TileDefaults.getColorForState(uiState, iconOnly) + val hapticsViewModel: TileHapticsViewModel? = + rememberViewModel(traceName = "TileHapticsViewModel") { + tileHapticsViewModelFactoryProvider.getHapticsViewModelFactory()?.create(tile) + } - TileExpandable( - color = { animatedColor }, - shape = tileShape, - squishiness = squishiness, - hapticsViewModel = hapticsViewModel, - modifier = - modifier - .borderOnFocus(color = MaterialTheme.colorScheme.secondary, tileShape.topEnd) - .fillMaxWidth() - .bounceable( - bounceable = currentBounceableInfo.bounceable, - previousBounceable = currentBounceableInfo.previousTile, - nextBounceable = currentBounceableInfo.nextTile, - orientation = Orientation.Horizontal, - bounceEnd = currentBounceableInfo.bounceEnd, - ), - ) { expandable -> - val longClick: (() -> Unit)? = - { - hapticsViewModel?.setTileInteractionState( - TileHapticsViewModel.TileInteractionState.LONG_CLICKED + // TODO(b/361789146): Draw the shapes instead of clipping + val tileShape by TileDefaults.animateTileShapeAsState(uiState.state) + val animatedColor by animateColorAsState(colors.background, label = "QSTileBackgroundColor") + + TileExpandable( + color = { animatedColor }, + shape = tileShape, + squishiness = squishiness, + hapticsViewModel = hapticsViewModel, + modifier = + modifier + .borderOnFocus(color = MaterialTheme.colorScheme.secondary, tileShape.topEnd) + .fillMaxWidth() + .bounceable( + bounceable = currentBounceableInfo.bounceable, + previousBounceable = currentBounceableInfo.previousTile, + nextBounceable = currentBounceableInfo.nextTile, + orientation = Orientation.Horizontal, + bounceEnd = currentBounceableInfo.bounceEnd, + ), + ) { expandable -> + val longClick: (() -> Unit)? = + { + hapticsViewModel?.setTileInteractionState( + TileHapticsViewModel.TileInteractionState.LONG_CLICKED + ) + tile.onLongClick(expandable) + } + .takeIf { uiState.handlesLongClick } + TileContainer( + onClick = { + var hasDetails = false + if (QsDetailedView.isEnabled) { + hasDetails = detailsViewModel?.onTileClicked(tile.spec) == true + } + if (!hasDetails) { + // For those tile's who doesn't have a detailed view, process with their + // `onClick` behavior. + tile.onClick(expandable) + hapticsViewModel?.setTileInteractionState( + TileHapticsViewModel.TileInteractionState.CLICKED + ) + if (uiState.accessibilityUiState.toggleableState != null) { + coroutineScope.launch { + currentBounceableInfo.bounceable.animateBounce() + } + } + } + }, + onLongClick = longClick, + accessibilityUiState = uiState.accessibilityUiState, + iconOnly = iconOnly, + ) { + val iconProvider: Context.() -> Icon = { getTileIcon(icon = icon) } + if (iconOnly) { + SmallTileContent( + iconProvider = iconProvider, + color = colors.icon, + modifier = Modifier.align(Alignment.Center), ) - tile.onLongClick(expandable) - } - .takeIf { uiState.handlesLongClick } - TileContainer( - onClick = { - var hasDetails = false - if (QsDetailedView.isEnabled) { - hasDetails = detailsViewModel?.onTileClicked(tile.spec) == true - } - if (!hasDetails) { - // For those tile's who doesn't have a detailed view, process with their - // `onClick` behavior. - tile.onClick(expandable) - hapticsViewModel?.setTileInteractionState( - TileHapticsViewModel.TileInteractionState.CLICKED + } else { + val iconShape by TileDefaults.animateIconShapeAsState(uiState.state) + val secondaryClick: (() -> Unit)? = + { + hapticsViewModel?.setTileInteractionState( + TileHapticsViewModel.TileInteractionState.CLICKED + ) + tile.onSecondaryClick() + } + .takeIf { uiState.handlesSecondaryClick } + LargeTileContent( + label = uiState.label, + secondaryLabel = uiState.secondaryLabel, + iconProvider = iconProvider, + sideDrawable = uiState.sideDrawable, + colors = colors, + iconShape = iconShape, + toggleClick = secondaryClick, + onLongClick = longClick, + accessibilityUiState = uiState.accessibilityUiState, + squishiness = squishiness, ) - if (uiState.accessibilityUiState.toggleableState != null) { - coroutineScope.launch { currentBounceableInfo.bounceable.animateBounce() } - } } - }, - onLongClick = longClick, - uiState = uiState, - iconOnly = iconOnly, - ) { - val iconProvider: Context.() -> Icon = { getTileIcon(icon = uiState.icon) } - if (iconOnly) { - SmallTileContent( - iconProvider = iconProvider, - color = colors.icon, - modifier = Modifier.align(Alignment.Center), - ) - } else { - val iconShape by TileDefaults.animateIconShapeAsState(uiState.state) - val secondaryClick: (() -> Unit)? = - { - hapticsViewModel?.setTileInteractionState( - TileHapticsViewModel.TileInteractionState.CLICKED - ) - tile.onSecondaryClick() - } - .takeIf { uiState.handlesSecondaryClick } - LargeTileContent( - label = uiState.label, - secondaryLabel = uiState.secondaryLabel, - iconProvider = iconProvider, - sideDrawable = uiState.sideDrawable, - colors = colors, - iconShape = iconShape, - toggleClick = secondaryClick, - onLongClick = longClick, - accessibilityUiState = uiState.accessibilityUiState, - squishiness = squishiness, - ) } } } @@ -257,7 +273,7 @@ private fun TileExpandable( fun TileContainer( onClick: () -> Unit, onLongClick: (() -> Unit)?, - uiState: TileUiState, + accessibilityUiState: AccessibilityUiState, iconOnly: Boolean, content: @Composable BoxScope.() -> Unit, ) { @@ -268,7 +284,7 @@ fun TileContainer( .tileCombinedClickable( onClick = onClick, onLongClick = onLongClick, - uiState = uiState, + accessibilityUiState = accessibilityUiState, iconOnly = iconOnly, ) .sysuiResTag(if (iconOnly) TEST_TAG_SMALL else TEST_TAG_LARGE) @@ -278,7 +294,11 @@ fun TileContainer( } @Composable -fun LargeStaticTile(uiState: TileUiState, modifier: Modifier = Modifier) { +fun LargeStaticTile( + uiState: TileUiState, + iconProvider: IconProvider, + modifier: Modifier = Modifier, +) { val colors = TileDefaults.getColorForState(uiState = uiState, iconOnly = false) Box( @@ -291,7 +311,7 @@ fun LargeStaticTile(uiState: TileUiState, modifier: Modifier = Modifier) { LargeTileContent( label = uiState.label, secondaryLabel = "", - iconProvider = { getTileIcon(icon = uiState.icon) }, + iconProvider = { getTileIcon(icon = iconProvider) }, sideDrawable = null, colors = colors, squishiness = { 1f }, @@ -299,8 +319,8 @@ fun LargeStaticTile(uiState: TileUiState, modifier: Modifier = Modifier) { } } -private fun Context.getTileIcon(icon: QSTile.Icon?): Icon { - return icon?.let { +private fun Context.getTileIcon(icon: IconProvider): Icon { + return icon.icon?.let { if (it is QSTileImpl.ResourceIcon) { Icon.Resource(it.resId, null) } else { @@ -321,28 +341,26 @@ fun Modifier.largeTilePadding(): Modifier { fun Modifier.tileCombinedClickable( onClick: () -> Unit, onLongClick: (() -> Unit)?, - uiState: TileUiState, + accessibilityUiState: AccessibilityUiState, iconOnly: Boolean, ): Modifier { val longPressLabel = longPressLabel() return combinedClickable( onClick = onClick, onLongClick = onLongClick, - onClickLabel = uiState.accessibilityUiState.clickLabel, + onClickLabel = accessibilityUiState.clickLabel, onLongClickLabel = longPressLabel, hapticFeedbackEnabled = !Flags.msdlFeedback(), ) .semantics { - role = uiState.accessibilityUiState.accessibilityRole - if (uiState.accessibilityUiState.accessibilityRole == Role.Switch) { - uiState.accessibilityUiState.toggleableState?.let { toggleableState = it } + role = accessibilityUiState.accessibilityRole + if (accessibilityUiState.accessibilityRole == Role.Switch) { + accessibilityUiState.toggleableState?.let { toggleableState = it } } - stateDescription = uiState.accessibilityUiState.stateDescription + stateDescription = accessibilityUiState.stateDescription } .thenIf(iconOnly) { - Modifier.semantics { - contentDescription = uiState.accessibilityUiState.contentDescription - } + Modifier.semantics { contentDescription = accessibilityUiState.contentDescription } } } @@ -474,14 +492,15 @@ private object TileDefaults { label = label, ) - val corner = remember { - object : CornerSize { - override fun toPx(shapeSize: Size, density: Density): Float { - return with(density) { animatedCornerRadius.toPx() } + return remember { + val corner = + object : CornerSize { + override fun toPx(shapeSize: Size, density: Density): Float { + return with(density) { animatedCornerRadius.toPx() } + } } - } + mutableStateOf(RoundedCornerShape(corner)) } - return remember { derivedStateOf { RoundedCornerShape(corner) } } } } @@ -493,5 +512,5 @@ private object TileDefaults { @ReadOnlyComposable private fun resources(): Resources { LocalConfiguration.current - return LocalContext.current.resources + return LocalResources.current } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt index 153238fc91c9..a66b51f6fe50 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt @@ -59,12 +59,14 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.toSize import androidx.compose.ui.zIndex import com.android.compose.modifiers.size import com.android.compose.modifiers.thenIf import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BADGE_ANGLE_RAD +import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeIconSize import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeSize import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeXOffset import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeYOffset @@ -149,16 +151,14 @@ fun InteractiveTileContainer( onClick = onClick, ) ) { + val size = with(LocalDensity.current) { BadgeIconSize.toDp() } Icon( Icons.Default.Remove, contentDescription = null, modifier = - Modifier.size( - width = { decorationSize.width.roundToInt() }, - height = { decorationSize.height.roundToInt() }, - ) - .align(Alignment.Center) - .graphicsLayer { this.alpha = badgeIconAlpha }, + Modifier.size(size).align(Alignment.Center).graphicsLayer { + this.alpha = badgeIconAlpha + }, ) } } @@ -219,12 +219,13 @@ fun StaticTileBadge( } ) { val secondaryColor = MaterialTheme.colorScheme.secondary + val size = with(LocalDensity.current) { BadgeIconSize.toDp() } Icon( icon, contentDescription = contentDescription, modifier = - Modifier.size(BadgeSize).align(Alignment.Center).drawBehind { - drawCircle(secondaryColor) + Modifier.size(size).align(Alignment.Center).drawBehind { + drawCircle(secondaryColor, radius = BadgeSize.toPx() / 2) }, ) } @@ -338,6 +339,7 @@ private fun offsetForAngle(angle: Float, radius: Float, center: Offset): Offset private object SelectionDefaults { val SelectedBorderWidth = 2.dp val BadgeSize = 24.dp + val BadgeIconSize = 16.sp val BadgeXOffset = -4.dp val BadgeYOffset = 4.dp val ResizingPillWidth = 8.dp diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt index 99f52c28a137..3ae90d2f976b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt @@ -16,14 +16,20 @@ package com.android.systemui.qs.panels.ui.compose.toolbar +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.android.systemui.compose.modifiers.sysuiResTag +import com.android.systemui.development.ui.compose.BuildNumber import com.android.systemui.qs.footer.ui.compose.IconButton import com.android.systemui.qs.panels.ui.viewmodel.toolbar.ToolbarViewModel +import com.android.systemui.qs.ui.compose.borderOnFocus @Composable fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) { @@ -44,7 +50,18 @@ fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) { Modifier.sysuiResTag("settings_button_container"), ) - Spacer(modifier = Modifier.weight(1f)) + Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) { + BuildNumber( + viewModelFactory = viewModel.buildNumberViewModelFactory, + textColor = MaterialTheme.colorScheme.onSurface, + modifier = + Modifier.borderOnFocus( + color = MaterialTheme.colorScheme.secondary, + cornerSize = CornerSize(1.dp), + ) + .wrapContentSize(), + ) + } IconButton( { viewModel.powerButtonViewModel }, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModel.kt index 03f0297e0d54..3287443f0405 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.panels.ui.viewmodel +import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import com.android.systemui.dagger.SysUISingleton @@ -25,6 +26,7 @@ import com.android.systemui.qs.pipeline.shared.TileSpec import javax.inject.Inject @SysUISingleton +@Stable class DetailsViewModel @Inject constructor(val currentTilesInteractor: CurrentTilesInteractor) { /** diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt index 19e542e6a21e..15e71c88bea1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt @@ -22,12 +22,18 @@ import android.service.quicksettings.Tile import android.text.TextUtils import android.widget.Switch import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable import androidx.compose.ui.semantics.Role import androidx.compose.ui.state.ToggleableState import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.tileimpl.SubtitleArrayMapping import com.android.systemui.res.R +import java.util.function.Supplier +/** + * Ui State for the tiles. It doesn't contain the icon to be able to invalidate the icon part + * separately. For the icon, use [IconProvider]. + */ @Immutable data class TileUiState( val label: String, @@ -35,7 +41,6 @@ data class TileUiState( val state: Int, val handlesLongClick: Boolean, val handlesSecondaryClick: Boolean, - val icon: QSTile.Icon?, val sideDrawable: Drawable?, val accessibilityUiState: AccessibilityUiState, ) @@ -90,7 +95,6 @@ fun QSTile.State.toUiState(resources: Resources): TileUiState { state = if (disabledByPolicy) Tile.STATE_UNAVAILABLE else state, handlesLongClick = handlesLongClick, handlesSecondaryClick = handlesSecondaryClick, - icon = icon ?: iconSupplier?.get(), sideDrawable = sideViewCustomDrawable, AccessibilityUiState( contentDescription?.toString() ?: "", @@ -104,6 +108,14 @@ fun QSTile.State.toUiState(resources: Resources): TileUiState { ) } +fun QSTile.State.toIconProvider(): IconProvider { + return when { + icon != null -> IconProvider.ConstantIcon(icon) + iconSupplier != null -> IconProvider.IconSupplier(iconSupplier) + else -> IconProvider.Empty + } +} + private fun QSTile.State.getStateText(resources: Resources): CharSequence { val arrayResId = SubtitleArrayMapping.getSubtitleId(spec) val array = resources.getStringArray(arrayResId) @@ -114,3 +126,21 @@ private fun getUnavailableText(spec: String?, resources: Resources): String { val arrayResId = SubtitleArrayMapping.getSubtitleId(spec) return resources.getStringArray(arrayResId)[Tile.STATE_UNAVAILABLE] } + +@Stable +sealed interface IconProvider { + + val icon: QSTile.Icon? + + data class ConstantIcon(override val icon: QSTile.Icon) : IconProvider + + data class IconSupplier(val supplier: Supplier<QSTile.Icon?>) : IconProvider { + override val icon: QSTile.Icon? + get() = supplier.get() + } + + data object Empty : IconProvider { + override val icon: QSTile.Icon? + get() = null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt index e54bfa29d2db..10d7871b8ea2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.setValue import com.android.systemui.animation.Expandable import com.android.systemui.classifier.domain.interactor.FalsingInteractor import com.android.systemui.classifier.domain.interactor.runIfNotFalseTap +import com.android.systemui.development.ui.viewmodel.BuildNumberViewModel import com.android.systemui.globalactions.GlobalActionsDialogLite import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator @@ -46,6 +47,7 @@ class ToolbarViewModel @AssistedInject constructor( editModeButtonViewModelFactory: EditModeButtonViewModel.Factory, + val buildNumberViewModelFactory: BuildNumberViewModel.Factory, private val footerActionsInteractor: FooterActionsInteractor, private val globalActionsDialogLiteProvider: Provider<GlobalActionsDialogLite>, private val falsingInteractor: FalsingInteractor, diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt index 7d396c58630e..8ffba1e5f3dd 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt @@ -19,26 +19,14 @@ package com.android.systemui.qs.tiles.dialog import android.view.LayoutInflater import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.viewinterop.AndroidView import com.android.systemui.res.R @Composable fun InternetDetailsContent(viewModel: InternetDetailsViewModel) { val coroutineScope = rememberCoroutineScope() - val context = LocalContext.current - - val internetDetailsContentManager = remember { - viewModel.contentManagerFactory.create( - canConfigMobileData = viewModel.getCanConfigMobileData(), - canConfigWifi = viewModel.getCanConfigWifi(), - coroutineScope = coroutineScope, - context = context, - ) - } AndroidView( modifier = Modifier.fillMaxSize(), @@ -46,11 +34,11 @@ fun InternetDetailsContent(viewModel: InternetDetailsViewModel) { // Inflate with the existing dialog xml layout and bind it with the manager val view = LayoutInflater.from(context).inflate(R.layout.internet_connectivity_dialog, null) - internetDetailsContentManager.bind(view) + viewModel.internetDetailsContentManager.bind(view, coroutineScope) view // TODO: b/377388104 - Polish the internet details view UI }, - onRelease = { internetDetailsContentManager.unBind() }, + onRelease = { viewModel.internetDetailsContentManager.unBind() }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt index 659488bdd0d3..d8e1755e6cca 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt @@ -43,6 +43,9 @@ import android.widget.Switch import android.widget.TextView import androidx.annotation.MainThread import androidx.annotation.WorkerThread +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry @@ -79,8 +82,6 @@ constructor( private val internetDetailsContentController: InternetDetailsContentController, @Assisted(CAN_CONFIG_MOBILE_DATA) private val canConfigMobileData: Boolean, @Assisted(CAN_CONFIG_WIFI) private val canConfigWifi: Boolean, - @Assisted private val coroutineScope: CoroutineScope, - @Assisted private var context: Context, private val uiEventLogger: UiEventLogger, @Main private val handler: Handler, @Background private val backgroundExecutor: Executor, @@ -121,26 +122,29 @@ constructor( private lateinit var shareWifiButton: Button private lateinit var airplaneModeButton: Button private var alertDialog: AlertDialog? = null - - private val canChangeWifiState = - WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(context) + private var canChangeWifiState = false private var wifiNetworkHeight = 0 private var backgroundOn: Drawable? = null private var backgroundOff: Drawable? = null private var clickJob: Job? = null private var defaultDataSubId = internetDetailsContentController.defaultDataSubscriptionId - @VisibleForTesting - internal var adapter = InternetAdapter(internetDetailsContentController, coroutineScope) + @VisibleForTesting internal lateinit var adapter: InternetAdapter @VisibleForTesting internal var wifiEntriesCount: Int = 0 @VisibleForTesting internal var hasMoreWifiEntries: Boolean = false + private lateinit var context: Context + private lateinit var coroutineScope: CoroutineScope + + var title by mutableStateOf("") + private set + + var subTitle by mutableStateOf("") + private set @AssistedFactory interface Factory { fun create( @Assisted(CAN_CONFIG_MOBILE_DATA) canConfigMobileData: Boolean, @Assisted(CAN_CONFIG_WIFI) canConfigWifi: Boolean, - coroutineScope: CoroutineScope, - context: Context, ): InternetDetailsContentManager } @@ -152,12 +156,16 @@ constructor( * * @param contentView The view to which the content manager should be bound. */ - fun bind(contentView: View) { + fun bind(contentView: View, coroutineScope: CoroutineScope) { if (DEBUG) { Log.d(TAG, "Bind InternetDetailsContentManager") } this.contentView = contentView + context = contentView.context + this.coroutineScope = coroutineScope + adapter = InternetAdapter(internetDetailsContentController, coroutineScope) + canChangeWifiState = WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(context) initializeLifecycle() initializeViews() @@ -323,11 +331,11 @@ constructor( } } - fun getTitleText(): String { + private fun getTitleText(): String { return internetDetailsContentController.getDialogTitleText().toString() } - fun getSubtitleText(): String { + private fun getSubtitleText(): String { return internetDetailsContentController.getSubtitleText(isProgressBarVisible).toString() } @@ -336,6 +344,13 @@ constructor( Log.d(TAG, "updateDetailsUI ") } + if (!::context.isInitialized) { + return + } + + title = getTitleText() + subTitle = getSubtitleText() + airplaneModeButton.visibility = if (internetContent.isAirplaneModeEnabled) View.VISIBLE else View.GONE diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt index 6709fd2bb508..fb63bea4fb9f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt @@ -28,38 +28,27 @@ class InternetDetailsViewModel @AssistedInject constructor( private val accessPointController: AccessPointController, - val contentManagerFactory: InternetDetailsContentManager.Factory, + private val contentManagerFactory: InternetDetailsContentManager.Factory, private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, -) : TileDetailsViewModel() { - override fun clickOnSettingsButton() { - qsTileIntentUserActionHandler.handle( - /* expandable= */ null, - Intent(Settings.ACTION_WIFI_SETTINGS), +) : TileDetailsViewModel { + val internetDetailsContentManager by lazy { + contentManagerFactory.create( + canConfigMobileData = accessPointController.canConfigMobileData(), + canConfigWifi = accessPointController.canConfigWifi(), ) } - override fun getTitle(): String { - // TODO: b/377388104 make title and sub title mutable states of string - // by internetDetailsContentManager.getTitleText() - // TODO: test title change between airplane mode and not airplane mode - // TODO: b/377388104 Update the placeholder text - return "Internet" - } + override val title: String + get() = internetDetailsContentManager.title - override fun getSubTitle(): String { - // TODO: b/377388104 make title and sub title mutable states of string - // by internetDetailsContentManager.getSubtitleText() - // TODO: test subtitle change between airplane mode and not airplane mode - // TODO: b/377388104 Update the placeholder text - return "Tab a network to connect" - } + override val subTitle: String + get() = internetDetailsContentManager.subTitle - fun getCanConfigMobileData(): Boolean { - return accessPointController.canConfigMobileData() - } - - fun getCanConfigWifi(): Boolean { - return accessPointController.canConfigWifi() + override fun clickOnSettingsButton() { + qsTileIntentUserActionHandler.handle( + /* expandable= */ null, + Intent(Settings.ACTION_WIFI_SETTINGS), + ) } @AssistedFactory diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt index 9a39c3c095ef..4f7e03bd3bc3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt @@ -23,18 +23,14 @@ import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogView class ModesDetailsViewModel( private val onSettingsClick: () -> Unit, val viewModel: ModesDialogViewModel, -) : TileDetailsViewModel() { +) : TileDetailsViewModel { override fun clickOnSettingsButton() { onSettingsClick() } - override fun getTitle(): String { - // TODO(b/388321032): Replace this string with a string in a translatable xml file. - return "Modes" - } + // TODO(b/388321032): Replace this string with a string in a translatable xml file. + override val title = "Modes" - override fun getSubTitle(): String { - // TODO(b/388321032): Replace this string with a string in a translatable xml file. - return "Silences interruptions from people and apps in different circumstances" - } + // TODO(b/388321032): Replace this string with a string in a translatable xml file. + override val subTitle = "Silences interruptions from people and apps in different circumstances" } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt index c84ddb6fdb36..59f209edb546 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt @@ -23,19 +23,15 @@ import com.android.systemui.screenrecord.RecordingController class ScreenRecordDetailsViewModel( val recordingController: RecordingController, val onStartRecordingClicked: Runnable, -) : TileDetailsViewModel() { +) : TileDetailsViewModel { override fun clickOnSettingsButton() { // No settings button in this tile. } - override fun getTitle(): String { - // TODO(b/388321032): Replace this string with a string in a translatable xml file, - return "Screen recording" - } + // TODO(b/388321032): Replace this string with a string in a translatable xml file, + override val title = "Screen recording" - override fun getSubTitle(): String { - // No sub-title in this tile. - return "" - } + // No sub-title in this tile. + override val subTitle = "" } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt index 5e7172ee3ba7..268efcef9062 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt @@ -21,6 +21,7 @@ import android.provider.Settings import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager import com.android.systemui.accessibility.hearingaid.HearingDevicesUiEventLogger.Companion.LAUNCH_SOURCE_QS_TILE import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.shared.QSSettingsPackageRepository import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.interactor.QSTileInput import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor @@ -37,6 +38,7 @@ constructor( @Main private val mainContext: CoroutineContext, private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, private val hearingDevicesDialogManager: HearingDevicesDialogManager, + private val settingsPackageRepository: QSSettingsPackageRepository, ) : QSTileUserActionInteractor<HearingDevicesTileModel> { override suspend fun handleInput(input: QSTileInput<HearingDevicesTileModel>) = @@ -53,7 +55,8 @@ constructor( is QSTileUserAction.LongClick -> { qsTileIntentUserActionHandler.handle( action.expandable, - Intent(Settings.ACTION_HEARING_DEVICES_SETTINGS), + Intent(Settings.ACTION_HEARING_DEVICES_SETTINGS) + .setPackage(settingsPackageRepository.getSettingsPackageName()), ) } is QSTileUserAction.ToggleClick -> {} diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 4753b9ac0457..7bb831baec20 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -687,7 +687,7 @@ constructor( if (!isDeviceEntered) { coroutineScope { launch { - deviceEntryHapticsInteractor.playSuccessHaptic + deviceEntryHapticsInteractor.playSuccessHapticOnDeviceEntry .sample(sceneInteractor.currentScene) .collect { currentScene -> if (Flags.msdlFeedback()) { diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java index eae0ba66925d..ade62a9b63d3 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java @@ -164,16 +164,17 @@ public class BrightnessDialog extends Activity { container.setVisibility(View.VISIBLE); ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) container.getLayoutParams(); - int horizontalMargin = - getResources().getDimensionPixelSize(R.dimen.notification_side_paddings); - lp.leftMargin = horizontalMargin; - lp.rightMargin = horizontalMargin; - - int verticalMargin = - getResources().getDimensionPixelSize( - R.dimen.notification_guts_option_vertical_padding); - - lp.topMargin = verticalMargin; + // Remove the margin. Have the container take all the space. Instead, insert padding. + // This allows for the background to be visible around the slider. + int margin = 0; + lp.topMargin = margin; + lp.bottomMargin = margin; + lp.leftMargin = margin; + lp.rightMargin = margin; + int padding = getResources().getDimensionPixelSize( + R.dimen.rounded_slider_background_padding + ); + container.setPadding(padding, padding, padding, padding); // If in multi-window or freeform, increase the top margin so the brightness dialog // doesn't get cut off. final int windowingMode = configuration.windowConfiguration.getWindowingMode(); @@ -182,17 +183,15 @@ public class BrightnessDialog extends Activity { lp.topMargin += 50; } - lp.bottomMargin = verticalMargin; - int orientation = configuration.orientation; int windowWidth = getWindowAvailableWidth(); if (orientation == Configuration.ORIENTATION_LANDSCAPE) { boolean shouldBeFullWidth = getIntent() .getBooleanExtra(EXTRA_BRIGHTNESS_DIALOG_IS_FULL_WIDTH, false); - lp.width = (shouldBeFullWidth ? windowWidth : windowWidth / 2) - horizontalMargin * 2; + lp.width = (shouldBeFullWidth ? windowWidth : windowWidth / 2) - margin * 2; } else if (orientation == Configuration.ORIENTATION_PORTRAIT) { - lp.width = windowWidth - horizontalMargin * 2; + lp.width = windowWidth - margin * 2; } container.setLayoutParams(lp); @@ -202,7 +201,7 @@ public class BrightnessDialog extends Activity { // Exclude this view (and its horizontal margins) from triggering gestures. // This prevents back gesture from being triggered by dragging close to the // edge of the slider (0% or 100%). - bounds.set(-horizontalMargin, 0, right - left + horizontalMargin, bottom - top); + bounds.set(-margin, 0, right - left + margin, bottom - top); v.setSystemGestureExclusionRects(List.of(bounds)); }); } diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt index 9e20055de856..962a3bd2376b 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt @@ -17,12 +17,15 @@ package com.android.systemui.settings.brightness import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.unit.dp import com.android.compose.theme.PlatformTheme import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer +import com.android.systemui.brightness.ui.compose.ContainerColors import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel import com.android.systemui.lifecycle.rememberViewModel @@ -44,7 +47,11 @@ private fun BrightnessSliderForDialog( rememberViewModel(traceName = "BrightnessDialog.viewModel") { brightnessSliderViewModelFactory.create(false) } - BrightnessSliderContainer(viewModel = viewModel, Modifier.fillMaxWidth()) + BrightnessSliderContainer( + viewModel = viewModel, + containerColors = ContainerColors.singleColor(ContainerColors.defaultContainerColor), + modifier = Modifier.fillMaxWidth().padding(8.dp), + ) } class ComposableProvider( diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index 3be2f1b7b957..362b5db012e1 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -17,6 +17,7 @@ package com.android.systemui.shade import android.content.Context +import android.content.res.Configuration import android.graphics.Rect import android.os.PowerManager import android.os.SystemClock @@ -25,11 +26,13 @@ import android.view.GestureDetector import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.view.WindowInsets import android.widget.FrameLayout import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcherOwner import androidx.activity.setViewTreeOnBackPressedDispatcherOwner import androidx.compose.ui.platform.ComposeView +import androidx.core.view.updateMargins import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleObserver @@ -101,7 +104,10 @@ constructor( ) : LifecycleOwner { private val logger = Logger(logBuffer, TAG) - private class CommunalWrapper(context: Context) : FrameLayout(context) { + private class CommunalWrapper( + context: Context, + private val communalSettingsInteractor: CommunalSettingsInteractor, + ) : FrameLayout(context) { private val consumers: MutableSet<Consumer<Boolean>> = ArraySet() override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { @@ -121,6 +127,24 @@ constructor( consumers.clear() } } + + override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets { + if ( + !communalSettingsInteractor.isV2FlagEnabled() || + resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE + ) { + return super.onApplyWindowInsets(windowInsets) + } + val type = WindowInsets.Type.displayCutout() + val insets = windowInsets.getInsets(type) + + // Reset horizontal margins added by window insets, so hub can be edge to edge. + if (insets.left > 0 || insets.right > 0) { + val lp = layoutParams as LayoutParams + lp.updateMargins(0, lp.topMargin, 0, lp.bottomMargin) + } + return WindowInsets.CONSUMED + } } /** The container view for the hub. This will not be initialized until [initView] is called. */ @@ -443,7 +467,8 @@ constructor( collectFlow(containerView, keyguardInteractor.isDreaming, { isDreaming = it }) collectFlow(containerView, communalViewModel.swipeToHubEnabled, { swipeToHubEnabled = it }) - communalContainerWrapper = CommunalWrapper(containerView.context) + communalContainerWrapper = + CommunalWrapper(containerView.context, communalSettingsInteractor) communalContainerWrapper?.addView(communalContainerView) logger.d("Hub container initialized") return communalContainerWrapper!! diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 305444f7ab5e..dafb1a559d59 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -30,8 +30,6 @@ import android.graphics.Region; import android.os.IBinder; import android.os.RemoteException; import android.os.Trace; -import android.os.UserHandle; -import android.provider.Settings; import android.util.Log; import android.view.Display; import android.view.IWindow; @@ -75,7 +73,6 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; -import com.android.systemui.util.settings.SecureSettings; import dagger.Lazy; @@ -134,7 +131,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW private final SysuiColorExtractor mColorExtractor; private final NotificationShadeWindowModel mNotificationShadeWindowModel; - private final SecureSettings mSecureSettings; /** * Layout params would be aggregated and dispatched all at once if this is > 0. * @@ -168,7 +164,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW Lazy<SelectedUserInteractor> userInteractor, UserTracker userTracker, NotificationShadeWindowModel notificationShadeWindowModel, - SecureSettings secureSettings, Lazy<CommunalInteractor> communalInteractor, @ShadeDisplayAware LayoutParams shadeWindowLayoutParams) { mContext = context; @@ -186,7 +181,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mBackgroundExecutor = backgroundExecutor; mColorExtractor = colorExtractor; mNotificationShadeWindowModel = notificationShadeWindowModel; - mSecureSettings = secureSettings; // prefix with {slow} to make sure this dumps at the END of the critical section. dumpManager.registerCriticalDumpable("{slow}NotificationShadeWindowControllerImpl", this); mAuthController = authController; @@ -424,7 +418,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW (long) mLpChanged.preferredMaxDisplayRefreshRate); } - if (state.bouncerShowing && !isSecureWindowsDisabled()) { + if (state.bouncerShowing) { mLpChanged.flags |= LayoutParams.FLAG_SECURE; } else { mLpChanged.flags &= ~LayoutParams.FLAG_SECURE; @@ -437,13 +431,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } } - private boolean isSecureWindowsDisabled() { - return mSecureSettings.getIntForUser( - Settings.Secure.DISABLE_SECURE_WINDOWS, - 0, - UserHandle.USER_CURRENT) == 1; - } - private void adjustScreenOrientation(NotificationShadeWindowState state) { if (state.bouncerShowing || state.isKeyguardShowingAndNotOccluded() || state.dozing) { if (mKeyguardStateController.isKeyguardScreenRotationAllowed()) { @@ -511,17 +498,18 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } private boolean isExpanded(NotificationShadeWindowState state) { + boolean visForBlur = !Flags.disableShadeVisibleWithBlur() && state.backgroundBlurRadius > 0; boolean isExpanded = !state.forceWindowCollapsed && (state.isKeyguardShowingAndNotOccluded() || state.panelVisible || state.keyguardFadingAway || state.bouncerShowing || state.headsUpNotificationShowing || state.scrimsVisibility != ScrimController.TRANSPARENT) - || state.backgroundBlurRadius > 0 + || visForBlur || state.launchingActivityFromNotification; mLogger.logIsExpanded(isExpanded, state.forceWindowCollapsed, state.isKeyguardShowingAndNotOccluded(), state.panelVisible, state.keyguardFadingAway, state.bouncerShowing, state.headsUpNotificationShowing, state.scrimsVisibility != ScrimController.TRANSPARENT, - state.backgroundBlurRadius > 0, state.launchingActivityFromNotification); + visForBlur, state.launchingActivityFromNotification); return isExpanded; } 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 20b44d73e097..5609326362fc 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 @@ -26,6 +26,7 @@ import androidx.compose.material3.ColorScheme import androidx.compose.runtime.getValue import androidx.compose.ui.graphics.Color import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.compose.animation.scene.OverlayKey import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator @@ -86,6 +87,22 @@ constructor( (ViewGroup, StatusBarLocation) -> BatteryMeterViewController = batteryMeterViewControllerFactory::create + val showClock: Boolean by + hydrator.hydratedStateOf( + traceName = "showClock", + initialValue = + shouldShowClock( + isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value, + overlays = sceneInteractor.currentOverlays.value, + ), + source = + combine( + shadeInteractor.isShadeLayoutWide, + sceneInteractor.currentOverlays, + ::shouldShowClock, + ), + ) + val notificationsChipHighlight: HeaderChipHighlight by hydrator.hydratedStateOf( traceName = "notificationsChipHighlight", @@ -114,13 +131,6 @@ constructor( }, ) - val isShadeLayoutWide: Boolean by - hydrator.hydratedStateOf( - traceName = "isShadeLayoutWide", - initialValue = shadeInteractor.isShadeLayoutWide.value, - source = shadeInteractor.isShadeLayoutWide, - ) - /** True if there is exactly one mobile connection. */ val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier @@ -271,6 +281,11 @@ constructor( } } + private fun shouldShowClock(isShadeLayoutWide: Boolean, overlays: Set<OverlayKey>): Boolean { + // Notifications shade on narrow layout renders its own clock. Hide the header clock. + return isShadeLayoutWide || Overlays.NotificationsShade !in overlays + } + private fun getFormatFromPattern(pattern: String?): DateFormat { val format = DateFormat.getInstanceForSkeleton(pattern, Locale.getDefault()) format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java index c2e355d07e9c..03c191e40ccf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java @@ -22,6 +22,7 @@ import android.app.Flags; import android.app.Notification; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; +import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.TypedValue; @@ -31,6 +32,8 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.VisibleForTesting; + import com.android.internal.R; import com.android.internal.widget.CachingIconView; import com.android.internal.widget.ConversationLayout; @@ -39,6 +42,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationContentView; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import java.util.ArrayList; import java.util.HashSet; @@ -214,7 +218,7 @@ public class NotificationGroupingUtil { } // in case no view is visible we make sure the time is visible int timeVisibility = !hasVisibleText - || row.getEntry().getSbn().getNotification().showsTime() + || showsTime(row) ? View.VISIBLE : View.GONE; time.setVisibility(timeVisibility); View left = null; @@ -243,6 +247,17 @@ public class NotificationGroupingUtil { } } + @VisibleForTesting + boolean showsTime(ExpandableNotificationRow row) { + StatusBarNotification sbn; + if (NotificationBundleUi.isEnabled()) { + sbn = row.getEntryAdapter() != null ? row.getEntryAdapter().getSbn() : null; + } else { + sbn = row.getEntry().getSbn(); + } + return (sbn != null && sbn.getNotification().showsTime()); + } + /** * Reset the modifications to this row for removing it from the group. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java index 0d34bdc7e477..041ed6504634 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java @@ -59,11 +59,13 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.statusbar.dagger.CentralSurfacesDependenciesModule; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.RemoteInputControllerLogger; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply; import com.android.systemui.statusbar.policy.RemoteInputUriController; import com.android.systemui.statusbar.policy.RemoteInputView; @@ -134,20 +136,21 @@ public class NotificationRemoteInputManager implements CoreStartable { view.getTag(com.android.internal.R.id.notification_action_index_tag); final ExpandableNotificationRow row = getNotificationRowForParent(view.getParent()); - final NotificationEntry entry = getNotificationForParent(view.getParent()); - mLogger.logInitialClick( - row != null ? row.getLoggingKey() : null, actionIndex, pendingIntent); + if (row == null) { + return false; + } + mLogger.logInitialClick(row.getLoggingKey(), actionIndex, pendingIntent); if (handleRemoteInput(view, pendingIntent)) { - mLogger.logRemoteInputWasHandled( - row != null ? row.getLoggingKey() : null, actionIndex); + mLogger.logRemoteInputWasHandled(row.getLoggingKey(), actionIndex); return true; } if (DEBUG) { Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent); } - logActionClick(view, entry, pendingIntent); + Notification.Action action = getActionFromView(view, row, pendingIntent); + logActionClick(view, row.getKey(), action); // The intent we are sending is for the application, which // won't have permission to immediately start an activity after // the user switches to home. We know it is safe to do at this @@ -156,33 +159,47 @@ public class NotificationRemoteInputManager implements CoreStartable { ActivityManager.getService().resumeAppSwitches(); } catch (RemoteException e) { } - Notification.Action action = getActionFromView(view, entry, pendingIntent); return mCallback.handleRemoteViewClick(view, pendingIntent, action == null ? false : action.isAuthenticationRequired(), actionIndex, () -> { Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view); mLogger.logStartingIntentWithDefaultHandler( - row != null ? row.getLoggingKey() : null, pendingIntent, actionIndex); + row.getLoggingKey(), pendingIntent, actionIndex); boolean started = RemoteViews.startPendingIntent(view, pendingIntent, options); - if (started) releaseNotificationIfKeptForRemoteInputHistory(entry); + if (started) { + if (NotificationBundleUi.isEnabled()) { + releaseNotificationIfKeptForRemoteInputHistory(row.getEntryAdapter()); + } else { + releaseNotificationIfKeptForRemoteInputHistory(row.getEntry()); + } + } return started; }); } private @Nullable Notification.Action getActionFromView(View view, - NotificationEntry entry, PendingIntent actionIntent) { + ExpandableNotificationRow row, PendingIntent actionIntent) { Integer actionIndex = (Integer) view.getTag(com.android.internal.R.id.notification_action_index_tag); if (actionIndex == null) { return null; } - if (entry == null) { + StatusBarNotification statusBarNotification = null; + if (NotificationBundleUi.isEnabled()) { + if (row.getEntryAdapter() != null) { + statusBarNotification = row.getEntryAdapter().getSbn(); + } + } else { + if (row.getEntry() != null) { + statusBarNotification = row.getEntry().getSbn(); + } + } + if (statusBarNotification == null) { Log.w(TAG, "Couldn't determine notification for click."); return null; } // Notification may be updated before this function is executed, and thus play safe // here and verify that the action object is still the one that where the click happens. - StatusBarNotification statusBarNotification = entry.getSbn(); Notification.Action[] actions = statusBarNotification.getNotification().actions; if (actions == null || actionIndex >= actions.length) { Log.w(TAG, "statusBarNotification.getNotification().actions is null or invalid"); @@ -199,14 +216,12 @@ public class NotificationRemoteInputManager implements CoreStartable { private void logActionClick( View view, - NotificationEntry entry, - PendingIntent actionIntent) { - Notification.Action action = getActionFromView(view, entry, actionIntent); + String key, + Notification.Action action) { if (action == null) { return; } ViewParent parent = view.getParent(); - String key = entry.getSbn().getKey(); int buttonIndex = -1; // If this is a default template, determine the index of the button. if (view.getId() == com.android.internal.R.id.action0 && @@ -214,20 +229,10 @@ public class NotificationRemoteInputManager implements CoreStartable { ViewGroup actionGroup = (ViewGroup) parent; buttonIndex = actionGroup.indexOfChild(view); } - final NotificationVisibility nv = mVisibilityProvider.obtain(entry, true); + final NotificationVisibility nv = mVisibilityProvider.obtain(key, true); mClickNotifier.onNotificationActionClick(key, buttonIndex, action, nv, false); } - private NotificationEntry getNotificationForParent(ViewParent parent) { - while (parent != null) { - if (parent instanceof ExpandableNotificationRow) { - return ((ExpandableNotificationRow) parent).getEntry(); - } - parent = parent.getParent(); - } - return null; - } - private @Nullable ExpandableNotificationRow getNotificationRowForParent(ViewParent parent) { while (parent != null) { if (parent instanceof ExpandableNotificationRow) { @@ -394,11 +399,21 @@ public class NotificationRemoteInputManager implements CoreStartable { } } + /** + * Use {@link com.android.systemui.statusbar.notification.row.NotificationActionClickManager} + * instead + */ public void addActionPressListener(Consumer<NotificationEntry> listener) { + NotificationBundleUi.assertInLegacyMode(); mActionPressListeners.addIfAbsent(listener); } + /** + * Use {@link com.android.systemui.statusbar.notification.row.NotificationActionClickManager} + * instead + */ public void removeActionPressListener(Consumer<NotificationEntry> listener) { + NotificationBundleUi.assertInLegacyMode(); mActionPressListeners.remove(listener); } @@ -681,12 +696,30 @@ public class NotificationRemoteInputManager implements CoreStartable { * (after unlock, if applicable), and will then wait a short time to allow the app to update the * notification in response to the action. */ + private void releaseNotificationIfKeptForRemoteInputHistory(EntryAdapter entryAdapter) { + if (entryAdapter == null) { + return; + } + if (mRemoteInputListener != null) { + mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory( + entryAdapter.getKey()); + } + entryAdapter.onNotificationActionClicked(); + } + + /** + * Checks if the notification is being kept due to the user sending an inline reply, and if + * so, releases that hold. This is called anytime an action on the notification is dispatched + * (after unlock, if applicable), and will then wait a short time to allow the app to update the + * notification in response to the action. + */ private void releaseNotificationIfKeptForRemoteInputHistory(NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); if (entry == null) { return; } if (mRemoteInputListener != null) { - mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(entry); + mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(entry.getKey()); } for (Consumer<NotificationEntry> listener : mActionPressListeners) { listener.accept(entry); @@ -866,7 +899,7 @@ public class NotificationRemoteInputManager implements CoreStartable { boolean isNotificationKeptForRemoteInputHistory(@NonNull String key); /** Called on user interaction to end lifetime extension for history */ - void releaseNotificationIfKeptForRemoteInputHistory(@NonNull NotificationEntry entry); + void releaseNotificationIfKeptForRemoteInputHistory(@NonNull String entryKey); /** Called when the RemoteInputController is attached to the manager */ void setRemoteInputController(@NonNull RemoteInputController remoteInputController); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsReturnAnimations.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsReturnAnimations.kt new file mode 100644 index 000000000000..176419454c21 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsReturnAnimations.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.chips + +import com.android.systemui.Flags +import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization + +/** Helper for reading or using the status_bar_chips_return_animations flag state. */ +object StatusBarChipsReturnAnimations { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_STATUS_BAR_CHIPS_RETURN_ANIMATIONS + + /** Is the feature enabled. */ + @JvmStatic + inline val isEnabled + get() = StatusBarChipsModernization.isEnabled && Flags.statusBarChipsReturnAnimations() +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt index be3afad4e1d1..7e7031200988 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.call.ui.viewmodel +import android.app.PendingIntent import android.content.Context import android.view.View import com.android.internal.jank.Cuj @@ -31,6 +32,7 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad import com.android.systemui.statusbar.chips.StatusBarChipsLog +import com.android.systemui.statusbar.chips.StatusBarChipsReturnAnimations import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel @@ -42,8 +44,10 @@ import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCall import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -59,67 +63,110 @@ constructor( private val activityStarter: ActivityStarter, @StatusBarChipsLog private val logger: LogBuffer, ) : OngoingActivityChipViewModel { - override val chip: StateFlow<OngoingActivityChipModel> = - interactor.ongoingCallState - .map { state -> - when (state) { - is OngoingCallModel.NoCall, - is OngoingCallModel.InCallWithVisibleApp -> OngoingActivityChipModel.Inactive() - is OngoingCallModel.InCall -> { - val key = state.notificationKey - val contentDescription = getContentDescription(state.appName) - val icon = - if (state.notificationIconView != null) { - StatusBarConnectedDisplays.assertInLegacyMode() - OngoingActivityChipModel.ChipIcon.StatusBarView( - state.notificationIconView, - contentDescription, - ) - } else if (StatusBarConnectedDisplays.isEnabled) { - OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon( - state.notificationKey, - contentDescription, - ) + private val chipWithReturnAnimation: StateFlow<OngoingActivityChipModel> = + if (StatusBarChipsReturnAnimations.isEnabled) { + interactor.ongoingCallState + .map { state -> + when (state) { + is OngoingCallModel.NoCall -> OngoingActivityChipModel.Inactive() + is OngoingCallModel.InCall -> + prepareChip(state, systemClock, isHidden = state.isAppVisible) + } + } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + OngoingActivityChipModel.Inactive(), + ) + } else { + MutableStateFlow(OngoingActivityChipModel.Inactive()).asStateFlow() + } + + private val chipLegacy: StateFlow<OngoingActivityChipModel> = + if (!StatusBarChipsReturnAnimations.isEnabled) { + interactor.ongoingCallState + .map { state -> + when (state) { + is OngoingCallModel.NoCall -> OngoingActivityChipModel.Inactive() + is OngoingCallModel.InCall -> + if (state.isAppVisible) { + OngoingActivityChipModel.Inactive() } else { - OngoingActivityChipModel.ChipIcon.SingleColorIcon(phoneIcon) + prepareChip(state, systemClock, isHidden = false) } - - val colors = ColorsModel.AccentThemed - - // This block mimics OngoingCallController#updateChip. - if (state.startTimeMs <= 0L) { - // If the start time is invalid, don't show a timer and show just an - // icon. See b/192379214. - OngoingActivityChipModel.Active.IconOnly( - key = key, - icon = icon, - colors = colors, - onClickListenerLegacy = getOnClickListener(state), - clickBehavior = getClickBehavior(state), - ) - } else { - val startTimeInElapsedRealtime = - state.startTimeMs - systemClock.currentTimeMillis() + - systemClock.elapsedRealtime() - OngoingActivityChipModel.Active.Timer( - key = key, - icon = icon, - colors = colors, - startTimeMs = startTimeInElapsedRealtime, - onClickListenerLegacy = getOnClickListener(state), - clickBehavior = getClickBehavior(state), - ) - } } } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + OngoingActivityChipModel.Inactive(), + ) + } else { + MutableStateFlow(OngoingActivityChipModel.Inactive()).asStateFlow() + } + + override val chip: StateFlow<OngoingActivityChipModel> = + if (StatusBarChipsReturnAnimations.isEnabled) { + chipWithReturnAnimation + } else { + chipLegacy + } + + /** Builds an [OngoingActivityChipModel.Active] from all the relevant information. */ + private fun prepareChip( + state: OngoingCallModel.InCall, + systemClock: SystemClock, + isHidden: Boolean, + ): OngoingActivityChipModel.Active { + val key = state.notificationKey + val contentDescription = getContentDescription(state.appName) + val icon = + if (state.notificationIconView != null) { + StatusBarConnectedDisplays.assertInLegacyMode() + OngoingActivityChipModel.ChipIcon.StatusBarView( + state.notificationIconView, + contentDescription, + ) + } else if (StatusBarConnectedDisplays.isEnabled) { + OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon( + state.notificationKey, + contentDescription, + ) + } else { + OngoingActivityChipModel.ChipIcon.SingleColorIcon(phoneIcon) } - .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Inactive()) - private fun getOnClickListener(state: OngoingCallModel.InCall): View.OnClickListener? { - if (state.intent == null) { - return null + val colors = ColorsModel.AccentThemed + + // This block mimics OngoingCallController#updateChip. + if (state.startTimeMs <= 0L) { + // If the start time is invalid, don't show a timer and show just an icon. + // See b/192379214. + return OngoingActivityChipModel.Active.IconOnly( + key = key, + icon = icon, + colors = colors, + onClickListenerLegacy = getOnClickListener(state.intent), + clickBehavior = getClickBehavior(state.intent), + isHidden = isHidden, + ) + } else { + val startTimeInElapsedRealtime = + state.startTimeMs - systemClock.currentTimeMillis() + systemClock.elapsedRealtime() + return OngoingActivityChipModel.Active.Timer( + key = key, + icon = icon, + colors = colors, + startTimeMs = startTimeInElapsedRealtime, + onClickListenerLegacy = getOnClickListener(state.intent), + clickBehavior = getClickBehavior(state.intent), + isHidden = isHidden, + ) } + } + private fun getOnClickListener(intent: PendingIntent?): View.OnClickListener? { + if (intent == null) return null return View.OnClickListener { view -> StatusBarChipsModernization.assertInLegacyMode() logger.log(TAG, LogLevel.INFO, {}, { "Chip clicked" }) @@ -127,7 +174,7 @@ constructor( view.requireViewById<ChipBackgroundContainer>(R.id.ongoing_activity_chip_background) // This mimics OngoingCallController#updateChipClickListener. activityStarter.postStartActivityDismissingKeyguard( - state.intent, + intent, ActivityTransitionAnimator.Controller.fromView( backgroundView, Cuj.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP, @@ -136,10 +183,8 @@ constructor( } } - private fun getClickBehavior( - state: OngoingCallModel.InCall - ): OngoingActivityChipModel.ClickBehavior = - if (state.intent == null) { + private fun getClickBehavior(intent: PendingIntent?): OngoingActivityChipModel.ClickBehavior = + if (intent == null) { OngoingActivityChipModel.ClickBehavior.None } else { OngoingActivityChipModel.ClickBehavior.ExpandAction( @@ -149,10 +194,7 @@ constructor( expandable.activityTransitionController( Cuj.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP ) - activityStarter.postStartActivityDismissingKeyguard( - state.intent, - animationController, - ) + activityStarter.postStartActivityDismissingKeyguard(intent, animationController) } ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt index 02e4ba4d6437..6f8552738d33 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.chips.ui.binder import android.annotation.IdRes import android.content.Context import android.content.res.ColorStateList +import android.graphics.Typeface import android.graphics.drawable.GradientDrawable import android.view.View import android.view.ViewGroup @@ -27,6 +28,7 @@ import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import androidx.annotation.UiThread +import com.android.systemui.FontStyles import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.ui.binder.ContentDescriptionViewBinder import com.android.systemui.common.ui.binder.IconViewBinder @@ -170,6 +172,16 @@ object OngoingActivityChipBinder { forceLayout() } + /** Updates the typefaces for any text shown in the chip. */ + fun updateTypefaces(binding: OngoingActivityChipViewBinding) { + binding.timeView.typeface = + Typeface.create(FontStyles.GSF_LABEL_LARGE_EMPHASIZED, Typeface.NORMAL) + binding.textView.typeface = + Typeface.create(FontStyles.GSF_LABEL_LARGE_EMPHASIZED, Typeface.NORMAL) + binding.shortTimeDeltaView.typeface = + Typeface.create(FontStyles.GSF_LABEL_LARGE_EMPHASIZED, Typeface.NORMAL) + } + private fun setChipIcon( chipModel: OngoingActivityChipModel.Active, backgroundView: ChipBackgroundContainer, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt index ac55bf2bee1c..55d753662a65 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.ui.compose +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -44,6 +45,7 @@ import com.android.systemui.statusbar.chips.ui.viewmodel.rememberChronometerStat import com.android.systemui.statusbar.chips.ui.viewmodel.rememberTimeRemainingState import kotlin.math.min +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier = Modifier) { val context = LocalContext.current @@ -52,7 +54,7 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier = val hasEmbeddedIcon = viewModel.icon is OngoingActivityChipModel.ChipIcon.StatusBarView || viewModel.icon is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon - val textStyle = MaterialTheme.typography.labelLarge + val textStyle = MaterialTheme.typography.labelLargeEmphasized val textColor = Color(viewModel.colors.text(context)) val maxTextWidth = dimensionResource(id = R.dimen.ongoing_activity_chip_max_text_width) val startPadding = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt index d67cb8f81a6c..4edb23dc9f0e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt @@ -18,24 +18,18 @@ package com.android.systemui.statusbar.chips.ui.compose import android.content.res.ColorStateList import android.view.ViewGroup -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape 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.graphics.Color -import androidx.compose.ui.graphics.Shape import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource @@ -43,7 +37,6 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.viewinterop.AndroidView import com.android.compose.animation.Expandable -import com.android.compose.modifiers.thenIf import com.android.systemui.animation.Expandable import com.android.systemui.common.ui.compose.Icon import com.android.systemui.common.ui.compose.load @@ -60,32 +53,45 @@ fun OngoingActivityChip( iconViewStore: NotificationIconContainerViewBinder.IconViewStore?, modifier: Modifier = Modifier, ) { - when (val clickBehavior = model.clickBehavior) { - is OngoingActivityChipModel.ClickBehavior.ExpandAction -> { - // Wrap the chip in an Expandable so we can animate the expand transition. - ExpandableChip( - color = { Color.Transparent }, - shape = - RoundedCornerShape( - dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius) - ), - modifier = modifier, - ) { expandable -> - ChipBody(model, iconViewStore, onClick = { clickBehavior.onClick(expandable) }) - } + val contentDescription = + when (val icon = model.icon) { + is OngoingActivityChipModel.ChipIcon.StatusBarView -> icon.contentDescription.load() + is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon -> + icon.contentDescription.load() + is OngoingActivityChipModel.ChipIcon.SingleColorIcon, + null -> null } - is OngoingActivityChipModel.ClickBehavior.ShowHeadsUpNotification -> { - ChipBody( - model, - iconViewStore, - onClick = { clickBehavior.onClick() }, - modifier = modifier, - ) + + val borderStroke = + model.colors.outline(LocalContext.current)?.let { + BorderStroke(dimensionResource(R.dimen.ongoing_activity_chip_outline_width), Color(it)) } - is OngoingActivityChipModel.ClickBehavior.None -> { - ChipBody(model, iconViewStore, modifier = modifier) + val onClick = + when (val clickBehavior = model.clickBehavior) { + is OngoingActivityChipModel.ClickBehavior.ExpandAction -> { expandable: Expandable -> + clickBehavior.onClick(expandable) + } + is OngoingActivityChipModel.ClickBehavior.ShowHeadsUpNotification -> { _ -> + clickBehavior.onClick() + } + is OngoingActivityChipModel.ClickBehavior.None -> null } + + Expandable( + color = Color(model.colors.background(LocalContext.current).defaultColor), + shape = + RoundedCornerShape(dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius)), + modifier = + modifier.height(dimensionResource(R.dimen.ongoing_appops_chip_height)).semantics { + if (contentDescription != null) { + this.contentDescription = contentDescription + } + }, + borderStroke = borderStroke, + onClick = onClick, + ) { + ChipBody(model, iconViewStore, isClickable = onClick != null) } } @@ -93,22 +99,13 @@ fun OngoingActivityChip( private fun ChipBody( model: OngoingActivityChipModel.Active, iconViewStore: NotificationIconContainerViewBinder.IconViewStore?, + isClickable: Boolean, modifier: Modifier = Modifier, - onClick: (() -> Unit)? = null, ) { - val context = LocalContext.current - val isClickable = onClick != null val hasEmbeddedIcon = model.icon is OngoingActivityChipModel.ChipIcon.StatusBarView || model.icon is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon - val contentDescription = - when (val icon = model.icon) { - is OngoingActivityChipModel.ChipIcon.StatusBarView -> icon.contentDescription.load() - is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon -> - icon.contentDescription.load() - is OngoingActivityChipModel.ChipIcon.SingleColorIcon -> null - null -> null - } + val chipSidePadding = dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding) val minWidth = if (isClickable) { @@ -119,68 +116,38 @@ private fun ChipBody( dimensionResource(id = R.dimen.ongoing_activity_chip_min_text_width) + chipSidePadding } - val outline = model.colors.outline(context) - val outlineWidth = dimensionResource(R.dimen.ongoing_activity_chip_outline_width) - - val shape = - RoundedCornerShape(dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius)) - - // Use a Box with `fillMaxHeight` to create a larger click surface for the chip. The visible - // height of the chip is determined by the height of the background of the Row below. - Box( - contentAlignment = Alignment.Center, + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, modifier = modifier .fillMaxHeight() - .clickable(enabled = isClickable, onClick = onClick ?: {}) - .semantics { - if (contentDescription != null) { - this.contentDescription = contentDescription - } - }, - ) { - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier.height(dimensionResource(R.dimen.ongoing_appops_chip_height)) - .thenIf(isClickable) { Modifier.widthIn(min = minWidth) } - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - layout(placeable.width, placeable.height) { - if (constraints.maxWidth >= minWidth.roundToPx()) { - placeable.place(0, 0) - } + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + layout(placeable.width, placeable.height) { + if (constraints.maxWidth >= minWidth.roundToPx()) { + placeable.place(0, 0) } } - .background(Color(model.colors.background(context).defaultColor), shape = shape) - .thenIf(outline != null) { - Modifier.border( - width = outlineWidth, - color = Color(outline!!), - shape = shape, - ) - } - .padding( - horizontal = - if (hasEmbeddedIcon) { - dimensionResource( - R.dimen - .ongoing_activity_chip_side_padding_for_embedded_padding_icon - ) - } else { - dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding) - } - ), - ) { - model.icon?.let { - ChipIcon(viewModel = it, iconViewStore = iconViewStore, colors = model.colors) - } + } + .padding( + horizontal = + if (hasEmbeddedIcon) { + dimensionResource( + R.dimen.ongoing_activity_chip_side_padding_for_embedded_padding_icon + ) + } else { + dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding) + } + ), + ) { + model.icon?.let { + ChipIcon(viewModel = it, iconViewStore = iconViewStore, colors = model.colors) + } - val isIconOnly = model is OngoingActivityChipModel.Active.IconOnly - if (!isIconOnly) { - ChipContent(viewModel = model) - } + val isIconOnly = model is OngoingActivityChipModel.Active.IconOnly + if (!isIconOnly) { + ChipContent(viewModel = model) } } } @@ -244,13 +211,3 @@ private fun StatusBarIcon( update = { iconView -> iconView.imageTintList = colorTintList }, ) } - -@Composable -private fun ExpandableChip( - color: () -> Color, - shape: Shape, - modifier: Modifier = Modifier, - content: @Composable (Expandable) -> Unit, -) { - Expandable(color = color(), shape = shape, modifier = modifier.clip(shape)) { content(it) } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt index 4954cb0a1b24..b2683762cb2a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.chips.ui.model import android.content.Context import android.content.res.ColorStateList import androidx.annotation.ColorInt -import com.android.settingslib.Utils import com.android.systemui.res.R /** Model representing how the chip in the status bar should be colored. */ @@ -34,14 +33,14 @@ sealed interface ColorsModel { @ColorInt fun outline(context: Context): Int? /** The chip should match the theme's primary accent color. */ - // TODO(b/347717946): The chip's color isn't getting updated when the user switches theme, it - // only gets updated when a different configuration change happens, like a rotation. data object AccentThemed : ColorsModel { override fun background(context: Context): ColorStateList = - Utils.getColorAttr(context, com.android.internal.R.attr.colorAccent) + ColorStateList.valueOf( + context.getColor(com.android.internal.R.color.materialColorPrimaryFixedDim) + ) override fun text(context: Context) = - Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary) + context.getColor(com.android.internal.R.color.materialColorOnPrimaryFixed) override fun outline(context: Context) = null } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt index 0cfc3216beb5..8e470742f174 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.chips.ui.model +import android.annotation.CurrentTimeMillisLong +import android.annotation.ElapsedRealtimeLong import android.view.View import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription @@ -102,7 +104,7 @@ sealed class OngoingActivityChipModel { * [ChipChronometer] is based off of elapsed realtime. See * [android.widget.Chronometer.setBase]. */ - val startTimeMs: Long, + @ElapsedRealtimeLong val startTimeMs: Long, override val onClickListenerLegacy: View.OnClickListener?, override val clickBehavior: ClickBehavior, override val isHidden: Boolean = false, @@ -129,10 +131,15 @@ sealed class OngoingActivityChipModel { override val icon: ChipIcon, override val colors: ColorsModel, /** - * The time of the event that this chip represents, relative to - * [com.android.systemui.util.time.SystemClock.currentTimeMillis]. + * The time of the event that this chip represents. Relative to + * [com.android.systemui.util.time.SystemClock.currentTimeMillis] because that's what's + * required by [android.widget.DateTimeView]. + * + * TODO(b/372657935): When the Compose chips are launched, we should convert this to be + * relative to [com.android.systemui.util.time.SystemClock.elapsedRealtime] so that + * this model and the [Timer] model use the same units. */ - val time: Long, + @CurrentTimeMillisLong val time: Long, override val onClickListenerLegacy: View.OnClickListener?, override val clickBehavior: ClickBehavior, override val isHidden: Boolean = false, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt index eae2c25d77d8..8228b5533fca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt @@ -555,7 +555,6 @@ constructor( secondary = DEFAULT_INTERNAL_INACTIVE_MODEL, ) - // TODO(b/392886257): Support 3 chips if there's space available. - private const val MAX_VISIBLE_CHIPS = 2 + private const val MAX_VISIBLE_CHIPS = 3 } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt index eb6ebcaa5796..803d422c0f0f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.chips.ui.viewmodel -import android.os.SystemClock import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -39,7 +38,14 @@ import kotlinx.coroutines.delay * Manages state and updates for the duration remaining between now and a given time in the future. */ class TimeRemainingState(private val timeSource: TimeSource, private val futureTimeMillis: Long) { - private var durationRemaining by mutableStateOf(Duration.ZERO) + // Start with the right duration from the outset so we don't use "now" as an initial value. + private var durationRemaining by + mutableStateOf( + calculateDurationRemaining( + currentTimeMillis = timeSource.getCurrentTime(), + futureTimeMillis = futureTimeMillis, + ) + ) private var startTimeMillis: Long = 0 /** @@ -56,7 +62,11 @@ class TimeRemainingState(private val timeSource: TimeSource, private val futureT while (true) { val currentTime = timeSource.getCurrentTime() durationRemaining = - (futureTimeMillis - currentTime).toDuration(DurationUnit.MILLISECONDS) + calculateDurationRemaining( + currentTimeMillis = currentTime, + futureTimeMillis = futureTimeMillis, + ) + // No need to update if duration is more than 1 minute in the past. Because, we will // stop displaying anything. if (durationRemaining.inWholeMilliseconds < -1.minutes.inWholeMilliseconds) { @@ -67,6 +77,13 @@ class TimeRemainingState(private val timeSource: TimeSource, private val futureT } } + private fun calculateDurationRemaining( + currentTimeMillis: Long, + futureTimeMillis: Long, + ): Duration { + return (futureTimeMillis - currentTimeMillis).toDuration(DurationUnit.MILLISECONDS) + } + private fun calculateNextUpdateDelay(duration: Duration): Long { val durationAbsolute = duration.absoluteValue return when { @@ -85,7 +102,7 @@ class TimeRemainingState(private val timeSource: TimeSource, private val futureT @Composable fun rememberTimeRemainingState( futureTimeMillis: Long, - timeSource: TimeSource = remember { TimeSource { SystemClock.elapsedRealtime() } }, + timeSource: TimeSource = remember { TimeSource { System.currentTimeMillis() } }, ): TimeRemainingState { val state = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java index 434eb7d3d410..a7929ecbd801 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java @@ -33,6 +33,7 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpHandler; import com.android.systemui.dump.DumpManager; +import com.android.systemui.media.NotificationMediaManager; import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.scene.shared.flag.SceneContainerFlag; @@ -44,7 +45,6 @@ import com.android.systemui.shade.ShadeSurfaceImpl; import com.android.systemui.shade.carrier.ShadeCarrierGroupController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationClickNotifier; -import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.StatusBarStateControllerImpl; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt index 7fa9f0e9dcc2..a658e1d55f7a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt @@ -267,7 +267,8 @@ constructor( if (StatusBarChipsModernization.isEnabled) { ongoingProcessRequiresStatusBarVisible } else { - ongoingCallStateLegacy is OngoingCallModel.InCall + ongoingCallStateLegacy is OngoingCallModel.InCall && + !ongoingCallStateLegacy.isAppVisible } val statusBarMode = toBarMode( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java index 41353b9921bd..4d68f2e6ef1b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java @@ -63,17 +63,6 @@ public class BundleEntry extends PipelineEntry { @Nullable @Override - public NotifSection getSection() { - return null; - } - - @Override - public int getSectionIndex() { - return 0; - } - - @Nullable - @Override public PipelineEntry getParent() { return null; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt index 64db9df8270c..26c302bf6409 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt @@ -20,6 +20,7 @@ import android.app.Notification import android.content.Context import android.os.Build import android.service.notification.StatusBarNotification +import android.util.Log import com.android.systemui.statusbar.notification.icon.IconPack import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import kotlinx.coroutines.flow.StateFlow @@ -118,5 +119,13 @@ class BundleEntryAdapter(val entry: BundleEntry) : EntryAdapter { override fun onNotificationBubbleIconClicked() { // do nothing. these cannot be a bubble + Log.wtf(TAG, "onNotificationBubbleIconClicked() called") + } + + override fun onNotificationActionClicked() { + // do nothing. these have no actions + Log.wtf(TAG, "onNotificationActionClicked() called") } } + +private const val TAG = "BundleEntryAdapter" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java index 0e75b6050678..3118ce56ac69 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java @@ -140,5 +140,10 @@ public interface EntryAdapter { * Process a click on a notification bubble icon */ void onNotificationBubbleIconClicked(); + + /** + * Processes a click on a notification action + */ + void onNotificationActionClicked(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt index 779c25a3b402..a5169865c3c9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt @@ -16,11 +16,11 @@ package com.android.systemui.statusbar.notification.collection -import androidx.annotation.VisibleForTesting import com.android.internal.logging.MetricsLogger import com.android.systemui.statusbar.notification.NotificationActivityStarter import com.android.systemui.statusbar.notification.collection.coordinator.VisualStabilityCoordinator import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier +import com.android.systemui.statusbar.notification.row.NotificationActionClickManager import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider import javax.inject.Inject @@ -33,6 +33,7 @@ constructor( private val peopleNotificationIdentifier: PeopleNotificationIdentifier, private val iconStyleProvider: NotificationIconStyleProvider, private val visualStabilityCoordinator: VisualStabilityCoordinator, + private val notificationActionClickManager: NotificationActionClickManager, ) : EntryAdapterFactory { override fun create(entry: PipelineEntry): EntryAdapter { return if (entry is NotificationEntry) { @@ -42,15 +43,11 @@ constructor( peopleNotificationIdentifier, iconStyleProvider, visualStabilityCoordinator, + notificationActionClickManager, entry, ) } else { BundleEntryAdapter((entry as BundleEntry)) } } - - @VisibleForTesting - fun getNotificationActivityStarter() : NotificationActivityStarter { - return notificationActivityStarter - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt index 4a1b9568c714..04dc7d5ed3ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt @@ -31,8 +31,8 @@ data class ListAttachState private constructor( var parent: PipelineEntry?, /** - * The section that this ListEntry was sorted into. If the child of the group, this will be the - * parent's section. Null if not attached to the list. + * The section that this PipelineEntry was sorted into. If the child of the group, this will be + * the parent's section. Null if not attached to the list. */ var section: NotifSection?, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java index 697d0a06cf9d..caa7abb1aa7a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java @@ -66,10 +66,6 @@ public abstract class ListEntry extends PipelineEntry { return mPreviousAttachState.getParent(); } - public int getSectionIndex() { - return mAttachState.getSection() != null ? mAttachState.getSection().getIndex() : -1; - } - /** * Stores the current attach state into {@link #getPreviousAttachState()}} and then starts a * fresh attach state (all entries will be null/default-initialized). diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt index f662a040fae6..0fc0e9c5eab8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt @@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifBundler import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter @@ -169,6 +170,14 @@ class NotifPipeline @Inject constructor( } /** + * NotifBundler that is used to determine whether a notification should be bundled according to + * classification. + */ + fun setNotifBundler(bundler: NotifBundler) { + mShadeListBuilder.setBundler(bundler) + } + + /** * StabilityManager that is used to determine whether to suppress group and section changes. * This should only be set once. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt index 0ff2dd7c7f43..1168c647c26a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt @@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.coordinator.Visual import com.android.systemui.statusbar.notification.icon.IconPack import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.notification.row.NotificationActionClickManager import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider import kotlinx.coroutines.flow.StateFlow @@ -33,6 +34,7 @@ class NotificationEntryAdapter( private val peopleNotificationIdentifier: PeopleNotificationIdentifier, private val iconStyleProvider: NotificationIconStyleProvider, private val visualStabilityCoordinator: VisualStabilityCoordinator, + private val notificationActionClickManager: NotificationActionClickManager, private val entry: NotificationEntry, ) : EntryAdapter { @@ -142,4 +144,8 @@ class NotificationEntryAdapter( override fun onNotificationBubbleIconClicked() { notificationActivityStarter.onNotificationBubbleIconClicked(entry) } + + override fun onNotificationActionClicked() { + notificationActionClickManager.onNotificationActionClicked(entry) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java index 84de77bac352..872cd68e1b21 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java @@ -70,7 +70,9 @@ public abstract class PipelineEntry { /** * @return Index of section assigned to this entry. */ - public abstract int getSectionIndex(); + public int getSectionIndex() { + return mAttachState.getSection() != null ? mAttachState.getSection().getIndex() : -1; + } /** * @return Parent PipelineEntry diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java index bb84ab8f421a..238ba8d9f490 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java @@ -48,6 +48,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; import com.android.systemui.statusbar.NotificationInteractionTracker; import com.android.systemui.statusbar.notification.NotifPipelineFlags; +import com.android.systemui.statusbar.notification.collection.coordinator.BundleCoordinator; import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection; import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener; import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener; @@ -58,8 +59,10 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.SemiSt import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort.StableOrder; import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper; import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger; +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifBundler; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifStabilityManager; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator; +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifBundler; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter; @@ -78,6 +81,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -122,7 +126,8 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { private final List<NotifComparator> mNotifComparators = new ArrayList<>(); private final List<NotifSection> mNotifSections = new ArrayList<>(); private NotifStabilityManager mNotifStabilityManager; - + private NotifBundler mNotifBundler; + private Map<String, BundleEntry> mIdToBundleEntry = new HashMap<>(); private final NamedListenerSet<OnBeforeTransformGroupsListener> mOnBeforeTransformGroupsListeners = new NamedListenerSet<>(); private final NamedListenerSet<OnBeforeSortListener> @@ -273,6 +278,21 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { } } + void setBundler(NotifBundler bundler) { + Assert.isMainThread(); + mPipelineState.requireState(STATE_IDLE); + + mNotifBundler = bundler; + if (mNotifBundler == null) { + throw new IllegalStateException(TAG + ".setBundler: null"); + } + + mIdToBundleEntry.clear(); + for (String id: mNotifBundler.getBundleIds()) { + mIdToBundleEntry.put(id, new BundleEntry(id)); + } + } + void setNotifStabilityManager(@NonNull NotifStabilityManager notifStabilityManager) { Assert.isMainThread(); mPipelineState.requireState(STATE_IDLE); @@ -297,6 +317,14 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { return mNotifStabilityManager; } + @NonNull + private NotifBundler getNotifBundler() { + if (mNotifBundler == null) { + return DefaultNotifBundler.INSTANCE; + } + return mNotifBundler; + } + void setComparators(List<NotifComparator> comparators) { Assert.isMainThread(); mPipelineState.requireState(STATE_IDLE); @@ -651,7 +679,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { j--; } } - } else { + } else if (tle instanceof NotificationEntry) { // maybe put top-level-entries back into their previous groups if (maybeSuppressGroupChange(tle.getRepresentativeEntry(), topLevelList)) { // entry was put back into its previous group, so we remove it from the list of @@ -659,7 +687,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable { topLevelList.remove(i); i--; } - } + } // Promoters ignore bundles so we don't have to demote any here. } Trace.endSection(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinator.kt index e6d5f4120a20..8833ff1ce20c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinator.kt @@ -20,15 +20,19 @@ import android.app.NotificationChannel.NEWS_ID import android.app.NotificationChannel.PROMOTIONS_ID import android.app.NotificationChannel.RECS_ID import android.app.NotificationChannel.SOCIAL_MEDIA_ID -import com.android.systemui.statusbar.notification.collection.PipelineEntry +import android.app.NotificationChannel.SYSTEM_RESERVED_IDS import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.PipelineEntry import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifBundler import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner import com.android.systemui.statusbar.notification.collection.render.NodeController import com.android.systemui.statusbar.notification.dagger.NewsHeader import com.android.systemui.statusbar.notification.dagger.PromoHeader import com.android.systemui.statusbar.notification.dagger.RecsHeader import com.android.systemui.statusbar.notification.dagger.SocialHeader +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.notification.stack.BUCKET_NEWS import com.android.systemui.statusbar.notification.stack.BUCKET_PROMO import com.android.systemui.statusbar.notification.stack.BUCKET_RECS @@ -90,6 +94,20 @@ class BundleCoordinator @Inject constructor( } } + val bundler = + object : NotifBundler("NotifBundler") { + + // Use list instead of set to keep fixed order + override val bundleIds: List<String> = SYSTEM_RESERVED_IDS + + override fun getBundleIdOrNull(entry: NotificationEntry?): String? { + return entry?.representativeEntry?.channel?.id?.takeIf { it in this.bundleIds } + } + } + override fun attach(pipeline: NotifPipeline) { + if (NotificationBundleUi.isEnabled) { + pipeline.setNotifBundler(bundler) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index fdb8cd871dd9..a0eab43f854b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -26,6 +26,7 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.NotifPipelineFlags +import com.android.systemui.statusbar.notification.collection.BundleEntry import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.NotifCollection import com.android.systemui.statusbar.notification.collection.NotifPipeline @@ -47,7 +48,9 @@ import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider import com.android.systemui.statusbar.notification.logKey +import com.android.systemui.statusbar.notification.row.NotificationActionClickManager import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.time.SystemClock @@ -82,6 +85,7 @@ constructor( private val mHeadsUpViewBinder: HeadsUpViewBinder, private val mVisualInterruptionDecisionProvider: VisualInterruptionDecisionProvider, private val mRemoteInputManager: NotificationRemoteInputManager, + private val notificationActionClickManager: NotificationActionClickManager, private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider, private val mFlags: NotifPipelineFlags, private val statusBarNotificationChipsInteractor: StatusBarNotificationChipsInteractor, @@ -107,7 +111,11 @@ constructor( pipeline.addOnBeforeFinalizeFilterListener(::onBeforeFinalizeFilter) pipeline.addPromoter(mNotifPromoter) pipeline.addNotificationLifetimeExtender(mLifetimeExtender) - mRemoteInputManager.addActionPressListener(mActionPressListener) + if (NotificationBundleUi.isEnabled) { + notificationActionClickManager.addActionClickListener(mActionPressListener) + } else { + mRemoteInputManager.addActionPressListener(mActionPressListener) + } if (StatusBarNotifChips.isEnabled) { applicationScope.launch { @@ -423,6 +431,7 @@ constructor( map[child.key] = GroupLocation.Child } } + is BundleEntry -> map[topLevelEntry.key] = GroupLocation.Bundle else -> error("unhandled type $topLevelEntry") } } @@ -781,7 +790,7 @@ constructor( */ private val mActionPressListener = Consumer<NotificationEntry> { entry -> - mHeadsUpManager.setUserActionMayIndirectlyRemove(entry) + mHeadsUpManager.setUserActionMayIndirectlyRemove(entry.key) mExecutor.execute { endNotifLifetimeExtensionIfExtended(entry) } } @@ -950,6 +959,7 @@ private enum class GroupLocation { Isolated, Summary, Child, + Bundle, } private fun Map<String, GroupLocation>.getLocation(key: String): GroupLocation = 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 56deb18df9ab..d542e67e665a 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 @@ -26,6 +26,7 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.notification.collection.BundleEntry import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.PipelineEntry import com.android.systemui.statusbar.notification.collection.NotifPipeline @@ -193,6 +194,7 @@ constructor( when (it) { is NotificationEntry -> listOfNotNull(it) is GroupEntry -> it.children + is BundleEntry -> emptyList() else -> error("unhandled type of $it") } } 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 df0cde51e8ba..818ef6b335c7 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 @@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.coordinator.dagger 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.promoted.AutomaticPromotionCoordinator +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection @@ -113,11 +114,12 @@ constructor( mCoordinators.add(remoteInputCoordinator) mCoordinators.add(dismissibilityCoordinator) mCoordinators.add(automaticPromotionCoordinator) - + if (NotificationBundleUi.isEnabled) { + mCoordinators.add(bundleCoordinator) + } if (NotificationsLiveDataStoreRefactor.isEnabled) { mCoordinators.add(statsLoggerCoordinator) } - // Manually add Ordered Sections if (NotificationMinimalism.isEnabled) { mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing @@ -135,7 +137,7 @@ constructor( mOrderedSections.add(conversationCoordinator.peopleSilentSectioner) // People Silent } mOrderedSections.add(rankingCoordinator.alertingSectioner) // Alerting - if (NotificationClassificationFlag.isEnabled) { + if (NotificationClassificationFlag.isEnabled && !NotificationBundleUi.isEnabled) { mOrderedSections.add(bundleCoordinator.newsSectioner) mOrderedSections.add(bundleCoordinator.socialSectioner) mOrderedSections.add(bundleCoordinator.recsSectioner) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java index 20c6736b74c8..b54f21b23bba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java @@ -34,6 +34,7 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.statusbar.IStatusBarService; +import com.android.systemui.statusbar.notification.collection.BundleEntry; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.PipelineEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; @@ -54,6 +55,7 @@ import com.android.systemui.statusbar.notification.row.icon.AppIconProvider; import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -306,7 +308,9 @@ public class PreparationCoordinator implements Coordinator { private void inflateAllRequiredViews(List<PipelineEntry> entries) { for (int i = 0, size = entries.size(); i < size; i++) { PipelineEntry entry = entries.get(i); - if (entry instanceof GroupEntry) { + if (NotificationBundleUi.isEnabled() && entry instanceof BundleEntry) { + // TODO(b/399738511) Inflate bundle views. + } else if (entry instanceof GroupEntry) { GroupEntry groupEntry = (GroupEntry) entry; inflateRequiredGroupViews(groupEntry); } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java index d1063d95a305..3fad8f0510a1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java @@ -132,7 +132,12 @@ public class RankingCoordinator implements Coordinator { public void onEntriesUpdated(@NonNull List<PipelineEntry> entries) { mHasSilentEntries = false; for (int i = 0; i < entries.size(); i++) { - if (entries.get(i).getRepresentativeEntry().getSbn().isClearable()) { + NotificationEntry notifEntry = entries.get(i).getRepresentativeEntry(); + if (notifEntry == null) { + // TODO(b/395698521) Handle BundleEntry + continue; + } + if (notifEntry.getSbn().isClearable()) { mHasSilentEntries = true; break; } @@ -147,6 +152,7 @@ public class RankingCoordinator implements Coordinator { @Override public boolean isInSection(PipelineEntry entry) { return !mHighPriorityProvider.isHighPriority(entry) + && entry.getRepresentativeEntry() != null && entry.getRepresentativeEntry().isAmbient(); } @@ -161,7 +167,12 @@ public class RankingCoordinator implements Coordinator { public void onEntriesUpdated(@NonNull List<PipelineEntry> entries) { mHasMinimizedEntries = false; for (int i = 0; i < entries.size(); i++) { - if (entries.get(i).getRepresentativeEntry().getSbn().isClearable()) { + NotificationEntry notifEntry = entries.get(i).getRepresentativeEntry(); + if (notifEntry == null) { + // TODO(b/395698521) Handle BundleEntry + continue; + } + if (notifEntry.getSbn().isClearable()) { mHasMinimizedEntries = true; break; } 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 e7c767f42c12..27c0dcccfe43 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 @@ -221,20 +221,20 @@ constructor( mSmartReplyHistoryExtender.isExtending(key) } else false - override fun releaseNotificationIfKeptForRemoteInputHistory(entry: NotificationEntry) { - if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entry.key})") + override fun releaseNotificationIfKeptForRemoteInputHistory(entryKey: String) { + if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entryKey})") if (!lifetimeExtensionRefactor()) { mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay( - entry.key, + entryKey, REMOTE_INPUT_EXTENDER_RELEASE_DELAY, ) mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay( - entry.key, + entryKey, REMOTE_INPUT_EXTENDER_RELEASE_DELAY, ) } mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay( - entry.key, + entryKey, REMOTE_INPUT_EXTENDER_RELEASE_DELAY, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java index c2f0806a9cd6..6b32c6a18ec0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.notification.collection.inflation; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE; -import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC; @@ -276,7 +275,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { if (LockscreenOtpRedaction.isSingleLineViewEnabled()) { if (inflaterParams.isChildInGroup() - && redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) { + && redactionType != REDACTION_TYPE_NONE) { params.requireContentViews(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE); } else { params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifBundler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifBundler.kt new file mode 100644 index 000000000000..14a9113f7eb4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifBundler.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.notification.collection.listbuilder.pluggable + +import com.android.systemui.statusbar.notification.collection.NotificationEntry + +/** Pluggable for bundling notifications according to classification. */ +abstract class NotifBundler protected constructor(name: String?) : Pluggable<NotifBundler?>(name) { + abstract val bundleIds: List<String> + + abstract fun getBundleIdOrNull(entry: NotificationEntry?): String? +} + +/** The default, no-op instance of NotifBundler which does not bundle anything. */ +object DefaultNotifBundler : NotifBundler("DefaultNotifBundler") { + override val bundleIds: List<String> + get() = listOf() + + override fun getBundleIdOrNull(entry: NotificationEntry?): String? { + return null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index ef3da9498f70..1e5aa01714be 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -83,6 +83,7 @@ import com.android.systemui.statusbar.notification.logging.dagger.NotificationsL import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractorImpl; import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; +import com.android.systemui.statusbar.notification.row.NotificationActionClickManager; import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactory; import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactoryLooperImpl; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; @@ -346,4 +347,5 @@ public interface NotificationsModule { /** Provides an instance of {@link EntryAdapterFactory} */ @Binds EntryAdapterFactory provideEntryAdapterFactory(EntryAdapterFactoryImpl impl); + } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt index 9728fcfcd6ba..25ae50c34659 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.notification.headsup import android.graphics.Region import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.notification.collection.EntryAdapter import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import dagger.Binds @@ -155,9 +154,9 @@ interface HeadsUpManager : Dumpable { fun setAnimationStateHandler(handler: AnimationStateHandler) /** - * Set an entry to be expanded and therefore stick in the heads up area if it's pinned until - * it's collapsed again. - */ + * Set an entry to be expanded and therefore stick in the heads up area if it's pinned until + * it's collapsed again. + */ fun setExpanded(key: String, row: ExpandableNotificationRow, expanded: Boolean) /** @@ -199,12 +198,12 @@ interface HeadsUpManager : Dumpable { * Notes that the user took an action on an entry that might indirectly cause the system or the * app to remove the notification. * - * @param entry the entry that might be indirectly removed by the user's action + * @param entry the key of the entry that might be indirectly removed by the user's action * @see * com.android.systemui.statusbar.notification.collection.coordinator.HeadsUpCoordinator.mActionPressListener * @see .canRemoveImmediately */ - fun setUserActionMayIndirectlyRemove(entry: NotificationEntry) + fun setUserActionMayIndirectlyRemove(entryKey: String) /** * Decides whether a click is invalid for a notification, i.e. it has not been shown long enough @@ -332,7 +331,7 @@ class HeadsUpManagerEmptyImpl @Inject constructor() : HeadsUpManager { override fun setUser(user: Int) {} - override fun setUserActionMayIndirectlyRemove(entry: NotificationEntry) {} + override fun setUserActionMayIndirectlyRemove(entryKey: String) {} override fun shouldSwallowClick(key: String): Boolean = false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java index ca94655318b9..ca83666079ec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java @@ -1126,8 +1126,8 @@ public class HeadsUpManagerImpl * @see HeadsUpCoordinator.mActionPressListener * @see #canRemoveImmediately(String) */ - public void setUserActionMayIndirectlyRemove(@NonNull NotificationEntry entry) { - HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey()); + public void setUserActionMayIndirectlyRemove(@NonNull String entryKey) { + HeadsUpEntry headsUpEntry = getHeadsUpEntry(entryKey); if (headsUpEntry != null) { headsUpEntry.mUserActionMayIndirectlyRemove = true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt index c3266fc57c2e..92c87e6403ec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt @@ -18,10 +18,10 @@ package com.android.systemui.statusbar.notification.init import android.service.notification.StatusBarNotification import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.media.NotificationMediaManager import com.android.systemui.people.widget.PeopleSpaceWidgetManager import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.NotificationListener -import com.android.systemui.statusbar.NotificationMediaManager import com.android.systemui.statusbar.NotificationPresenter import com.android.systemui.statusbar.notification.AnimatedImageNotificationManager import com.android.systemui.statusbar.notification.NotificationActivityStarter diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt index 1abd48c0c50b..a99ca072b6c8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt @@ -70,9 +70,6 @@ constructor( private fun OngoingCallModel.getNotifData(): NotifAndPromotedContent? = when (this) { is OngoingCallModel.InCall -> NotifAndPromotedContent(notificationKey, promotedContent) - is OngoingCallModel.InCallWithVisibleApp -> - // TODO(b/395989259): support InCallWithVisibleApp when it has notif data - null is OngoingCallModel.NoCall -> null } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index 6837cb2a6292..689222608abe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -128,9 +128,14 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView updateColors(); } - private void updateColors() { - if (usesTransparentBackground()) { - mNormalColor = SurfaceEffectColors.surfaceEffect1(getContext()); + protected void updateColors() { + if (notificationRowTransparency()) { + if (mIsBlurSupported) { + mNormalColor = SurfaceEffectColors.surfaceEffect1(getContext()); + } else { + mNormalColor = mContext.getColor( + com.android.internal.R.color.materialColorSurfaceContainer); + } } else { mNormalColor = mContext.getColor( com.android.internal.R.color.materialColorSurfaceContainerHigh); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt index 640d364895ae..dccc28f5fe49 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt @@ -34,11 +34,11 @@ import android.view.LayoutInflater import android.view.View import android.widget.ImageView import android.widget.LinearLayout -import android.widget.Switch import android.widget.TextView import com.android.settingslib.Utils import com.android.systemui.res.R import com.android.systemui.util.Assert +import com.google.android.material.materialswitch.MaterialSwitch /** Half-shelf for notification channel controls */ class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { @@ -139,12 +139,12 @@ class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, a class AppControlView(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { lateinit var iconView: ImageView lateinit var channelName: TextView - lateinit var switch: Switch + lateinit var switch: MaterialSwitch override fun onFinishInflate() { iconView = requireViewById(R.id.icon) channelName = requireViewById(R.id.app_name) - switch = requireViewById(R.id.toggle) + switch = requireViewById(R.id.material_toggle) setOnClickListener { switch.toggle() } } @@ -155,7 +155,7 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { lateinit var controller: ChannelEditorDialogController private lateinit var channelName: TextView private lateinit var channelDescription: TextView - private lateinit var switch: Switch + private lateinit var switch: MaterialSwitch private val highlightColor: Int var gentle = false @@ -175,7 +175,7 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { super.onFinishInflate() channelName = requireViewById(R.id.channel_name) channelDescription = requireViewById(R.id.channel_description) - switch = requireViewById(R.id.toggle) + switch = requireViewById(R.id.material_toggle) switch.setOnCheckedChangeListener { _, b -> channel?.let { controller.proposeEditForChannel( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 68ad4fad31c1..8da2f768bf71 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -19,9 +19,12 @@ package com.android.systemui.statusbar.notification.row; import static android.app.Flags.notificationsRedesignTemplates; import static android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY; import static android.service.notification.NotificationListenerService.REASON_CANCEL; +import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_EXPANDED; +import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED; import static com.android.systemui.Flags.notificationRowTransparency; import static com.android.systemui.Flags.notificationsPinnedHunInShade; +import static com.android.systemui.Flags.notificationRowAccessibilityExpanded; import static com.android.systemui.flags.Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE; import static com.android.systemui.statusbar.notification.NotificationUtils.logKey; import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED; @@ -65,6 +68,7 @@ import android.view.ViewGroup; import android.view.ViewParent; import android.view.ViewStub; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.widget.Chronometer; @@ -111,7 +115,6 @@ import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.NotificationEntryAdapter; import com.android.systemui.statusbar.notification.collection.PipelineEntry; import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; @@ -120,6 +123,7 @@ import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.headsup.PinnedStatus; import com.android.systemui.statusbar.notification.logging.NotificationCounters; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction; @@ -182,7 +186,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private boolean mShowSnooze = false; private boolean mIsFaded; - private boolean mIsPromotedOngoing = false; private boolean mHasStatusBarChipDuringHeadsUpAnimation = false; @Nullable @@ -406,10 +409,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } private void toggleExpansionState(View v, boolean shouldLogExpandClickMetric) { - boolean isGroupRoot = NotificationBundleUi.isEnabled() - ? mGroupMembershipManager.isGroupRoot(mEntryAdapter) - : mGroupMembershipManager.isGroupSummary(mEntry); - if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) && isGroupRoot) { + if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) && isGroupRoot()) { mGroupExpansionChanging = true; if (NotificationBundleUi.isEnabled()) { final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntryAdapter); @@ -871,7 +871,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private void updateLimitsForView(NotificationContentView layout) { final int maxExpandedHeight; - if (isPromotedOngoing()) { + if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { maxExpandedHeight = mMaxExpandedHeightForPromotedOngoing; } else { maxExpandedHeight = mMaxExpandedHeight; @@ -979,7 +979,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } else if (isAboveShelf() != wasAboveShelf) { mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf); } - updateBackgroundOpacity(); + updateColors(); } /** @@ -1350,7 +1350,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mIsSummaryWithChildren) { return mChildrenContainer.getIntrinsicHeight(); } - if (isPromotedOngoing()) { + if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { return getMaxExpandHeight(); } if (mExpandedWhenPinned) { @@ -2775,7 +2775,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return false; } - public void applyLaunchAnimationParams(LaunchAnimationParameters params) { if (params == null) { // `null` params indicates the animation is over, which means we can't access @@ -2940,7 +2939,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mIsSummaryWithChildren && !shouldShowPublic()) { return !mChildrenExpanded; } - if (isPromotedOngoing()) { + if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { return false; } return mEnableNonGroupedNotificationExpand && mExpandable; @@ -2951,17 +2950,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mPrivateLayout.updateExpandButtons(isExpandable()); } - /** - * Set this notification to be promoted ongoing - */ - public void setPromotedOngoing(boolean promotedOngoing) { - if (PromotedNotificationUiForceExpanded.isUnexpectedlyInLegacyMode()) { - return; - } - - mIsPromotedOngoing = promotedOngoing; - setExpandable(!mIsPromotedOngoing); - } /** * Sets whether the status bar is showing a chip corresponding to this notification. @@ -3062,10 +3050,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public void setUserLocked(boolean userLocked) { - if (isPromotedOngoing()) return; + if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) return; mUserLocked = userLocked; mPrivateLayout.setUserExpanding(userLocked); + if (android.app.Flags.expandingPublicView()) { + mPublicLayout.setUserExpanding(userLocked); + } // This is intentionally not guarded with mIsSummaryWithChildren since we might have had // children but not anymore. if (mChildrenContainer != null) { @@ -3122,7 +3113,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mChildrenContainer.setOnKeyguard(onKeyguard); } } - updateBackgroundOpacity(); + updateColors(); } } @@ -3193,6 +3184,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return mGroupExpansionManager.isGroupExpanded(mEntry); } + private boolean isGroupRoot() { + return NotificationBundleUi.isEnabled() + ? mGroupMembershipManager.isGroupRoot(mEntryAdapter) + : mGroupMembershipManager.isGroupSummary(mEntry); + } + private void onAttachedChildrenCountChanged() { final boolean wasSummary = mIsSummaryWithChildren; mIsSummaryWithChildren = mChildrenContainer != null @@ -3242,7 +3239,16 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public boolean isPromotedOngoing() { - return PromotedNotificationUiForceExpanded.isEnabled() && mIsPromotedOngoing; + if (!PromotedNotificationUi.isEnabled()) { + return false; + } + + final NotificationEntry entry = mEntry; + if (entry == null) { + return false; + } + + return entry.isPromotedOngoing(); } private boolean isPromotedNotificationExpanded(boolean allowOnKeyguard) { @@ -3271,6 +3277,27 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } /** + * Is this row currently showing an expanded state? This method is different from + * {@link #isExpanded()}, because it also handles groups, and pinned notifications. + */ + private boolean isShowingExpanded() { + if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) && isGroupRoot()) { + // is group and expanded? + return isGroupExpanded(); + } else if (mEnableNonGroupedNotificationExpand) { + if (isPinned()) { + // is pinned and expanded? + return mExpandedWhenPinned; + } else { + // is regular notification and expanded? + return isExpanded(); + } + } else { + return false; + } + } + + /** * Check whether the view state is currently expanded. This is given by the system in {@link * #setSystemExpanded(boolean)} and can be overridden by user expansion or * collapsing in {@link #setUserExpanded(boolean)}. Note that the visual appearance of this @@ -3283,7 +3310,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public boolean isExpanded(boolean allowOnKeyguard) { - if (isPromotedOngoing()) { + if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { return isPromotedNotificationExpanded(allowOnKeyguard); } @@ -3952,9 +3979,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView public void onExpandedByGesture(boolean userExpanded) { int event = MetricsEvent.ACTION_NOTIFICATION_GESTURE_EXPANDER; - if (NotificationBundleUi.isEnabled() - ? mGroupMembershipManager.isGroupRoot(mEntryAdapter) - : mGroupMembershipManager.isGroupSummary(mEntry)) { + if (isGroupRoot()) { event = MetricsEvent.ACTION_NOTIFICATION_GROUP_GESTURE_EXPANDER; } mMetricsLogger.action(event, userExpanded); @@ -4007,6 +4032,18 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mExpansionChangedListener != null) { mExpansionChangedListener.onExpansionChanged(nowExpanded); } + if (notificationRowAccessibilityExpanded()) { + notifyAccessibilityContentExpansionChanged(); + } + } + } + + private void notifyAccessibilityContentExpansionChanged() { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + AccessibilityEvent event = AccessibilityEvent.obtain(); + event.setEventType(TYPE_WINDOW_CONTENT_CHANGED); + event.setContentChangeTypes(CONTENT_CHANGE_TYPE_EXPANDED); + sendAccessibilityEventUnchecked(event); } } @@ -4037,33 +4074,50 @@ public class ExpandableNotificationRow extends ActivatableNotificationView super.onInitializeAccessibilityNodeInfoInternal(info); final boolean isLongClickable = isNotificationRowLongClickable(); if (isLongClickable) { - info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK); + info.addAction(AccessibilityAction.ACTION_LONG_CLICK); } info.setLongClickable(isLongClickable); if (canViewBeDismissed() && !mIsSnoozed) { - info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS); + info.addAction(AccessibilityAction.ACTION_DISMISS); } - boolean expandable = shouldShowPublic(); - boolean isExpanded = false; - if (!expandable) { - if (mIsSummaryWithChildren) { - expandable = true; - if (!mIsMinimized || isExpanded()) { - isExpanded = isGroupExpanded(); + + if (notificationRowAccessibilityExpanded()) { + if (isAccessibilityExpandable()) { + if (isShowingExpanded()) { + info.addAction(AccessibilityAction.ACTION_COLLAPSE); + info.setExpandedState(AccessibilityNodeInfo.EXPANDED_STATE_FULL); + } else { + info.addAction(AccessibilityAction.ACTION_EXPAND); + info.setExpandedState(AccessibilityNodeInfo.EXPANDED_STATE_COLLAPSED); } } else { - expandable = mPrivateLayout.isContentExpandable(); - isExpanded = isExpanded(); + info.setExpandedState(AccessibilityNodeInfo.EXPANDED_STATE_UNDEFINED); } - } - if (expandable && !mIsSnoozed) { - if (isExpanded) { - info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE); - } else { - info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); + } else { + boolean expandable = shouldShowPublic(); + boolean isExpanded = false; + if (!expandable) { + if (mIsSummaryWithChildren) { + expandable = true; + if (!mIsMinimized || isExpanded()) { + isExpanded = isGroupExpanded(); + } + } else { + expandable = mPrivateLayout.isContentExpandable(); + isExpanded = isExpanded(); + } + } + + if (expandable) { + if (isExpanded) { + info.addAction(AccessibilityAction.ACTION_COLLAPSE); + } else { + info.addAction(AccessibilityAction.ACTION_EXPAND); + } } } + NotificationMenuRowPlugin provider = getProvider(); if (provider != null) { MenuItem snoozeMenu = provider.getSnoozeMenuItem(getContext()); @@ -4076,6 +4130,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } + /** @return whether this row's expansion state can be toggled by an accessibility action. */ + private boolean isAccessibilityExpandable() { + // don't add expand accessibility actions to snoozed notifications + return !mIsSnoozed && isContentExpandable(); + } + @Override public boolean performAccessibilityActionInternal(int action, Bundle arguments) { if (super.performAccessibilityActionInternal(action, arguments)) { @@ -4339,8 +4399,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView + (!shouldShowPublic() && mIsSummaryWithChildren)); pw.print(", mShowNoBackground: " + mShowNoBackground); pw.print(", clipBounds: " + getClipBounds()); - if (PromotedNotificationUiForceExpanded.isEnabled()) { - pw.print(", isPromotedOngoing: " + isPromotedOngoing()); + pw.print(", isPromotedOngoing: " + isPromotedOngoing()); + if (notificationRowAccessibilityExpanded()) { + pw.print(", isShowingExpanded: " + isShowingExpanded()); + pw.print(", isAccessibilityExpandable: " + isAccessibilityExpandable()); } pw.print(", isExpandable: " + isExpandable()); pw.print(", mExpandable: " + mExpandable); @@ -4569,11 +4631,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } - private void updateBackgroundOpacity() { - if (mBackgroundNormal != null) { - // Row background should be opaque when it's displayed as a heads-up notification or - // displayed on keyguard. - mBackgroundNormal.setForceOpaque(mIsHeadsUp || mOnKeyguard); - } + @Override + protected boolean usesTransparentBackground() { + return super.usesTransparentBackground() && !mIsHeadsUp && !mOnKeyguard; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManager.kt new file mode 100644 index 000000000000..2b451406eaad --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManager.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.util.ListenerSet +import java.util.function.Consumer +import javax.inject.Inject + +/** + * Pipeline components can register consumers here to be informed when a notification action is + * clicked + */ +@SysUISingleton +class NotificationActionClickManager @Inject constructor() { + private val actionClickListeners = ListenerSet<Consumer<NotificationEntry>>() + + fun addActionClickListener(listener: Consumer<NotificationEntry>) { + actionClickListeners.addIfAbsent(listener) + } + + fun removeActionClickListener(listener: Consumer<NotificationEntry>) { + actionClickListeners.remove(listener) + } + + fun onNotificationActionClicked(entry: NotificationEntry) { + for (listener in actionClickListeners) { + listener.accept(entry) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java index e1219e88a405..4914e1073059 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java @@ -39,7 +39,6 @@ import androidx.annotation.Nullable; import com.android.internal.graphics.ColorUtils; import com.android.internal.util.ContrastColorUtil; import com.android.systemui.Dumpable; -import com.android.systemui.common.shared.colors.SurfaceEffectColors; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.shared.NotificationAddXOnHoverToDismiss; import com.android.systemui.util.DrawableDumpKt; @@ -156,14 +155,6 @@ public class NotificationBackgroundView extends View implements Dumpable, mBgIsColorized = b; } - /** Sets if the background should be opaque. */ - public void setForceOpaque(boolean forceOpaque) { - mForceOpaque = forceOpaque; - if (notificationRowTransparency()) { - updateBaseLayerColor(); - } - } - private Path calculateDismissButtonCutoutPath(Rect backgroundBounds) { // TODO(b/365585705): Adapt to RTL after the UX design is finalized. @@ -327,10 +318,7 @@ public class NotificationBackgroundView extends View implements Dumpable, // For colorized notifications, this uses a color that matches the tint color at 90% alpha. int color = isColorized() ? ColorUtils.setAlphaComponent(mTintColor, (int) (MAX_ALPHA * 0.9f)) - : SurfaceEffectColors.surfaceEffect1(getContext()); - if (mForceOpaque) { - color = ColorUtils.setAlphaComponent(color, MAX_ALPHA); - } + : mNormalColor; getBaseBackgroundLayer().setColorFilter( new PorterDuffColorFilter( color, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 51569c1596ef..3ffc052b5acc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -59,7 +59,6 @@ import com.android.systemui.statusbar.notification.NmSummarizationUiFlag; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; @@ -1006,10 +1005,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder entry.setPromotedNotificationContentModel(result.mPromotedContent); } - if (PromotedNotificationUiForceExpanded.isEnabled()) { - row.setPromotedOngoing(entry.isOngoingPromoted()); - } - boolean setRepliesAndActions = true; if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { if (result.inflatedContentView != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index 482b315aa14d..b1c145e08777 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -53,7 +53,6 @@ import com.android.systemui.statusbar.notification.NmSummarizationUiFlag import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED @@ -1293,6 +1292,7 @@ constructor( runningInflations, e, row, + entry, callback, logger, "applying view synchronously", @@ -1318,6 +1318,7 @@ constructor( runningInflations, InflationException(invalidReason), row, + entry, callback, logger, "applied invalid view", @@ -1377,6 +1378,7 @@ constructor( runningInflations, e, row, + entry, callback, logger, "applying view", @@ -1480,6 +1482,7 @@ constructor( runningInflations: HashMap<Int, CancellationSignal>, e: Exception, notification: ExpandableNotificationRow?, + entry: NotificationEntry, callback: InflationCallback?, logger: NotificationRowContentBinderLogger, logContext: String, @@ -1487,7 +1490,7 @@ constructor( Assert.isMainThread() logger.logAsyncTaskException(notification?.loggingKey, logContext, e) runningInflations.values.forEach(Consumer { obj: CancellationSignal -> obj.cancel() }) - callback?.handleInflationException(notification?.entry, e) + callback?.handleInflationException(entry, e) } /** @@ -1520,10 +1523,6 @@ constructor( entry.promotedNotificationContentModel = result.promotedContent } - if (PromotedNotificationUiForceExpanded.isEnabled) { - row.setPromotedOngoing(entry.isOngoingPromoted()) - } - result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) } setContentViewsFromRemoteViews( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt index f00c3ae20e30..53728c7da62d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt @@ -89,7 +89,8 @@ data class ActiveNotificationModel( init { if (!PromotedNotificationContentModel.featureFlagEnabled()) { if (promotedContent != null) { - Log.wtf(TAG, "passing non-null promoted content without feature flag enabled") + // TODO(b/401018545): convert to Log.wtf and fix tests (see: ag/32114199) + Log.e(TAG, "passing non-null promoted content without feature flag enabled") } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 9aa4c54c4292..e4e56c5de65b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -58,10 +58,10 @@ 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.log.SessionTracker; +import com.android.systemui.media.NotificationMediaManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.res.R; import com.android.systemui.scene.shared.model.Scenes; -import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.KeyguardStateController; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index fc721bfae369..a4ee4ad6f6ec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -140,6 +140,7 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.media.NotificationMediaManager; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.views.NavigationBarView; import com.android.systemui.notetask.NoteTaskController; @@ -191,7 +192,6 @@ import com.android.systemui.statusbar.LiftReveal; import com.android.systemui.statusbar.LightRevealScrim; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.NotificationLockscreenUserManager; -import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShadeDepthController; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java index 74b1c3bbfd77..2c8866fef030 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java @@ -38,6 +38,7 @@ import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.InitController; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor; +import com.android.systemui.media.NotificationMediaManager; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter.OnDismissAction; import com.android.systemui.power.domain.interactor.PowerInteractor; @@ -50,7 +51,6 @@ import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.NotificationLockscreenUserManager; -import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShadeWindowController; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index b6628926dc4b..61b7d80a8c85 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -25,7 +25,6 @@ import android.view.View import androidx.annotation.VisibleForTesting import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable -import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main @@ -162,6 +161,10 @@ constructor( notificationKey = currentInfo.key, appName = currentInfo.appName, promotedContent = currentInfo.promotedContent, + // [hasOngoingCall()] filters out the case in which the call is ongoing but the app + // is visible (we issue [OngoingCallModel.NoCall] below in that case), so this can + // be safely made false. + isAppVisible = false, ) } else { return OngoingCallModel.NoCall diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt index d6ca656356e3..bed9e7cf4646 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt @@ -51,9 +51,7 @@ import kotlinx.coroutines.flow.stateIn * This class monitors call notifications and the visibility of call apps to determine the * appropriate chip state. It emits: * * - [OngoingCallModel.NoCall] when there is no call notification - * * - [OngoingCallModel.InCallWithVisibleApp] when there is a call notification but the call app is - * visible - * * - [OngoingCallModel.InCall] when there is a call notification and the call app is not visible + * * - [OngoingCallModel.InCall] when there is a call notification */ @SysUISingleton class OngoingCallInteractor @@ -85,12 +83,14 @@ constructor( initialValue = OngoingCallModel.NoCall, ) + // TODO(b/400720280): maybe put this inside [OngoingCallModel]. @VisibleForTesting val isStatusBarRequiredForOngoingCall = combine(ongoingCallState, isChipSwipedAway) { callState, chipSwipedAway -> - callState is OngoingCallModel.InCall && !chipSwipedAway + callState.willCallChipBeVisible() && !chipSwipedAway } + // TODO(b/400720280): maybe put this inside [OngoingCallModel]. @VisibleForTesting val isGestureListeningEnabled = combine( @@ -98,9 +98,12 @@ constructor( statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode, isChipSwipedAway, ) { callState, isFullscreen, chipSwipedAway -> - callState is OngoingCallModel.InCall && !chipSwipedAway && isFullscreen + callState.willCallChipBeVisible() && !chipSwipedAway && isFullscreen } + private fun OngoingCallModel.willCallChipBeVisible() = + this is OngoingCallModel.InCall && !isAppVisible + private fun createOngoingCallStateFlow( notification: ActiveNotificationModel? ): Flow<OngoingCallModel> { @@ -147,34 +150,23 @@ constructor( model: ActiveNotificationModel, isVisible: Boolean, ): OngoingCallModel { - return when { - isVisible -> { - logger.d({ "Call app is visible: uid=$int1" }) { int1 = model.uid } - OngoingCallModel.InCallWithVisibleApp( - startTimeMs = model.whenTime, - notificationIconView = model.statusBarChipIconView, - intent = model.contentIntent, - notificationKey = model.key, - appName = model.appName, - promotedContent = model.promotedContent, - ) - } - - else -> { - logger.d({ "Active call detected: startTime=$long1 hasIcon=$bool1" }) { - long1 = model.whenTime - bool1 = model.statusBarChipIconView != null - } - OngoingCallModel.InCall( - startTimeMs = model.whenTime, - notificationIconView = model.statusBarChipIconView, - intent = model.contentIntent, - notificationKey = model.key, - appName = model.appName, - promotedContent = model.promotedContent, - ) - } + logger.d({ + "Active call detected: uid=$int1 startTime=$long1 hasIcon=$bool1 isAppVisible=$bool2" + }) { + int1 = model.uid + long1 = model.whenTime + bool1 = model.statusBarChipIconView != null + bool2 = isVisible } + return OngoingCallModel.InCall( + startTimeMs = model.whenTime, + notificationIconView = model.statusBarChipIconView, + intent = model.contentIntent, + notificationKey = model.key, + appName = model.appName, + promotedContent = model.promotedContent, + isAppVisible = isVisible, + ) } private fun setStatusBarRequiredForOngoingCall(statusBarRequired: Boolean) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt index 62f0ba032f36..322dfff8f144 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt @@ -26,25 +26,6 @@ sealed interface OngoingCallModel { data object NoCall : OngoingCallModel /** - * There is an ongoing call but the call app is currently visible, so we don't need to show the - * chip. - * - * @property startTimeMs see [InCall.startTimeMs]. - * @property notificationIconView see [InCall.notificationIconView]. - * @property intent see [InCall.intent]. - * @property appName see [InCall.appName]. - * @property promotedContent see [InCall.promotedContent]. - */ - data class InCallWithVisibleApp( - val startTimeMs: Long, - val notificationIconView: StatusBarIconView?, - val intent: PendingIntent?, - val notificationKey: String, - val appName: String, - val promotedContent: PromotedNotificationContentModel?, - ) : OngoingCallModel - - /** * There *is* an ongoing call. * * @property startTimeMs the time that the phone call started, based on the notification's @@ -58,6 +39,7 @@ sealed interface OngoingCallModel { * @property appName the user-readable name of the app that posted the call notification. * @property promotedContent if the call notification also meets promoted notification criteria, * this field is filled in with the content related to promotion. Otherwise null. + * @property isAppVisible whether the app to which the call belongs is currently visible. */ data class InCall( val startTimeMs: Long, @@ -66,5 +48,6 @@ sealed interface OngoingCallModel { val notificationKey: String, val appName: String, val promotedContent: PromotedNotificationContentModel?, + val isAppVisible: Boolean, ) : OngoingCallModel } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 29528502aa03..db1977b3ff45 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -45,6 +45,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.Mobil import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryKairosImpl import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorKairosAdapter import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorKairosImpl import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel @@ -92,8 +93,10 @@ import kotlinx.coroutines.flow.Flow DemoModeMobileConnectionDataSourceKairosImpl.Module::class, MobileRepositorySwitcherKairos.Module::class, MobileConnectionsRepositoryKairosImpl.Module::class, + MobileIconsInteractorKairosImpl.Module::class, MobileConnectionRepositoryKairosFactoryImpl.Module::class, MobileConnectionsRepositoryKairosAdapter.Module::class, + MobileIconsInteractorKairosAdapter.Module::class, ] ) abstract class StatusBarPipelineModule { @@ -171,7 +174,7 @@ abstract class StatusBarPipelineModule { @Provides fun mobileIconsInteractor( impl: Provider<MobileIconsInteractorImpl>, - kairosImpl: Provider<MobileIconsInteractorKairosImpl>, + kairosImpl: Provider<MobileIconsInteractorKairosAdapter>, ): MobileIconsInteractor { return if (Flags.statusBarMobileIconKairos()) { kairosImpl.get() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt index 4580ad974b29..a9399593973b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,38 +22,37 @@ import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.graph.SignalDrawable import com.android.settingslib.mobile.MobileIconCarrierIdOverrides import com.android.settingslib.mobile.MobileIconCarrierIdOverridesImpl -import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.KairosBuilder +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.State +import com.android.systemui.kairos.combine +import com.android.systemui.kairos.flatMap +import com.android.systemui.kairos.map +import com.android.systemui.kairosBuilder import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel.DefaultIcon import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel.OverriddenIcon import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn +@ExperimentalKairosApi interface MobileIconInteractorKairos { /** The table log created for this connection */ val tableLogBuffer: TableLogBuffer /** The current mobile data activity */ - val activity: Flow<DataActivityModel> + val activity: State<DataActivityModel> /** See [MobileConnectionsRepository.mobileIsDefault]. */ - val mobileIsDefault: Flow<Boolean> + val mobileIsDefault: State<Boolean> /** * True when telephony tells us that the data state is CONNECTED. See @@ -61,31 +60,31 @@ interface MobileIconInteractorKairos { * consider this connection to be serving data, and thus want to show a network type icon, when * data is connected. Other data connection states would typically cause us not to show the icon */ - val isDataConnected: StateFlow<Boolean> + val isDataConnected: State<Boolean> /** True if we consider this connection to be in service, i.e. can make calls */ - val isInService: StateFlow<Boolean> + val isInService: State<Boolean> /** True if this connection is emergency only */ - val isEmergencyOnly: StateFlow<Boolean> + val isEmergencyOnly: State<Boolean> /** Observable for the data enabled state of this connection */ - val isDataEnabled: StateFlow<Boolean> + val isDataEnabled: State<Boolean> /** True if the RAT icon should always be displayed and false otherwise. */ - val alwaysShowDataRatIcon: StateFlow<Boolean> + val alwaysShowDataRatIcon: State<Boolean> /** Canonical representation of the current mobile signal strength as a triangle. */ - val signalLevelIcon: StateFlow<SignalIconModel> + val signalLevelIcon: State<SignalIconModel> /** Observable for RAT type (network type) indicator */ - val networkTypeIconGroup: StateFlow<NetworkTypeIconModel> + val networkTypeIconGroup: State<NetworkTypeIconModel> /** Whether or not to show the slice attribution */ - val showSliceAttribution: StateFlow<Boolean> + val showSliceAttribution: State<Boolean> /** True if this connection is satellite-based */ - val isNonTerrestrial: StateFlow<Boolean> + val isNonTerrestrial: State<Boolean> /** * Provider name for this network connection. The name can be one of 3 values: @@ -95,7 +94,7 @@ interface MobileIconInteractorKairos { * override in [connectionInfo.operatorAlphaShort], a value that is derived from * [ServiceState] */ - val networkName: StateFlow<NetworkNameModel> + val networkName: State<NetworkNameModel> /** * Provider name for this network connection. The name can be one of 3 values: @@ -108,119 +107,110 @@ interface MobileIconInteractorKairos { * TODO(b/296600321): De-duplicate this field with [networkName] after determining the data * provided is identical */ - val carrierName: StateFlow<String> + val carrierName: State<String> /** True if there is only one active subscription. */ - val isSingleCarrier: StateFlow<Boolean> + val isSingleCarrier: State<Boolean> /** * True if this connection is considered roaming. The roaming bit can come from [ServiceState], * or directly from the telephony manager's CDMA ERI number value. Note that we don't consider a * connection to be roaming while carrier network change is active */ - val isRoaming: StateFlow<Boolean> + val isRoaming: State<Boolean> /** See [MobileIconsInteractor.isForceHidden]. */ - val isForceHidden: Flow<Boolean> + val isForceHidden: State<Boolean> /** See [MobileConnectionRepository.isAllowedDuringAirplaneMode]. */ - val isAllowedDuringAirplaneMode: StateFlow<Boolean> + val isAllowedDuringAirplaneMode: State<Boolean> /** True when in carrier network change mode */ - val carrierNetworkChangeActive: StateFlow<Boolean> + val carrierNetworkChangeActive: State<Boolean> } /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */ -@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@ExperimentalKairosApi class MobileIconInteractorKairosImpl( - @Background scope: CoroutineScope, - defaultSubscriptionHasDataEnabled: StateFlow<Boolean>, - override val alwaysShowDataRatIcon: StateFlow<Boolean>, - alwaysUseCdmaLevel: StateFlow<Boolean>, - override val isSingleCarrier: StateFlow<Boolean>, - override val mobileIsDefault: StateFlow<Boolean>, - defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>, - defaultMobileIconGroup: StateFlow<MobileIconGroup>, - isDefaultConnectionFailed: StateFlow<Boolean>, - override val isForceHidden: Flow<Boolean>, - connectionRepository: MobileConnectionRepository, + defaultSubscriptionHasDataEnabled: State<Boolean>, + override val alwaysShowDataRatIcon: State<Boolean>, + alwaysUseCdmaLevel: State<Boolean>, + override val isSingleCarrier: State<Boolean>, + override val mobileIsDefault: State<Boolean>, + defaultMobileIconMapping: State<Map<String, MobileIconGroup>>, + defaultMobileIconGroup: State<MobileIconGroup>, + isDefaultConnectionFailed: State<Boolean>, + override val isForceHidden: State<Boolean>, + private val connectionRepository: MobileConnectionRepositoryKairos, private val context: Context, - val carrierIdOverrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl(), -) : MobileIconInteractor, MobileIconInteractorKairos { - override val tableLogBuffer: TableLogBuffer = connectionRepository.tableLogBuffer + private val carrierIdOverrides: MobileIconCarrierIdOverrides = + MobileIconCarrierIdOverridesImpl(), +) : MobileIconInteractorKairos, KairosBuilder by kairosBuilder() { + override val tableLogBuffer: TableLogBuffer + get() = connectionRepository.tableLogBuffer - override val activity = connectionRepository.dataActivityDirection + override val activity: State<DataActivityModel> + get() = connectionRepository.dataActivityDirection - override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled + override val isDataEnabled: State<Boolean> = connectionRepository.dataEnabled - override val carrierNetworkChangeActive: StateFlow<Boolean> = - connectionRepository.carrierNetworkChangeActive + override val carrierNetworkChangeActive: State<Boolean> + get() = connectionRepository.carrierNetworkChangeActive // True if there exists _any_ icon override for this carrierId. Note that overrides can include // any or none of the icon groups defined in MobileMappings, so we still need to check on a // per-network-type basis whether or not the given icon group is overridden - private val carrierIdIconOverrideExists = - connectionRepository.carrierId - .map { carrierIdOverrides.carrierIdEntryExists(it) } - .distinctUntilChanged() - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + private val carrierIdIconOverrideExists: State<Boolean> = + connectionRepository.carrierId.map { carrierIdOverrides.carrierIdEntryExists(it) } - override val networkName = + override val networkName: State<NetworkNameModel> = combine(connectionRepository.operatorAlphaShort, connectionRepository.networkName) { - operatorAlphaShort, - networkName -> - if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) { - NetworkNameModel.IntentDerived(operatorAlphaShort) - } else { - networkName - } + operatorAlphaShort, + networkName -> + if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) { + NetworkNameModel.IntentDerived(operatorAlphaShort) + } else { + networkName } - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - connectionRepository.networkName.value, - ) + } - override val carrierName = + override val carrierName: State<String> = combine(connectionRepository.operatorAlphaShort, connectionRepository.carrierName) { - operatorAlphaShort, - networkName -> - if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) { - operatorAlphaShort - } else { - networkName.name - } + operatorAlphaShort, + networkName -> + if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) { + operatorAlphaShort + } else { + networkName.name } - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - connectionRepository.carrierName.value.name, - ) + } /** What the mobile icon would be before carrierId overrides */ - private val defaultNetworkType: StateFlow<MobileIconGroup> = + private val defaultNetworkType: State<MobileIconGroup> = combine( - connectionRepository.resolvedNetworkType, - defaultMobileIconMapping, - defaultMobileIconGroup, - ) { resolvedNetworkType, mapping, defaultGroup -> - when (resolvedNetworkType) { - is ResolvedNetworkType.CarrierMergedNetworkType -> - resolvedNetworkType.iconGroupOverride - else -> { - mapping[resolvedNetworkType.lookupKey] ?: defaultGroup - } + connectionRepository.resolvedNetworkType, + defaultMobileIconMapping, + defaultMobileIconGroup, + ) { resolvedNetworkType, mapping, defaultGroup -> + when (resolvedNetworkType) { + is ResolvedNetworkType.CarrierMergedNetworkType -> + resolvedNetworkType.iconGroupOverride + + else -> { + mapping[resolvedNetworkType.lookupKey] ?: defaultGroup } } - .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value) + } - override val networkTypeIconGroup = - combine(defaultNetworkType, carrierIdIconOverrideExists) { networkType, overrideExists -> + override val networkTypeIconGroup: State<NetworkTypeIconModel> = buildState { + combineTransactionally(defaultNetworkType, carrierIdIconOverrideExists) { + networkType, + overrideExists -> // DefaultIcon comes out of the icongroup lookup, we check for overrides here if (overrideExists) { val iconOverride = carrierIdOverrides.getOverrideFor( - connectionRepository.carrierId.value, + connectionRepository.carrierId.sample(), networkType.name, context.resources, ) @@ -233,106 +223,101 @@ class MobileIconInteractorKairosImpl( DefaultIcon(networkType) } } - .distinctUntilChanged() - .logDiffsForTable( - tableLogBuffer = tableLogBuffer, - initialValue = DefaultIcon(defaultMobileIconGroup.value), - ) - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - DefaultIcon(defaultMobileIconGroup.value), - ) + .also { logDiffsForTable(it, tableLogBuffer = tableLogBuffer) } + } - override val showSliceAttribution: StateFlow<Boolean> = + override val showSliceAttribution: State<Boolean> = combine( - connectionRepository.allowNetworkSliceIndicator, - connectionRepository.hasPrioritizedNetworkCapabilities, - ) { allowed, hasPrioritizedNetworkCapabilities -> - allowed && hasPrioritizedNetworkCapabilities - } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + connectionRepository.allowNetworkSliceIndicator, + connectionRepository.hasPrioritizedNetworkCapabilities, + ) { allowed, hasPrioritizedNetworkCapabilities -> + allowed && hasPrioritizedNetworkCapabilities + } - override val isNonTerrestrial: StateFlow<Boolean> = connectionRepository.isNonTerrestrial + override val isNonTerrestrial: State<Boolean> + get() = connectionRepository.isNonTerrestrial - override val isRoaming: StateFlow<Boolean> = + override val isRoaming: State<Boolean> = combine( - connectionRepository.carrierNetworkChangeActive, - connectionRepository.isGsm, - connectionRepository.isRoaming, - connectionRepository.cdmaRoaming, - ) { carrierNetworkChangeActive, isGsm, isRoaming, cdmaRoaming -> - if (carrierNetworkChangeActive) { - false - } else if (isGsm) { - isRoaming - } else { - cdmaRoaming - } + connectionRepository.carrierNetworkChangeActive, + connectionRepository.isGsm, + connectionRepository.isRoaming, + connectionRepository.cdmaRoaming, + ) { carrierNetworkChangeActive, isGsm, isRoaming, cdmaRoaming -> + if (carrierNetworkChangeActive) { + false + } else if (isGsm) { + isRoaming + } else { + cdmaRoaming } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + } - private val level: StateFlow<Int> = + private val level: State<Int> = combine( - connectionRepository.isGsm, - connectionRepository.primaryLevel, - connectionRepository.cdmaLevel, - alwaysUseCdmaLevel, - ) { isGsm, primaryLevel, cdmaLevel, alwaysUseCdmaLevel -> - when { - // GSM connections should never use the CDMA level - isGsm -> primaryLevel - alwaysUseCdmaLevel -> cdmaLevel - else -> primaryLevel - } + connectionRepository.isGsm, + connectionRepository.primaryLevel, + connectionRepository.cdmaLevel, + alwaysUseCdmaLevel, + ) { isGsm, primaryLevel, cdmaLevel, alwaysUseCdmaLevel -> + when { + // GSM connections should never use the CDMA level + isGsm -> primaryLevel + alwaysUseCdmaLevel -> cdmaLevel + else -> primaryLevel } - .stateIn(scope, SharingStarted.WhileSubscribed(), 0) + } - private val numberOfLevels: StateFlow<Int> = connectionRepository.numberOfLevels + private val numberOfLevels: State<Int> + get() = connectionRepository.numberOfLevels - override val isDataConnected: StateFlow<Boolean> = + override val isDataConnected: State<Boolean> = connectionRepository.dataConnectionState .map { it == Connected } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + .also { + onActivated { logDiffsForTable(it, tableLogBuffer, "icon", "isDataConnected") } + } - override val isInService = connectionRepository.isInService + override val isInService + get() = connectionRepository.isInService - override val isEmergencyOnly: StateFlow<Boolean> = connectionRepository.isEmergencyOnly + override val isEmergencyOnly: State<Boolean> + get() = connectionRepository.isEmergencyOnly - override val isAllowedDuringAirplaneMode = connectionRepository.isAllowedDuringAirplaneMode + override val isAllowedDuringAirplaneMode: State<Boolean> + get() = connectionRepository.isAllowedDuringAirplaneMode /** Whether or not to show the error state of [SignalDrawable] */ - private val showExclamationMark: StateFlow<Boolean> = + private val showExclamationMark: State<Boolean> = combine(defaultSubscriptionHasDataEnabled, isDefaultConnectionFailed, isInService) { - isDefaultDataEnabled, - isDefaultConnectionFailed, - isInService -> - !isDefaultDataEnabled || isDefaultConnectionFailed || !isInService - } - .stateIn(scope, SharingStarted.WhileSubscribed(), true) + isDefaultDataEnabled, + isDefaultConnectionFailed, + isInService -> + !isDefaultDataEnabled || isDefaultConnectionFailed || !isInService + } - private val cellularShownLevel: StateFlow<Int> = + private val cellularShownLevel: State<Int> = combine(level, isInService, connectionRepository.inflateSignalStrength) { - level, - isInService, - inflate -> - if (isInService) { - if (inflate) level + 1 else level - } else 0 + level, + isInService, + inflate -> + when { + !isInService -> 0 + inflate -> level + 1 + else -> level } - .stateIn(scope, SharingStarted.WhileSubscribed(), 0) + } // Satellite level is unaffected by the inflateSignalStrength property // See b/346904529 for details - private val satelliteShownLevel: StateFlow<Int> = + private val satelliteShownLevel: State<Int> = if (Flags.carrierRoamingNbIotNtn()) { - connectionRepository.satelliteLevel - } else { - combine(level, isInService) { level, isInService -> if (isInService) level else 0 } - } - .stateIn(scope, SharingStarted.WhileSubscribed(), 0) + connectionRepository.satelliteLevel + } else { + combine(level, isInService) { level, isInService -> if (isInService) level else 0 } + } - private val cellularIcon: Flow<SignalIconModel.Cellular> = + private val cellularIcon: State<SignalIconModel.Cellular> = combine( cellularShownLevel, numberOfLevels, @@ -347,7 +332,7 @@ class MobileIconInteractorKairosImpl( ) } - private val satelliteIcon: Flow<SignalIconModel.Satellite> = + private val satelliteIcon: State<SignalIconModel.Satellite> = satelliteShownLevel.map { SignalIconModel.Satellite( level = it, @@ -357,24 +342,14 @@ class MobileIconInteractorKairosImpl( ) } - override val signalLevelIcon: StateFlow<SignalIconModel> = run { - val initial = - SignalIconModel.Cellular( - cellularShownLevel.value, - numberOfLevels.value, - showExclamationMark.value, - carrierNetworkChangeActive.value, - ) + override val signalLevelIcon: State<SignalIconModel> = isNonTerrestrial - .flatMapLatest { ntn -> + .flatMap { ntn -> if (ntn) { satelliteIcon } else { cellularIcon } } - .distinctUntilChanged() - .logDiffsForTable(tableLogBuffer, columnPrefix = "icon", initialValue = initial) - .stateIn(scope, SharingStarted.WhileSubscribed(), initial) - } + .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnPrefix = "icon") } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt new file mode 100644 index 000000000000..a3b9b17257cf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.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 com.android.systemui.statusbar.pipeline.mobile.domain.interactor + +import com.android.systemui.kairos.BuildScope +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.toColdConflatedFlow +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel +import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +@ExperimentalKairosApi +fun BuildScope.MobileIconInteractorKairosAdapter( + kairosImpl: MobileIconInteractorKairos +): MobileIconInteractor = + with(kairosImpl) { + MobileIconInteractorKairosAdapter( + tableLogBuffer = tableLogBuffer, + activity = activity.toColdConflatedFlow(kairosNetwork), + mobileIsDefault = mobileIsDefault.toColdConflatedFlow(kairosNetwork), + isDataConnected = isDataConnected.toStateFlow(), + isInService = isInService.toStateFlow(), + isEmergencyOnly = isEmergencyOnly.toStateFlow(), + isDataEnabled = isDataEnabled.toStateFlow(), + alwaysShowDataRatIcon = alwaysShowDataRatIcon.toStateFlow(), + signalLevelIcon = signalLevelIcon.toStateFlow(), + networkTypeIconGroup = networkTypeIconGroup.toStateFlow(), + showSliceAttribution = showSliceAttribution.toStateFlow(), + isNonTerrestrial = isNonTerrestrial.toStateFlow(), + networkName = networkName.toStateFlow(), + carrierName = carrierName.toStateFlow(), + isSingleCarrier = isSingleCarrier.toStateFlow(), + isRoaming = isRoaming.toStateFlow(), + isForceHidden = isForceHidden.toColdConflatedFlow(kairosNetwork), + isAllowedDuringAirplaneMode = isAllowedDuringAirplaneMode.toStateFlow(), + carrierNetworkChangeActive = carrierNetworkChangeActive.toStateFlow(), + ) + } + +private class MobileIconInteractorKairosAdapter( + override val tableLogBuffer: TableLogBuffer, + override val activity: Flow<DataActivityModel>, + override val mobileIsDefault: Flow<Boolean>, + override val isDataConnected: StateFlow<Boolean>, + override val isInService: StateFlow<Boolean>, + override val isEmergencyOnly: StateFlow<Boolean>, + override val isDataEnabled: StateFlow<Boolean>, + override val alwaysShowDataRatIcon: StateFlow<Boolean>, + override val signalLevelIcon: StateFlow<SignalIconModel>, + override val networkTypeIconGroup: StateFlow<NetworkTypeIconModel>, + override val showSliceAttribution: StateFlow<Boolean>, + override val isNonTerrestrial: StateFlow<Boolean>, + override val networkName: StateFlow<NetworkNameModel>, + override val carrierName: StateFlow<String>, + override val isSingleCarrier: StateFlow<Boolean>, + override val isRoaming: StateFlow<Boolean>, + override val isForceHidden: Flow<Boolean>, + override val isAllowedDuringAirplaneMode: StateFlow<Boolean>, + override val carrierNetworkChangeActive: StateFlow<Boolean>, +) : MobileIconInteractor diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt index e8e0a833af2a..14a276b60933 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,41 +21,47 @@ import android.telephony.CarrierConfigManager import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager.PROFILE_CLASS_PROVISIONING import com.android.settingslib.SignalIcon.MobileIconGroup -import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.Flags +import com.android.systemui.KairosActivatable +import com.android.systemui.KairosBuilder +import com.android.systemui.activated import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS +import com.android.systemui.kairos.BuildScope +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.Incremental +import com.android.systemui.kairos.State +import com.android.systemui.kairos.asyncEvent +import com.android.systemui.kairos.buildSpec +import com.android.systemui.kairos.combine +import com.android.systemui.kairos.filter +import com.android.systemui.kairos.flatMap +import com.android.systemui.kairos.flatten +import com.android.systemui.kairos.map +import com.android.systemui.kairos.mapValues +import com.android.systemui.kairos.stateOf +import com.android.systemui.kairosBuilder import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.core.NewStatusBarIcons import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel -import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository -import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryKairos import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository import com.android.systemui.util.CarrierConfigTracker -import java.lang.ref.WeakReference +import dagger.Binds +import dagger.Provides +import dagger.multibindings.ElementsIntoSet import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi +import javax.inject.Provider +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.transformLatest /** * Business layer logic for the set of mobile subscription icons. @@ -67,98 +73,79 @@ import kotlinx.coroutines.flow.transformLatest * represents each RAT (LTE, 3G, etc.), as well as can produce an interactor for each individual * icon */ +@ExperimentalKairosApi interface MobileIconsInteractorKairos { /** See [MobileConnectionsRepository.mobileIsDefault]. */ - val mobileIsDefault: StateFlow<Boolean> + val mobileIsDefault: State<Boolean> /** List of subscriptions, potentially filtered for CBRS */ - val filteredSubscriptions: Flow<List<SubscriptionModel>> - - /** Subscription ID of the current default data subscription */ - val defaultDataSubId: Flow<Int?> + val filteredSubscriptions: State<List<SubscriptionModel>> /** * The current list of [MobileIconInteractor]s associated with the current list of * [filteredSubscriptions] */ - val icons: StateFlow<List<MobileIconInteractor>> + val icons: Incremental<Int, MobileIconInteractorKairos> /** Whether the mobile icons can be stacked vertically. */ - val isStackable: StateFlow<Boolean> - - /** - * Observable for the subscriptionId of the current mobile data connection. Null if we don't - * have a valid subscription id - */ - val activeMobileDataSubscriptionId: StateFlow<Int?> + val isStackable: State<Boolean> /** True if the active mobile data subscription has data enabled */ - val activeDataConnectionHasDataEnabled: StateFlow<Boolean> + val activeDataConnectionHasDataEnabled: State<Boolean> /** * Flow providing a reference to the Interactor for the active data subId. This represents the - * [MobileIconInteractor] responsible for the active data connection, if any. + * [MobileIconInteractorKairos] responsible for the active data connection, if any. */ - val activeDataIconInteractor: StateFlow<MobileIconInteractor?> + val activeDataIconInteractor: State<MobileIconInteractorKairos?> /** True if the RAT icon should always be displayed and false otherwise. */ - val alwaysShowDataRatIcon: StateFlow<Boolean> + val alwaysShowDataRatIcon: State<Boolean> /** True if the CDMA level should be preferred over the primary level. */ - val alwaysUseCdmaLevel: StateFlow<Boolean> + val alwaysUseCdmaLevel: State<Boolean> /** True if there is only one active subscription. */ - val isSingleCarrier: StateFlow<Boolean> + val isSingleCarrier: State<Boolean> /** The icon mapping from network type to [MobileIconGroup] for the default subscription */ - val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> + val defaultMobileIconMapping: State<Map<String, MobileIconGroup>> /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */ - val defaultMobileIconGroup: StateFlow<MobileIconGroup> + val defaultMobileIconGroup: State<MobileIconGroup> /** True only if the default network is mobile, and validation also failed */ - val isDefaultConnectionFailed: StateFlow<Boolean> + val isDefaultConnectionFailed: State<Boolean> /** True once the user has been set up */ - val isUserSetUp: StateFlow<Boolean> + val isUserSetUp: State<Boolean> /** True if we're configured to force-hide the mobile icons and false otherwise. */ - val isForceHidden: Flow<Boolean> + val isForceHidden: State<Boolean> /** * True if the device-level service state (with -1 subscription id) reports emergency calls * only. This value is only useful when there are no other subscriptions OR all existing * subscriptions report that they are not in service. */ - val isDeviceInEmergencyCallsOnlyMode: Flow<Boolean> - - /** - * Vends out a [MobileIconInteractor] tracking the [MobileConnectionRepository] for the given - * subId. - */ - fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor + val isDeviceInEmergencyCallsOnlyMode: State<Boolean> } -@OptIn(ExperimentalCoroutinesApi::class) -@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@ExperimentalKairosApi @SysUISingleton class MobileIconsInteractorKairosImpl @Inject constructor( - private val mobileConnectionsRepo: MobileConnectionsRepository, + private val mobileConnectionsRepo: MobileConnectionsRepositoryKairos, private val carrierConfigTracker: CarrierConfigTracker, @MobileSummaryLog private val tableLogger: TableLogBuffer, connectivityRepository: ConnectivityRepository, userSetupRepo: UserSetupRepository, - @Background private val scope: CoroutineScope, private val context: Context, private val featureFlagsClassic: FeatureFlagsClassic, -) : MobileIconsInteractor, MobileIconsInteractorKairos { - - // Weak reference lookup for created interactors - private val reuseCache = mutableMapOf<Int, WeakReference<MobileIconInteractor>>() +) : MobileIconsInteractorKairos, KairosBuilder by kairosBuilder() { - override val mobileIsDefault = + override val mobileIsDefault: State<Boolean> = combine( mobileConnectionsRepo.mobileIsDefault, mobileConnectionsRepo.hasCarrierMergedConnection, @@ -167,47 +154,36 @@ constructor( // the `isDefault` calculation. See b/272586234. mobileIsDefault || hasCarrierMergedConnection } - .logDiffsForTable( - tableLogger, - LOGGING_PREFIX, - columnName = "mobileIsDefault", - initialValue = false, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), false) - - override val activeMobileDataSubscriptionId: StateFlow<Int?> = - mobileConnectionsRepo.activeMobileDataSubscriptionId - - override val activeDataConnectionHasDataEnabled: StateFlow<Boolean> = - mobileConnectionsRepo.activeMobileDataRepository - .flatMapLatest { it?.dataEnabled ?: flowOf(false) } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) - - override val activeDataIconInteractor: StateFlow<MobileIconInteractor?> = - mobileConnectionsRepo.activeMobileDataSubscriptionId - .mapLatest { - if (it != null) { - getMobileConnectionInteractorForSubId(it) - } else { - null + .also { + onActivated { + logDiffsForTable( + it, + tableLogger, + LOGGING_PREFIX, + columnName = "mobileIsDefault", + ) } } - .stateIn(scope, SharingStarted.WhileSubscribed(), null) - private val unfilteredSubscriptions: Flow<List<SubscriptionModel>> = - mobileConnectionsRepo.subscriptions + override val activeDataConnectionHasDataEnabled: State<Boolean> = + mobileConnectionsRepo.activeMobileDataRepository.flatMap { + it?.dataEnabled ?: stateOf(false) + } + + private val unfilteredSubscriptions: State<Collection<SubscriptionModel>> + get() = mobileConnectionsRepo.subscriptions /** Any filtering that we can do based purely on the info of each subscription individually. */ - private val subscriptionsBasedFilteredSubs = - unfilteredSubscriptions - .map { it.filterBasedOnProvisioning().filterBasedOnNtn() } - .distinctUntilChanged() + private val subscriptionsBasedFilteredSubs: State<List<SubscriptionModel>> = + unfilteredSubscriptions.map { + it.asSequence().filterBasedOnProvisioning().filterBasedOnNtn().toList() + } - private fun List<SubscriptionModel>.filterBasedOnProvisioning(): List<SubscriptionModel> = + private fun Sequence<SubscriptionModel>.filterBasedOnProvisioning() = if (!featureFlagsClassic.isEnabled(FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS)) { this } else { - this.filter { it.profileClass != PROFILE_CLASS_PROVISIONING } + filter { it.profileClass != PROFILE_CLASS_PROVISIONING } } /** @@ -219,9 +195,10 @@ constructor( * need to filter out those subscriptions here so we guarantee the subscription never turns into * an icon. See b/336881301. */ - private fun List<SubscriptionModel>.filterBasedOnNtn(): List<SubscriptionModel> { - return this.filter { !it.isExclusivelyNonTerrestrial } - } + private fun Sequence<SubscriptionModel>.filterBasedOnNtn(): Sequence<SubscriptionModel> = + filter { + !it.isExclusivelyNonTerrestrial + } /** * Generally, SystemUI wants to show iconography for each subscription that is listed by @@ -236,22 +213,23 @@ constructor( * [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN], * and by checking which subscription is opportunistic, or which one is active. */ - override val filteredSubscriptions: Flow<List<SubscriptionModel>> = + override val filteredSubscriptions: State<List<SubscriptionModel>> = buildState { combine( subscriptionsBasedFilteredSubs, mobileConnectionsRepo.activeMobileDataSubscriptionId, - connectivityRepository.vcnSubId, + connectivityRepository.vcnSubId.toState(), ) { preFilteredSubs, activeId, vcnSubId -> filterSubsBasedOnOpportunistic(preFilteredSubs, activeId, vcnSubId) } - .distinctUntilChanged() - .logDiffsForTable( - tableLogger, - LOGGING_PREFIX, - columnName = "filteredSubscriptions", - initialValue = listOf(), - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) + .also { + logDiffsForTable( + it, + tableLogger, + LOGGING_PREFIX, + columnName = "filteredSubscriptions", + ) + } + } private fun filterSubsBasedOnOpportunistic( subList: List<SubscriptionModel>, @@ -298,19 +276,25 @@ constructor( } } - override val defaultDataSubId = mobileConnectionsRepo.defaultDataSubId - - override val icons = - filteredSubscriptions - .mapLatest { subs -> - subs.map { getMobileConnectionInteractorForSubId(it.subscriptionId) } + override val icons: Incremental<Int, MobileIconInteractorKairos> = buildIncremental { + val filteredSubIds = + filteredSubscriptions.map { it.asSequence().map { sub -> sub.subscriptionId }.toSet() } + mobileConnectionsRepo.mobileConnectionsBySubId + .filterIncrementally { (subId, _) -> + // Filter out repo if subId is not present in the filtered set + filteredSubIds.map { subId in it } } - .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) + // Just map the repos to interactors + .mapValues { (subId, repo) -> buildSpec { mobileConnection(repo) } } + .applyLatestSpecForKey() + } - override val isStackable = + override val isStackable: State<Boolean> = if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) { - icons.flatMapLatest { icons -> - combine(icons.map { it.signalLevelIcon }) { signalLevelIcons -> + icons.flatMap { iconsBySubId: Map<Int, MobileIconInteractorKairos> -> + iconsBySubId.values + .map { it.signalLevelIcon } + .combine { signalLevelIcons -> // These are only stackable if: // - They are cellular // - There's exactly two @@ -319,11 +303,15 @@ constructor( it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels } } - } - } else { - flowOf(false) } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + } else { + stateOf(false) + } + + override val activeDataIconInteractor: State<MobileIconInteractorKairos?> = + combine(mobileConnectionsRepo.activeMobileDataSubscriptionId, icons) { activeSubId, icons -> + activeSubId?.let { icons[activeSubId] } + } /** * Copied from the old pipeline. We maintain a 2s period of time where we will keep the @@ -335,67 +323,59 @@ constructor( * * The goal of this is to minimize the flickering in the UI of the cellular indicator */ - private val forcingCellularValidation = + private val forcingCellularValidation: State<Boolean> = buildState { mobileConnectionsRepo.activeSubChangedInGroupEvent - .filter { mobileConnectionsRepo.defaultConnectionIsValidated.value } - .transformLatest { - emit(true) - delay(2000) - emit(false) + .filter(mobileConnectionsRepo.defaultConnectionIsValidated) + .mapLatestBuild { + asyncEvent { + delay(2.seconds) + false + } + .holdState(true) + } + .holdState(stateOf(false)) + .flatten() + .also { + logDiffsForTable(it, tableLogger, LOGGING_PREFIX, columnName = "forcingValidation") } - .logDiffsForTable( - tableLogger, - LOGGING_PREFIX, - columnName = "forcingValidation", - initialValue = false, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + } /** * Mapping from network type to [MobileIconGroup] using the config generated for the default * subscription Id. This mapping is the same for every subscription. */ - override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> = - mobileConnectionsRepo.defaultMobileIconMapping.stateIn( - scope, - SharingStarted.WhileSubscribed(), - initialValue = mapOf(), - ) + override val defaultMobileIconMapping: State<Map<String, MobileIconGroup>> + get() = mobileConnectionsRepo.defaultMobileIconMapping - override val alwaysShowDataRatIcon: StateFlow<Boolean> = - mobileConnectionsRepo.defaultDataSubRatConfig - .mapLatest { it.alwaysShowDataRatIcon } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val alwaysShowDataRatIcon: State<Boolean> = + mobileConnectionsRepo.defaultDataSubRatConfig.map { it.alwaysShowDataRatIcon } - override val alwaysUseCdmaLevel: StateFlow<Boolean> = - mobileConnectionsRepo.defaultDataSubRatConfig - .mapLatest { it.alwaysShowCdmaRssi } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val alwaysUseCdmaLevel: State<Boolean> = + mobileConnectionsRepo.defaultDataSubRatConfig.map { it.alwaysShowCdmaRssi } - override val isSingleCarrier: StateFlow<Boolean> = + override val isSingleCarrier: State<Boolean> = mobileConnectionsRepo.subscriptions .map { it.size == 1 } - .logDiffsForTable( - tableLogger, - columnPrefix = LOGGING_PREFIX, - columnName = "isSingleCarrier", - initialValue = false, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + .also { + onActivated { + logDiffsForTable( + it, + tableLogger, + columnPrefix = LOGGING_PREFIX, + columnName = "isSingleCarrier", + ) + } + } /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */ - override val defaultMobileIconGroup: StateFlow<MobileIconGroup> = - mobileConnectionsRepo.defaultMobileIconGroup.stateIn( - scope, - SharingStarted.WhileSubscribed(), - initialValue = TelephonyIcons.G, - ) + override val defaultMobileIconGroup: State<MobileIconGroup> + get() = mobileConnectionsRepo.defaultMobileIconGroup /** * We want to show an error state when cellular has actually failed to validate, but not if some * other transport type is active, because then we expect there not to be validation. */ - override val isDefaultConnectionFailed: StateFlow<Boolean> = + override val isDefaultConnectionFailed: State<Boolean> = combine( mobileIsDefault, mobileConnectionsRepo.defaultConnectionIsValidated, @@ -407,46 +387,63 @@ constructor( else -> !defaultConnectionIsValidated } } - .logDiffsForTable( - tableLogger, - LOGGING_PREFIX, - columnName = "isDefaultConnectionFailed", - initialValue = false, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), false) - - override val isUserSetUp: StateFlow<Boolean> = userSetupRepo.isUserSetUp - - override val isForceHidden: Flow<Boolean> = - connectivityRepository.forceHiddenSlots - .map { it.contains(ConnectivitySlot.MOBILE) } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) - - override val isDeviceInEmergencyCallsOnlyMode: Flow<Boolean> = - mobileConnectionsRepo.isDeviceEmergencyCallCapable - - /** Vends out new [MobileIconInteractor] for a particular subId */ - override fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor = - reuseCache[subId]?.get() ?: createMobileConnectionInteractorForSubId(subId) - - private fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor = - MobileIconInteractorImpl( - scope, - activeDataConnectionHasDataEnabled, - alwaysShowDataRatIcon, - alwaysUseCdmaLevel, - isSingleCarrier, - mobileIsDefault, - defaultMobileIconMapping, - defaultMobileIconGroup, - isDefaultConnectionFailed, - isForceHidden, - mobileConnectionsRepo.getRepoForSubId(subId), - context, - ) - .also { reuseCache[subId] = WeakReference(it) } + .also { + onActivated { + logDiffsForTable( + it, + tableLogger, + LOGGING_PREFIX, + columnName = "isDefaultConnectionFailed", + ) + } + } + + override val isUserSetUp: State<Boolean> = buildState { userSetupRepo.isUserSetUp.toState() } + + override val isForceHidden: State<Boolean> = buildState { + connectivityRepository.forceHiddenSlots.toState().map { + it.contains(ConnectivitySlot.MOBILE) + } + } + + override val isDeviceInEmergencyCallsOnlyMode: State<Boolean> + get() = mobileConnectionsRepo.isDeviceEmergencyCallCapable + + /** Vends out a new [MobileIconInteractorKairos] for a particular subId */ + private fun BuildScope.mobileConnection( + repo: MobileConnectionRepositoryKairos + ): MobileIconInteractorKairos = activated { + MobileIconInteractorKairosImpl( + activeDataConnectionHasDataEnabled, + alwaysShowDataRatIcon, + alwaysUseCdmaLevel, + isSingleCarrier, + mobileIsDefault, + defaultMobileIconMapping, + defaultMobileIconGroup, + isDefaultConnectionFailed, + isForceHidden, + repo, + context, + ) + } companion object { private const val LOGGING_PREFIX = "Intr" } + + @dagger.Module + interface Module { + + @Binds fun bindImpl(impl: MobileIconsInteractorKairosImpl): MobileIconsInteractorKairos + + companion object { + @Provides + @ElementsIntoSet + fun kairosActivatable( + impl: Provider<MobileIconsInteractorKairosImpl> + ): Set<@JvmSuppressWildcards KairosActivatable> = + if (Flags.statusBarMobileIconKairos()) setOf(impl.get()) else emptySet() + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt new file mode 100644 index 000000000000..87877b3e9f43 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.mobile.domain.interactor + +import android.content.Context +import com.android.settingslib.SignalIcon +import com.android.settingslib.mobile.MobileMappings +import com.android.systemui.Flags +import com.android.systemui.KairosActivatable +import com.android.systemui.KairosBuilder +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.KairosNetwork +import com.android.systemui.kairos.buildSpec +import com.android.systemui.kairos.combine +import com.android.systemui.kairos.map +import com.android.systemui.kairos.mapValues +import com.android.systemui.kairos.toColdConflatedFlow +import com.android.systemui.kairosBuilder +import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryKairos +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy +import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository +import dagger.Provides +import dagger.multibindings.ElementsIntoSet +import javax.inject.Inject +import javax.inject.Provider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn + +@ExperimentalKairosApi +@SysUISingleton +class MobileIconsInteractorKairosAdapter +@Inject +constructor( + private val kairosInteractor: MobileIconsInteractorKairos, + private val repo: MobileConnectionsRepository, + repoK: MobileConnectionsRepositoryKairos, + kairosNetwork: KairosNetwork, + @Application scope: CoroutineScope, + context: Context, + mobileMappingsProxy: MobileMappingsProxy, + private val userSetupRepo: UserSetupRepository, +) : MobileIconsInteractor, KairosBuilder by kairosBuilder() { + + private val interactorsBySubIdK = buildIncremental { + kairosInteractor.icons + .mapValues { (subId, interactor) -> + buildSpec { MobileIconInteractorKairosAdapter(interactor) } + } + .applyLatestSpecForKey() + } + + private val interactorsBySubId = + interactorsBySubIdK + .toColdConflatedFlow(kairosNetwork) + .stateIn(scope, SharingStarted.Eagerly, emptyMap()) + + override val defaultDataSubId: Flow<Int?> + get() = repo.defaultDataSubId + + override val mobileIsDefault: StateFlow<Boolean> = + kairosInteractor.mobileIsDefault + .toColdConflatedFlow(kairosNetwork) + .stateIn(scope, SharingStarted.WhileSubscribed(), repo.mobileIsDefault.value) + + override val filteredSubscriptions: Flow<List<SubscriptionModel>> = + kairosInteractor.filteredSubscriptions.toColdConflatedFlow(kairosNetwork) + + override val icons: StateFlow<List<MobileIconInteractor>> = + interactorsBySubIdK + .map { it.values.toList() } + .toColdConflatedFlow(kairosNetwork) + .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) + + override val isStackable: StateFlow<Boolean> = + kairosInteractor.isStackable + .toColdConflatedFlow(kairosNetwork) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val activeMobileDataSubscriptionId: StateFlow<Int?> + get() = repo.activeMobileDataSubscriptionId + + override val activeDataConnectionHasDataEnabled: StateFlow<Boolean> = + kairosInteractor.activeDataConnectionHasDataEnabled + .toColdConflatedFlow(kairosNetwork) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val activeDataIconInteractor: StateFlow<MobileIconInteractor?> = + combine(repoK.activeMobileDataSubscriptionId, interactorsBySubIdK) { subId, interactors -> + interactors[subId] + } + .toColdConflatedFlow(kairosNetwork) + .stateIn(scope, SharingStarted.WhileSubscribed(), null) + + override val alwaysShowDataRatIcon: StateFlow<Boolean> = + kairosInteractor.alwaysShowDataRatIcon + .toColdConflatedFlow(kairosNetwork) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val alwaysUseCdmaLevel: StateFlow<Boolean> = + kairosInteractor.alwaysUseCdmaLevel + .toColdConflatedFlow(kairosNetwork) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val isSingleCarrier: StateFlow<Boolean> = + kairosInteractor.isSingleCarrier + .toColdConflatedFlow(kairosNetwork) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val defaultMobileIconMapping: StateFlow<Map<String, SignalIcon.MobileIconGroup>> = + kairosInteractor.defaultMobileIconMapping + .toColdConflatedFlow(kairosNetwork) + .stateIn(scope, SharingStarted.WhileSubscribed(), emptyMap()) + + override val defaultMobileIconGroup: StateFlow<SignalIcon.MobileIconGroup> = + kairosInteractor.defaultMobileIconGroup + .toColdConflatedFlow(kairosNetwork) + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + mobileMappingsProxy.getDefaultIcons(MobileMappings.Config.readConfig(context)), + ) + + override val isDefaultConnectionFailed: StateFlow<Boolean> = + kairosInteractor.isDefaultConnectionFailed + .toColdConflatedFlow(kairosNetwork) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val isUserSetUp: StateFlow<Boolean> + get() = userSetupRepo.isUserSetUp + + override val isForceHidden: Flow<Boolean> = + kairosInteractor.isForceHidden + .toColdConflatedFlow(kairosNetwork) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val isDeviceInEmergencyCallsOnlyMode: Flow<Boolean> + get() = repo.isDeviceEmergencyCallCapable + + override fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor = + interactorsBySubId.value[subId] ?: error("Unknown subscription id: $subId") + + @dagger.Module + object Module { + @Provides + @ElementsIntoSet + fun kairosActivatable( + impl: Provider<MobileIconsInteractorKairosAdapter> + ): Set<@JvmSuppressWildcards KairosActivatable> = + if (Flags.statusBarMobileIconKairos()) setOf(impl.get()) else emptySet() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModelKairos.kt new file mode 100644 index 000000000000..fce8c85338f3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModelKairos.kt @@ -0,0 +1,113 @@ +/* + * 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.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import android.graphics.Color +import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor +import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn + +/** + * A view model for an individual mobile icon that embeds the notion of a [StatusBarLocation]. This + * allows the mobile icon to change some view parameters at different locations + * + * @param commonImpl for convenience, this class wraps a base interface that can provides all of the + * common implementations between locations. See [MobileIconViewModel] + * @property location the [StatusBarLocation] of this VM. + * @property verboseLogger an optional logger to log extremely verbose view updates. + */ +abstract class LocationBasedMobileViewModelKairos( + val commonImpl: MobileIconViewModelCommonKairos, + val location: StatusBarLocation, + val verboseLogger: VerboseMobileViewLogger?, +) : MobileIconViewModelCommonKairos by commonImpl { + val defaultColor: Int = Color.WHITE + + companion object { + fun viewModelForLocation( + commonImpl: MobileIconViewModelCommon, + interactor: MobileIconInteractor, + verboseMobileViewLogger: VerboseMobileViewLogger, + location: StatusBarLocation, + scope: CoroutineScope, + ): LocationBasedMobileViewModel = + when (location) { + StatusBarLocation.HOME -> + HomeMobileIconViewModel(commonImpl, verboseMobileViewLogger) + StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModel(commonImpl) + StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl) + StatusBarLocation.SHADE_CARRIER_GROUP -> + ShadeCarrierGroupMobileIconViewModel(commonImpl, interactor, scope) + } + } +} + +class HomeMobileIconViewModelKairos( + commonImpl: MobileIconViewModelCommonKairos, + verboseMobileViewLogger: VerboseMobileViewLogger, +) : + MobileIconViewModelCommonKairos, + LocationBasedMobileViewModelKairos( + commonImpl, + location = StatusBarLocation.HOME, + verboseMobileViewLogger, + ) + +class QsMobileIconViewModelKairos(commonImpl: MobileIconViewModelCommonKairos) : + MobileIconViewModelCommonKairos, + LocationBasedMobileViewModelKairos( + commonImpl, + location = StatusBarLocation.QS, + // Only do verbose logging for the Home location. + verboseLogger = null, + ) + +class ShadeCarrierGroupMobileIconViewModelKairos( + commonImpl: MobileIconViewModelCommonKairos, + interactor: MobileIconInteractor, + scope: CoroutineScope, +) : + MobileIconViewModelCommonKairos, + LocationBasedMobileViewModelKairos( + commonImpl, + location = StatusBarLocation.SHADE_CARRIER_GROUP, + // Only do verbose logging for the Home location. + verboseLogger = null, + ) { + private val isSingleCarrier = interactor.isSingleCarrier + val carrierName = interactor.carrierName + + override val isVisible: StateFlow<Boolean> = + combine(super.isVisible, isSingleCarrier) { isVisible, isSingleCarrier -> + if (isSingleCarrier) false else isVisible + } + .stateIn(scope, SharingStarted.WhileSubscribed(), super.isVisible.value) +} + +class KeyguardMobileIconViewModelKairos(commonImpl: MobileIconViewModelCommonKairos) : + MobileIconViewModelCommonKairos, + LocationBasedMobileViewModelKairos( + commonImpl, + location = StatusBarLocation.KEYGUARD, + // Only do verbose logging for the Home location. + verboseLogger = null, + ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt new file mode 100644 index 000000000000..cc7fc0964dae --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt @@ -0,0 +1,328 @@ +/* + * 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.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import com.android.systemui.Flags.statusBarStaticInoutIndicators +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.log.table.logDiffsForTable +import com.android.systemui.res.R +import com.android.systemui.statusbar.core.NewStatusBarIcons +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor +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.mobile.ui.model.MobileContentDescription +import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn + +/** Common interface for all of the location-based mobile icon view models. */ +interface MobileIconViewModelCommonKairos : MobileIconViewModelCommon { + override val subscriptionId: Int + /** True if this view should be visible at all. */ + override val isVisible: StateFlow<Boolean> + override val icon: Flow<SignalIconModel> + override val contentDescription: Flow<MobileContentDescription?> + override val roaming: Flow<Boolean> + /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ + override val networkTypeIcon: Flow<Icon.Resource?> + /** The slice attribution. Drawn as a background layer */ + override val networkTypeBackground: StateFlow<Icon.Resource?> + override val activityInVisible: Flow<Boolean> + override val activityOutVisible: Flow<Boolean> + override val activityContainerVisible: Flow<Boolean> +} + +/** + * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over + * a single line of service via [MobileIconInteractor] and update the UI based on that + * subscription's information. + * + * There will be exactly one [MobileIconViewModel] per filtered subscription offered from + * [MobileIconsInteractor.filteredSubscriptions]. + * + * For the sake of keeping log spam in check, every flow funding the [MobileIconViewModelCommon] + * interface is implemented as a [StateFlow]. This ensures that each location-based mobile icon view + * model gets the exact same information, as well as allows us to log that unified state only once + * per icon. + */ +class MobileIconViewModelKairos( + override val subscriptionId: Int, + iconInteractor: MobileIconInteractor, + airplaneModeInteractor: AirplaneModeInteractor, + constants: ConnectivityConstants, + scope: CoroutineScope, +) : MobileIconViewModelCommonKairos { + private val cellProvider by lazy { + CellularIconViewModelKairos( + subscriptionId, + iconInteractor, + airplaneModeInteractor, + constants, + scope, + ) + } + + private val satelliteProvider by lazy { + CarrierBasedSatelliteViewModelKairosImpl( + subscriptionId, + airplaneModeInteractor, + iconInteractor, + scope, + ) + } + + /** + * Similar to repository switching, this allows us to split up the logic of satellite/cellular + * states, since they are different by nature + */ + private val vmProvider: Flow<MobileIconViewModelCommon> = + iconInteractor.isNonTerrestrial + .mapLatest { nonTerrestrial -> + if (nonTerrestrial) { + satelliteProvider + } else { + cellProvider + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), cellProvider) + + override val isVisible: StateFlow<Boolean> = + vmProvider + .flatMapLatest { it.isVisible } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val icon: Flow<SignalIconModel> = vmProvider.flatMapLatest { it.icon } + + override val contentDescription: Flow<MobileContentDescription?> = + vmProvider.flatMapLatest { it.contentDescription } + + override val roaming: Flow<Boolean> = vmProvider.flatMapLatest { it.roaming } + + override val networkTypeIcon: Flow<Icon.Resource?> = + vmProvider.flatMapLatest { it.networkTypeIcon } + + override val networkTypeBackground: StateFlow<Icon.Resource?> = + vmProvider + .flatMapLatest { it.networkTypeBackground } + .stateIn(scope, SharingStarted.WhileSubscribed(), null) + + override val activityInVisible: Flow<Boolean> = + vmProvider.flatMapLatest { it.activityInVisible } + + override val activityOutVisible: Flow<Boolean> = + vmProvider.flatMapLatest { it.activityOutVisible } + + override val activityContainerVisible: Flow<Boolean> = + vmProvider.flatMapLatest { it.activityContainerVisible } +} + +/** Representation of this network when it is non-terrestrial (e.g., satellite) */ +private class CarrierBasedSatelliteViewModelKairosImpl( + override val subscriptionId: Int, + airplaneModeInteractor: AirplaneModeInteractor, + interactor: MobileIconInteractor, + scope: CoroutineScope, +) : MobileIconViewModelCommon, MobileIconViewModelCommonKairos { + override val isVisible: StateFlow<Boolean> = + airplaneModeInteractor.isAirplaneMode + .map { !it } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val icon: Flow<SignalIconModel> = interactor.signalLevelIcon + + override val contentDescription: Flow<MobileContentDescription?> = MutableStateFlow(null) + + /** These fields are not used for satellite icons currently */ + override val roaming: Flow<Boolean> = flowOf(false) + override val networkTypeIcon: Flow<Icon.Resource?> = flowOf(null) + override val networkTypeBackground: StateFlow<Icon.Resource?> = MutableStateFlow(null) + override val activityInVisible: Flow<Boolean> = flowOf(false) + override val activityOutVisible: Flow<Boolean> = flowOf(false) + override val activityContainerVisible: Flow<Boolean> = flowOf(false) +} + +/** Terrestrial (cellular) icon. */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +private class CellularIconViewModelKairos( + override val subscriptionId: Int, + iconInteractor: MobileIconInteractor, + airplaneModeInteractor: AirplaneModeInteractor, + constants: ConnectivityConstants, + scope: CoroutineScope, +) : MobileIconViewModelCommon, MobileIconViewModelCommonKairos { + override val isVisible: StateFlow<Boolean> = + if (!constants.hasDataCapabilities) { + flowOf(false) + } else { + combine( + airplaneModeInteractor.isAirplaneMode, + iconInteractor.isAllowedDuringAirplaneMode, + iconInteractor.isForceHidden, + ) { isAirplaneMode, isAllowedDuringAirplaneMode, isForceHidden -> + if (isForceHidden) { + false + } else if (isAirplaneMode) { + isAllowedDuringAirplaneMode + } else { + true + } + } + } + .distinctUntilChanged() + .logDiffsForTable( + iconInteractor.tableLogBuffer, + columnName = "visible", + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val icon: Flow<SignalIconModel> = iconInteractor.signalLevelIcon + + override val contentDescription: Flow<MobileContentDescription?> = + combine(iconInteractor.signalLevelIcon, iconInteractor.networkName) { icon, nameModel -> + when (icon) { + is SignalIconModel.Cellular -> + MobileContentDescription.Cellular( + nameModel.name, + icon.levelDescriptionRes(), + ) + else -> null + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), null) + + private fun SignalIconModel.Cellular.levelDescriptionRes() = + when (level) { + 0 -> R.string.accessibility_no_signal + 1 -> R.string.accessibility_one_bar + 2 -> R.string.accessibility_two_bars + 3 -> R.string.accessibility_three_bars + 4 -> { + if (numberOfLevels == 6) { + R.string.accessibility_four_bars + } else { + R.string.accessibility_signal_full + } + } + 5 -> { + if (numberOfLevels == 6) { + R.string.accessibility_signal_full + } else { + R.string.accessibility_no_signal + } + } + else -> R.string.accessibility_no_signal + } + + private val showNetworkTypeIcon: Flow<Boolean> = + combine( + iconInteractor.isDataConnected, + iconInteractor.isDataEnabled, + iconInteractor.alwaysShowDataRatIcon, + iconInteractor.mobileIsDefault, + iconInteractor.carrierNetworkChangeActive, + ) { dataConnected, dataEnabled, alwaysShow, mobileIsDefault, carrierNetworkChange -> + alwaysShow || + (!carrierNetworkChange && (dataEnabled && dataConnected && mobileIsDefault)) + } + .distinctUntilChanged() + .logDiffsForTable( + iconInteractor.tableLogBuffer, + columnName = "showNetworkTypeIcon", + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val networkTypeIcon: Flow<Icon.Resource?> = + combine(iconInteractor.networkTypeIconGroup, showNetworkTypeIcon) { + networkTypeIconGroup, + shouldShow -> + val desc = + if (networkTypeIconGroup.contentDescription != 0) + ContentDescription.Resource(networkTypeIconGroup.contentDescription) + else null + val icon = + if (networkTypeIconGroup.iconId != 0) + Icon.Resource(networkTypeIconGroup.iconId, desc) + else null + return@combine when { + !shouldShow -> null + else -> icon + } + } + .distinctUntilChanged() + .stateIn(scope, SharingStarted.WhileSubscribed(), null) + + override val networkTypeBackground = + iconInteractor.showSliceAttribution + .map { + when { + it && NewStatusBarIcons.isEnabled -> + Icon.Resource(R.drawable.mobile_network_type_background_updated, null) + it -> Icon.Resource(R.drawable.mobile_network_type_background, null) + else -> null + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), null) + + override val roaming: StateFlow<Boolean> = + iconInteractor.isRoaming + .logDiffsForTable( + iconInteractor.tableLogBuffer, + columnName = "roaming", + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + private val activity: Flow<DataActivityModel?> = + if (!constants.shouldShowActivityConfig) { + flowOf(null) + } else { + iconInteractor.activity + } + + override val activityInVisible: Flow<Boolean> = + activity + .map { it?.hasActivityIn ?: false } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val activityOutVisible: Flow<Boolean> = + activity + .map { it?.hasActivityOut ?: false } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val activityContainerVisible: Flow<Boolean> = + if (statusBarStaticInoutIndicators()) { + flowOf(constants.shouldShowActivityConfig) + } else { + activity.map { it != null && (it.hasActivityIn || it.hasActivityOut) } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt new file mode 100644 index 000000000000..a65540738828 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt @@ -0,0 +1,152 @@ +/* + * 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.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import androidx.annotation.VisibleForTesting +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.coroutines.newTracingContext +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger +import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger +import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView +import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn + +/** + * View model for describing the system's current mobile cellular connections. The result is a list + * of [MobileIconViewModel]s which describe the individual icons and can be bound to + * [ModernStatusBarMobileView]. + */ +@SysUISingleton +class MobileIconsViewModelKairos +@Inject +constructor( + val logger: MobileViewLogger, + private val verboseLogger: VerboseMobileViewLogger, + private val interactor: MobileIconsInteractor, + private val airplaneModeInteractor: AirplaneModeInteractor, + private val constants: ConnectivityConstants, + @Background private val scope: CoroutineScope, +) { + @VisibleForTesting + val reuseCache = ConcurrentHashMap<Int, Pair<MobileIconViewModel, CoroutineScope>>() + + val activeMobileDataSubscriptionId: StateFlow<Int?> = interactor.activeMobileDataSubscriptionId + + val subscriptionIdsFlow: StateFlow<List<Int>> = + interactor.filteredSubscriptions + .mapLatest { subscriptions -> + subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) + + val mobileSubViewModels: StateFlow<List<MobileIconViewModelCommon>> = + subscriptionIdsFlow + .map { ids -> ids.map { commonViewModelForSub(it) } } + .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) + + private val firstMobileSubViewModel: StateFlow<MobileIconViewModelCommon?> = + mobileSubViewModels + .map { + if (it.isEmpty()) { + null + } else { + // Mobile icons get reversed by [StatusBarIconController], so the last element + // in this list will show up visually first. + it.last() + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), null) + + /** + * A flow that emits `true` if the mobile sub that's displayed first visually is showing its + * network type icon and `false` otherwise. + */ + val firstMobileSubShowingNetworkTypeIcon: StateFlow<Boolean> = + firstMobileSubViewModel + .flatMapLatest { firstMobileSubViewModel -> + firstMobileSubViewModel?.networkTypeIcon?.map { it != null } ?: flowOf(false) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + val isStackable: StateFlow<Boolean> = interactor.isStackable + + init { + scope.launch { subscriptionIdsFlow.collect { invalidateCaches(it) } } + } + + fun viewModelForSub(subId: Int, location: StatusBarLocation): LocationBasedMobileViewModel { + val common = commonViewModelForSub(subId) + return LocationBasedMobileViewModel.viewModelForLocation( + common, + interactor.getMobileConnectionInteractorForSubId(subId), + verboseLogger, + location, + scope, + ) + } + + private fun commonViewModelForSub(subId: Int): MobileIconViewModelCommon { + return reuseCache.getOrPut(subId) { createViewModel(subId) }.first + } + + private fun createViewModel(subId: Int): Pair<MobileIconViewModel, CoroutineScope> { + // Create a child scope so we can cancel it + val vmScope = scope.createChildScope(newTracingContext("MobileIconViewModel")) + val vm = + MobileIconViewModel( + subId, + interactor.getMobileConnectionInteractorForSubId(subId), + airplaneModeInteractor, + constants, + vmScope, + ) + + return Pair(vm, vmScope) + } + + private fun CoroutineScope.createChildScope(extraContext: CoroutineContext) = + CoroutineScope(coroutineContext + Job(coroutineContext[Job]) + extraContext) + + private fun invalidateCaches(subIds: List<Int>) { + reuseCache.keys + .filter { !subIds.contains(it) } + .forEach { id -> + reuseCache + .remove(id) + // Cancel the view model's scope after removing it + ?.second + ?.cancel() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt new file mode 100644 index 000000000000..2dbb02c8f095 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.mobile.ui.viewmodel + +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator +import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf + +@OptIn(ExperimentalCoroutinesApi::class) +class StackedMobileIconViewModelKairos +@AssistedInject +constructor(mobileIconsViewModel: MobileIconsViewModel) : ExclusiveActivatable() { + private val hydrator = Hydrator("StackedMobileIconViewModel") + + private val isStackable: Boolean by + hydrator.hydratedStateOf( + traceName = "isStackable", + source = mobileIconsViewModel.isStackable, + initialValue = false, + ) + + private val iconViewModelFlow: Flow<List<MobileIconViewModelCommon>> = + combine( + mobileIconsViewModel.mobileSubViewModels, + mobileIconsViewModel.activeMobileDataSubscriptionId, + ) { viewModels, activeSubId -> + // Sort to get the active subscription first, if it's set + viewModels.sortedByDescending { it.subscriptionId == activeSubId } + } + + val dualSim: DualSim? by + hydrator.hydratedStateOf( + traceName = "dualSim", + source = + iconViewModelFlow.flatMapLatest { viewModels -> + combine(viewModels.map { it.icon }) { icons -> + icons + .toList() + .filterIsInstance<SignalIconModel.Cellular>() + .takeIf { it.size == 2 } + ?.let { DualSim(it[0], it[1]) } + } + }, + initialValue = null, + ) + + val networkTypeIcon: Icon.Resource? by + hydrator.hydratedStateOf( + traceName = "networkTypeIcon", + source = + iconViewModelFlow.flatMapLatest { viewModels -> + viewModels.firstOrNull()?.networkTypeIcon ?: flowOf(null) + }, + initialValue = null, + ) + + val isIconVisible: Boolean by derivedStateOf { isStackable && dualSim != null } + + override suspend fun onActivated(): Nothing { + hydrator.activate() + } + + @AssistedFactory + interface Factory { + fun create(): StackedMobileIconViewModelKairos + } + + data class DualSim( + val primary: SignalIconModel.Cellular, + val secondary: SignalIconModel.Cellular, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt index cfd50973924d..6fada264e397 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt @@ -203,6 +203,8 @@ constructor( OngoingActivityChipBinder.createBinding( view.requireViewById(R.id.ongoing_activity_chip_secondary) ) + OngoingActivityChipBinder.updateTypefaces(primaryChipViewBinding) + OngoingActivityChipBinder.updateTypefaces(secondaryChipViewBinding) launch { combine( viewModel.ongoingActivityChipsLegacy, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index 2b0bf1a3d343..f1f2b88e9943 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -461,6 +461,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } } } + unregisterBackCallback(); if (logClose) { mUiEventLogger.logWithInstanceId( @@ -558,11 +559,6 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene @Override public void onVisibilityAggregated(boolean isVisible) { - if (isVisible) { - registerBackCallback(); - } else { - unregisterBackCallback(); - } super.onVisibilityAggregated(isVisible); mEditText.setEnabled(isVisible && !mSending); } @@ -623,6 +619,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene setAttachment(mEntry.remoteInputAttachment); updateSendButton(); + registerBackCallback(); } public void onNotificationUpdateOrReset() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt index d3af1e5b65fe..6d959be1c5f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.policy import android.app.ActivityOptions +import android.app.Flags.notificationsRedesignTemplates import android.app.Notification import android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY import android.app.PendingIntent @@ -53,7 +54,6 @@ import com.android.systemui.statusbar.SmartReplyController import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.headsup.HeadsUpManager import com.android.systemui.statusbar.notification.logging.NotificationLogger -import com.android.systemui.statusbar.notification.row.MagicActionBackgroundDrawable import com.android.systemui.statusbar.phone.KeyguardDismissUtil import com.android.systemui.statusbar.policy.InflatedSmartReplyState.SuppressedActions import com.android.systemui.statusbar.policy.SmartReplyView.SmartActions @@ -397,16 +397,21 @@ constructor( delayOnClickListener: Boolean, packageContext: Context, ): Button { - val isMagicAction = Flags.notificationMagicActionsTreatment() && + val isMagicAction = + Flags.notificationMagicActionsTreatment() && smartActions.fromAssistant && action.extras.getBoolean(Notification.Action.EXTRA_IS_MAGIC, false) - val layoutRes = if (isMagicAction) { - R.layout.magic_action_button - } else { - R.layout.smart_action_button - } - return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false) - as Button) + val layoutRes = + if (isMagicAction) { + R.layout.magic_action_button + } else { + if (notificationsRedesignTemplates()) { + R.layout.notification_2025_smart_action_button + } else { + R.layout.smart_action_button + } + } + return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false) as Button) .apply { text = action.title @@ -435,7 +440,6 @@ constructor( // Mark this as an Action button (layoutParams as SmartReplyView.LayoutParams).mButtonType = SmartButtonType.ACTION } - } private fun onSmartActionClick( @@ -499,9 +503,11 @@ constructor( replyIndex: Int, choice: CharSequence, delayOnClickListener: Boolean, - ): Button = - (LayoutInflater.from(parent.context).inflate(R.layout.smart_reply_button, parent, false) - as Button) + ): Button { + val layoutRes = + if (notificationsRedesignTemplates()) R.layout.notification_2025_smart_reply_button + else R.layout.smart_reply_button + return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false) as Button) .apply { text = choice val onClickListener = @@ -531,6 +537,7 @@ constructor( // Mark this as a Reply button (layoutParams as SmartReplyView.LayoutParams).mButtonType = SmartButtonType.REPLY } + } private fun onSmartReplyClick( entry: NotificationEntry, diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt new file mode 100644 index 000000000000..c11e4c507914 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.topwindoweffects; + +import android.content.Context +import android.graphics.PixelFormat +import android.view.Gravity +import android.view.WindowInsets +import android.view.WindowManager +import com.android.app.viewcapture.ViewCaptureAwareWindowManager +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor +import com.android.systemui.topwindoweffects.domain.interactor.SqueezeEffectInteractor +import com.android.systemui.topwindoweffects.ui.compose.EffectsWindowRoot +import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import javax.inject.Inject + +@SysUISingleton +class TopLevelWindowEffects @Inject constructor( + @Application private val context: Context, + @Application private val applicationScope: CoroutineScope, + private val windowManager: ViewCaptureAwareWindowManager, + private val squeezeEffectInteractor: SqueezeEffectInteractor, + private val keyEventInteractor: KeyEventInteractor, + private val viewModelFactory: SqueezeEffectViewModel.Factory +) : CoreStartable { + + override fun start() { + applicationScope.launch { + var root: EffectsWindowRoot? = null + squeezeEffectInteractor.isSqueezeEffectEnabled.collectLatest { enabled -> + // TODO: move window ops to a separate UI thread + if (enabled) { + keyEventInteractor.isPowerButtonDown.collectLatest { down -> + // TODO: ignore new window creation when ignoring short power press duration + if (down && root == null) { + root = EffectsWindowRoot( + context = context, + viewModelFactory = viewModelFactory, + onEffectFinished = { + if (root?.isAttachedToWindow == true) { + windowManager.removeView(root) + root = null + } + } + ) + root?.let { + windowManager.addView(it, getWindowManagerLayoutParams()) + } + } + } + } + } + } + } + + private fun getWindowManagerLayoutParams(): WindowManager.LayoutParams { + val lp = WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, + PixelFormat.TRANSPARENT + ) + + lp.privateFlags = lp.privateFlags or + (WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS + or WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION + or WindowManager.LayoutParams.PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED + or WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED + or WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) + + lp.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + + lp.title = "TopLevelWindowEffects" + lp.fitInsetsTypes = WindowInsets.Type.systemOverlays() + lp.gravity = Gravity.TOP + + return lp + } +} diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/SqueezeEffectRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/SqueezeEffectRepositoryModule.kt new file mode 100644 index 000000000000..5a2af9228771 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/SqueezeEffectRepositoryModule.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.topwindoweffects.dagger + +import com.android.systemui.topwindoweffects.data.repository.SqueezeEffectRepository +import com.android.systemui.topwindoweffects.data.repository.SqueezeEffectRepositoryImpl +import dagger.Binds +import dagger.Module + +@Module +interface SqueezeEffectRepositoryModule { + + @Binds + fun squeezeEffectRepository( + squeezeEffectRepositoryImpl: SqueezeEffectRepositoryImpl + ) : SqueezeEffectRepository +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/TopLevelWindowEffectsModule.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/TopLevelWindowEffectsModule.kt new file mode 100644 index 000000000000..6fbfedc94774 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/TopLevelWindowEffectsModule.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.topwindoweffects.dagger + +import com.android.systemui.CoreStartable +import com.android.systemui.topwindoweffects.TopLevelWindowEffects +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +interface TopLevelWindowEffectsModule { + + @Binds + @IntoMap + @ClassKey(TopLevelWindowEffects::class) + fun bindTopLevelWindowEffectsCoreStartable(impl: TopLevelWindowEffects): CoreStartable +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepository.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepository.kt new file mode 100644 index 000000000000..9e0b25633d5f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepository.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.topwindoweffects.data.repository + +import kotlinx.coroutines.flow.Flow + +interface SqueezeEffectRepository { + val isSqueezeEffectEnabled: Flow<Boolean> +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryImpl.kt new file mode 100644 index 000000000000..9e0feed35016 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryImpl.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.topwindoweffects.data.repository + +import android.database.ContentObserver +import android.os.Handler +import android.provider.Settings.Global.POWER_BUTTON_LONG_PRESS +import com.android.internal.R +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.shared.Flags +import com.android.systemui.util.settings.GlobalSettings +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext + +@SysUISingleton +class SqueezeEffectRepositoryImpl @Inject constructor( + @Background private val bgHandler: Handler?, + @Background private val bgCoroutineContext: CoroutineContext, + private val globalSettings: GlobalSettings +) : SqueezeEffectRepository { + + override val isSqueezeEffectEnabled: Flow<Boolean> = conflatedCallbackFlow { + val observer = object : ContentObserver(bgHandler) { + override fun onChange(selfChange: Boolean) { + trySendWithFailureLogging(squeezeEffectEnabled, TAG, + "updated isSqueezeEffectEnabled") + } + } + trySendWithFailureLogging(squeezeEffectEnabled, TAG, "init isSqueezeEffectEnabled") + globalSettings.registerContentObserverAsync(POWER_BUTTON_LONG_PRESS, observer) + awaitClose { globalSettings.unregisterContentObserverAsync(observer) } + }.flowOn(bgCoroutineContext) + + private val squeezeEffectEnabled + get() = Flags.enableLppSqueezeEffect() && globalSettings.getInt( + POWER_BUTTON_LONG_PRESS, R.integer.config_longPressOnPowerBehavior + ) == 5 // 5 corresponds to launch assistant in config_longPressOnPowerBehavior + + companion object { + private const val TAG = "SqueezeEffectRepository" + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractor.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractor.kt new file mode 100644 index 000000000000..879fde769aee --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractor.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.topwindoweffects.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.topwindoweffects.data.repository.SqueezeEffectRepository +import javax.inject.Inject + +@SysUISingleton +class SqueezeEffectInteractor @Inject constructor( + squeezeEffectRepository: SqueezeEffectRepository +) { + val isSqueezeEffectEnabled = squeezeEffectRepository.isSqueezeEffectEnabled +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt new file mode 100644 index 000000000000..61448f45c0e2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.topwindoweffects.ui.compose + +import android.annotation.SuppressLint +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.AbstractComposeView +import com.android.systemui.compose.ComposeInitializer +import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel + +@SuppressLint("ViewConstructor") +class EffectsWindowRoot( + context: Context, + private val onEffectFinished: () -> Unit, + private val viewModelFactory: SqueezeEffectViewModel.Factory +) : AbstractComposeView(context) { + + override fun onAttachedToWindow() { + ComposeInitializer.onAttachedToWindow(this) + super.onAttachedToWindow() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + ComposeInitializer.onDetachedFromWindow(this) + } + + @Composable + override fun Content() { + SqueezeEffect( + viewModelFactory = viewModelFactory, + onEffectFinished = onEffectFinished + ) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt new file mode 100644 index 000000000000..9809b0592dcf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.topwindoweffects.ui.compose + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Matrix +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.withTransform +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.VectorPainter +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.res.R +import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel + +private val SqueezeEffectMaxThickness = 12.dp +private val SqueezeColor = Color.Black + +@Composable +fun SqueezeEffect( + viewModelFactory: SqueezeEffectViewModel.Factory, + onEffectFinished: () -> Unit, + modifier: Modifier = Modifier +) { + val viewModel = rememberViewModel(traceName = "SqueezeEffect") { viewModelFactory.create() } + val down = viewModel.isPowerButtonPressed + val longPressed = viewModel.isPowerButtonLongPressed + // TODO: Choose the correct resource based on primary / secondary display + val top = rememberVectorPainter(ImageVector.vectorResource(R.drawable.rounded_corner_top)) + val bottom = rememberVectorPainter(ImageVector.vectorResource(R.drawable.rounded_corner_bottom)) + + val squeezeProgress by animateFloatAsState( + targetValue = + if (down && !longPressed) { + 1f + } else { + 0f + }, + animationSpec = tween(durationMillis = 400), + finishedListener = { onEffectFinished() } + ) + + Canvas(modifier = modifier.fillMaxSize()) { + if (squeezeProgress <= 0) { + return@Canvas + } + + val squeezeThickness = SqueezeEffectMaxThickness.toPx() * squeezeProgress + + drawRect(color = SqueezeColor, size = Size(size.width, squeezeThickness)) + + drawRect( + color = SqueezeColor, + topLeft = Offset(0f, size.height - squeezeThickness), + size = Size(size.width, squeezeThickness) + ) + + drawRect(color = SqueezeColor, size = Size(squeezeThickness, size.height)) + + drawRect( + color = SqueezeColor, + topLeft = Offset(size.width - squeezeThickness, 0f), + size = Size(squeezeThickness, size.height) + ) + + drawTransform( + dx = squeezeThickness, + dy = squeezeThickness, + rotation = 0f, + corner = top + ) + + drawTransform( + dx = size.width - squeezeThickness, + dy = squeezeThickness, + rotation = 90f, + corner = top + ) + + drawTransform( + dx = squeezeThickness, + dy = size.height - squeezeThickness, + rotation = 270f, + corner = bottom + ) + + drawTransform( + dx = size.width - squeezeThickness, + dy = size.height - squeezeThickness, + rotation = 180f, + corner = bottom + ) + } +} + +private fun DrawScope.drawTransform( + dx: Float, + dy: Float, + rotation: Float = 0f, + corner: VectorPainter, +) { + withTransform(transformBlock = { + transform(matrix = Matrix().apply { + translate(dx, dy) + if (rotation != 0f) { + rotateZ(rotation) + } + }) + }) { + with(corner) { + draw(size = intrinsicSize) + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/viewmodel/SqueezeEffectViewModel.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/viewmodel/SqueezeEffectViewModel.kt new file mode 100644 index 000000000000..1cab327740ce --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/viewmodel/SqueezeEffectViewModel.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.topwindoweffects.ui.viewmodel + +import androidx.compose.runtime.getValue +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +class SqueezeEffectViewModel +@AssistedInject +constructor( + keyEventInteractor: KeyEventInteractor +) : ExclusiveActivatable() { + private val hydrator = Hydrator("SqueezeEffectViewModel.hydrator") + + val isPowerButtonPressed: Boolean by hydrator.hydratedStateOf( + traceName = "isPowerButtonPressed", + initialValue = false, + source = keyEventInteractor.isPowerButtonDown + ) + + val isPowerButtonLongPressed: Boolean by hydrator.hydratedStateOf( + traceName = "isPowerButtonLongPressed", + initialValue = false, + source = keyEventInteractor.isPowerButtonLongPressed + ) + + override suspend fun onActivated(): Nothing { + hydrator.activate() + } + + @AssistedFactory + interface Factory { + fun create(): SqueezeEffectViewModel + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt index eecea9228ea3..2f8da2eae68a 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt @@ -84,7 +84,7 @@ fun TutorialSelectionScreen( ), ) { val isCompactWindow = hasCompactWindowSize() - val padding = if (isCompactWindow) 24.dp else 60.dp + val padding = if (isCompactWindow) 24.dp else 48.dp val configuration = LocalConfiguration.current when (configuration.orientation) { Configuration.ORIENTATION_LANDSCAPE -> { @@ -121,7 +121,7 @@ fun TutorialSelectionScreen( // because other composables have weight 1, Done button will be positioned first DoneButton( onDoneButtonClicked = onDoneButtonClicked, - modifier = Modifier.padding(horizontal = padding), + modifier = Modifier.padding(start = padding, top = 0.dp, end = padding, bottom = 32.dp), ) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt index 720d5507e224..f6582a005035 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt @@ -72,10 +72,10 @@ fun Slider( valueRange: ClosedFloatingPointRange<Float>, onValueChanged: (Float) -> Unit, onValueChangeFinished: ((Float) -> Unit)?, - stepDistance: Float, isEnabled: Boolean, accessibilityParams: AccessibilityParams, modifier: Modifier = Modifier, + stepDistance: Float = 0f, colors: SliderColors = SliderDefaults.colors(), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, haptics: Haptics = Haptics.Disabled, @@ -83,7 +83,7 @@ fun Slider( isReverseDirection: Boolean = false, track: (@Composable (SliderState) -> Unit)? = null, ) { - require(stepDistance > 0) { "stepDistance must be positive" } + require(stepDistance >= 0) { "stepDistance must not be negative" } val coroutineScope = rememberCoroutineScope() val snappedValue = snapValue(value, valueRange, stepDistance) val hapticsViewModel = haptics.createViewModel(snappedValue, valueRange, interactionSource) @@ -192,16 +192,20 @@ private fun AccessibilityParams.createSemantics( setProgress { targetValue -> val targetDirection = when { - targetValue > value -> 1 - targetValue < value -> -1 - else -> 0 + targetValue > value -> 1f + targetValue < value -> -1f + else -> 0f + } + val offset = + if (stepDistance > 0) { + // advance to the next step when stepDistance is > 0 + targetDirection * stepDistance + } else { + // advance to the desired value otherwise + targetValue - value } - val newValue = - (value + targetDirection * stepDistance).coerceIn( - valueRange.start, - valueRange.endInclusive, - ) + val newValue = (value + offset).coerceIn(valueRange.start, valueRange.endInclusive) onValueChanged(newValue) true } 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 e8054c07eac8..8105ae0960ad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -283,7 +283,6 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { () -> mSelectedUserInteractor, mock(UserTracker.class), mKosmos.getNotificationShadeWindowModel(), - mSecureSettings, mKosmos::getCommunalInteractor, mKosmos.getShadeLayoutParams()); mFeatureFlags = new FakeFeatureFlags(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt index b274f1c2c6df..d9c59d37b031 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt @@ -88,11 +88,6 @@ class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() { companion object { private val INTENT = Intent("some.intent.action") - private val DRAWABLE = - mock<Icon> { - whenever(this.contentDescription) - .thenReturn(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID)) - } private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337 @Parameters( @@ -337,7 +332,17 @@ class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() { homeControls.setState( lockScreenState = - KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = DRAWABLE) + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = + mock<Icon> { + whenever(contentDescription) + .thenReturn( + ContentDescription.Resource( + res = CONTENT_DESCRIPTION_RESOURCE_ID + ) + ) + } + ) ) homeControls.onTriggeredResult = if (startActivity) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt index 175e8d4331a1..e048d462ef8d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt @@ -33,8 +33,8 @@ import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.RequiresFlagsDisabled import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.platform.test.flag.junit.FlagsParameterization import android.testing.TestableLooper -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast import com.android.settingslib.bluetooth.LocalBluetoothManager @@ -77,6 +77,8 @@ import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit import org.mockito.kotlin.eq +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters private const val KEY = "TEST_KEY" private const val KEY_OLD = "TEST_KEY_OLD" @@ -89,12 +91,24 @@ private const val BROADCAST_APP_NAME = "BROADCAST_APP_NAME" private const val NORMAL_APP_NAME = "NORMAL_APP_NAME" @SmallTest -@RunWith(AndroidJUnit4::class) +@RunWith(ParameterizedAndroidJunit4::class) @TestableLooper.RunWithLooper -public class MediaDeviceManagerTest : SysuiTestCase() { +public class MediaDeviceManagerTest(flags: FlagsParameterization) : SysuiTestCase() { - private companion object { + companion object { val OTHER_DEVICE_ICON_STUB = TestStubDrawable() + + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.progressionOf( + com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_DEVICE_MANAGER_BACKGROUND_EXECUTION + ) + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) } @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() @@ -187,6 +201,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Test fun loadMediaData() { manager.onMediaDataLoaded(KEY, null, mediaData) + fakeBgExecutor.runAllReady() + fakeFgExecutor.runAllReady() verify(lmmFactory).create(PACKAGE) } @@ -195,6 +211,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { manager.onMediaDataLoaded(KEY, null, mediaData) manager.onMediaDataRemoved(KEY, false) fakeBgExecutor.runAllReady() + fakeFgExecutor.runAllReady() verify(lmm).unregisterCallback(any()) verify(muteAwaitManager).stopListening() } @@ -406,6 +423,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { manager.onMediaDataLoaded(KEY, null, mediaData) // WHEN the notification is removed manager.onMediaDataRemoved(KEY, true) + fakeBgExecutor.runAllReady() + fakeFgExecutor.runAllReady() // THEN the listener receives key removed event verify(listener).onKeyRemoved(eq(KEY), eq(true)) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java index 205ccea657df..9ad2235965bd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java @@ -406,11 +406,6 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { } @Override - int getHeaderIconSize() { - return 10; - } - - @Override CharSequence getHeaderText() { return mHeaderTitle; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java index f0902e35b837..f1bf7c0bcf13 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java @@ -50,6 +50,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.media.BluetoothMediaDevice; import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; +import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCaseExtKt; import com.android.systemui.animation.DialogTransitionAnimator; @@ -152,9 +153,9 @@ public class MediaOutputBroadcastDialogTest extends SysuiTestCase { volumePanelGlobalStateInteractor, mUserTracker); mMediaSwitchingController.mLocalMediaManager = mLocalMediaManager; - mMediaOutputBroadcastDialog = - new MediaOutputBroadcastDialog( - mContext, false, mBroadcastSender, mMediaSwitchingController); + mMediaOutputBroadcastDialog = new MediaOutputBroadcastDialog(mContext, false, + mBroadcastSender, mMediaSwitchingController, mContext.getMainExecutor(), + ThreadUtils.getBackgroundExecutor()); mMediaOutputBroadcastDialog.show(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java index d3ecb3d8c944..420fd6e33abc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java @@ -53,6 +53,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.flags.Flags; import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; +import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCaseExtKt; import com.android.systemui.animation.DialogTransitionAnimator; @@ -455,6 +456,8 @@ public class MediaOutputDialogTest extends SysuiTestCase { controller, mDialogTransitionAnimator, mUiEventLogger, + mContext.getMainExecutor(), + ThreadUtils.getBackgroundExecutor(), true); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java index 88c2697fe2f2..5c26dac5eb30 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java @@ -1573,6 +1573,25 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { assertThat(items.get(1).isFirstDeviceInGroup()).isFalse(); } + @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) + @Test + public void deviceListUpdateWithDifferentDevices_firstSelectedDeviceIsFirstDeviceInGroup() { + when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true); + doReturn(mMediaDevices) + .when(mLocalMediaManager) + .getSelectedMediaDevice(); + mMediaSwitchingController.start(mCb); + reset(mCb); + mMediaSwitchingController.getMediaItemList().clear(); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); + mMediaDevices.clear(); + mMediaDevices.add(mMediaDevice2); + mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); + + List<MediaItem> items = mMediaSwitchingController.getMediaItemList(); + assertThat(items.get(0).isFirstDeviceInGroup()).isTrue(); + } + private int getNumberOfConnectDeviceButtons() { int numberOfConnectDeviceButtons = 0; for (MediaItem item : mMediaSwitchingController.getMediaItemList()) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt index c20a801cd5e3..a8bfbd18d0c5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt @@ -103,6 +103,8 @@ class InternetDetailsContentManagerTest : SysuiTestCase() { whenever(internetWifiEntry.hasInternetAccess()).thenReturn(true) whenever(wifiEntries.size).thenReturn(1) whenever(internetDetailsContentController.getDialogTitleText()).thenReturn(TITLE) + whenever(internetDetailsContentController.getSubtitleText(ArgumentMatchers.anyBoolean())) + .thenReturn("") whenever(internetDetailsContentController.getMobileNetworkTitle(ArgumentMatchers.anyInt())) .thenReturn(MOBILE_NETWORK_TITLE) whenever( @@ -128,15 +130,13 @@ class InternetDetailsContentManagerTest : SysuiTestCase() { internetDetailsContentController, canConfigMobileData = true, canConfigWifi = true, - coroutineScope = scope, - context = mContext, uiEventLogger = mock<UiEventLogger>(), handler = handler, backgroundExecutor = bgExecutor, keyguard = keyguard, ) - internetDetailsContentManager.bind(contentView) + internetDetailsContentManager.bind(contentView, scope) internetDetailsContentManager.adapter = internetAdapter internetDetailsContentManager.connectedWifiEntry = internetWifiEntry internetDetailsContentManager.wifiEntriesCount = wifiEntries.size @@ -777,6 +777,26 @@ class InternetDetailsContentManagerTest : SysuiTestCase() { } } + @Test + fun updateTitleAndSubtitle() { + assertThat(internetDetailsContentManager.title).isEqualTo("Internet") + assertThat(internetDetailsContentManager.subTitle).isEqualTo("") + + whenever(internetDetailsContentController.getDialogTitleText()).thenReturn("New title") + whenever(internetDetailsContentController.getSubtitleText(ArgumentMatchers.anyBoolean())) + .thenReturn("New subtitle") + + internetDetailsContentManager.updateContent(true) + bgExecutor.runAllReady() + + internetDetailsContentManager.internetContentData.observe( + internetDetailsContentManager.lifecycleOwner!! + ) { + assertThat(internetDetailsContentManager.title).isEqualTo("New title") + assertThat(internetDetailsContentManager.subTitle).isEqualTo("New subtitle") + } + } + companion object { private const val TITLE = "Internet" private const val MOBILE_NETWORK_TITLE = "Mobile Title" diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt index 2d3f538689b3..155059ea5ed9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt @@ -36,6 +36,7 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.KeyguardUnlockAnimationController import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager +import com.android.systemui.log.assertLogsWtf import com.android.systemui.model.sysUiState import com.android.systemui.navigationbar.NavigationBarController import com.android.systemui.navigationbar.NavigationModeController @@ -221,7 +222,7 @@ class LauncherProxyServiceTest : SysuiTestCase() { `when`(processWrapper.isSystemUser).thenReturn(false) `when`(userManager.isVisibleBackgroundUsersSupported()).thenReturn(false) val spyContext = spy(context) - val ops = createLauncherProxyService(spyContext) + val ops = assertLogsWtf { createLauncherProxyService(spyContext) }.result ops.startConnectionToCurrentUser() verify(spyContext, times(0)).bindServiceAsUser(any(), any(), anyInt(), any()) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java new file mode 100644 index 000000000000..c231be181977 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.ringtone; + +import static org.junit.Assert.assertThrows; + +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Binder; +import android.os.UserHandle; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class RingtonePlayerTest extends SysuiTestCase { + + private AudioManager mAudioManager; + + private static final String TAG = "RingtonePlayerTest"; + + @Before + public void setup() throws Exception { + mAudioManager = getContext().getSystemService(AudioManager.class); + } + + @Test + public void testRingtonePlayerUriUserCheck() { + android.media.IRingtonePlayer irp = mAudioManager.getRingtonePlayer(); + final AudioAttributes aa = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE).build(); + // get a UserId that doesn't belong to mine + final int otherUserId = UserHandle.myUserId() == 0 ? 10 : 0; + // build a URI that I shouldn't have access to + final Uri uri = new Uri.Builder() + .scheme("content").authority(otherUserId + "@media") + .appendPath("external").appendPath("downloads") + .appendPath("bogusPathThatDoesNotMatter.mp3") + .build(); + if (android.media.audio.Flags.ringtoneUserUriCheck()) { + assertThrows(SecurityException.class, () -> + irp.play(new Binder(), uri, aa, 1.0f /*volume*/, false /*looping*/) + ); + + assertThrows(SecurityException.class, () -> + irp.getTitle(uri)); + } + } + +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt index a64ff321cd4d..11aaeefe8214 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt @@ -134,10 +134,7 @@ class BrightnessDialogTest(val flags: FlagsParameterization) : SysuiTestCase() { val frame = activityRule.activity.requireViewById<View>(viewId) val lp = frame.layoutParams as ViewGroup.MarginLayoutParams - val horizontalMargin = - activityRule.activity.resources.getDimensionPixelSize( - R.dimen.notification_side_paddings - ) + val horizontalMargin = 0 assertThat(lp.leftMargin).isEqualTo(horizontalMargin) assertThat(lp.rightMargin).isEqualTo(horizontalMargin) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java index 81213caaa5f4..3570cf827808 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java @@ -77,7 +77,6 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable; import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener; -import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.util.time.FakeSystemClock; @@ -1795,7 +1794,7 @@ public class ShadeListBuilderTest extends SysuiTestCase { invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS); // THEN an exception is NOT thrown directly, but a WTF IS logged. - LogAssertKt.assertLogsWtfs(() -> { + LogAssertKt.assertRunnableLogsWtfs(() -> { dispatchBuild(); runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); }); @@ -1818,7 +1817,7 @@ public class ShadeListBuilderTest extends SysuiTestCase { addNotif(0, PACKAGE_2); invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1); - LogAssertKt.assertLogsWtfs(() -> { + LogAssertKt.assertRunnableLogsWtfs(() -> { Assert.assertThrows(IllegalStateException.class, () -> { dispatchBuild(); runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); @@ -1844,13 +1843,13 @@ public class ShadeListBuilderTest extends SysuiTestCase { addNotif(0, PACKAGE_2); invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS); - LogAssertKt.assertLogsWtfs(() -> { + LogAssertKt.assertRunnableLogsWtfs(() -> { dispatchBuild(); runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); }); invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS); - LogAssertKt.assertLogsWtfs(() -> { + LogAssertKt.assertRunnableLogsWtfs(() -> { // Note: dispatchBuild itself triggers a non-reentrant pipeline run. dispatchBuild(); runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); @@ -1874,7 +1873,7 @@ public class ShadeListBuilderTest extends SysuiTestCase { addNotif(0, PACKAGE_1); invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS); - LogAssertKt.assertLogsWtfs(() -> { + LogAssertKt.assertRunnableLogsWtfs(() -> { dispatchBuild(); runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); }); @@ -1897,7 +1896,7 @@ public class ShadeListBuilderTest extends SysuiTestCase { addNotif(0, PACKAGE_1); invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1); - LogAssertKt.assertLogsWtfs(() -> { + LogAssertKt.assertRunnableLogsWtfs(() -> { Assert.assertThrows(IllegalStateException.class, () -> { dispatchBuild(); runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); @@ -1922,7 +1921,7 @@ public class ShadeListBuilderTest extends SysuiTestCase { addNotif(0, PACKAGE_2); invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS); - LogAssertKt.assertLogsWtfs(() -> { + LogAssertKt.assertRunnableLogsWtfs(() -> { dispatchBuild(); runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); }); @@ -1945,7 +1944,7 @@ public class ShadeListBuilderTest extends SysuiTestCase { addNotif(0, PACKAGE_2); invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1); - LogAssertKt.assertLogsWtfs(() -> { + LogAssertKt.assertRunnableLogsWtfs(() -> { Assert.assertThrows(IllegalStateException.class, () -> { dispatchBuild(); runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); @@ -1970,7 +1969,7 @@ public class ShadeListBuilderTest extends SysuiTestCase { addNotif(0, PACKAGE_2); invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS); - LogAssertKt.assertLogsWtfs(() -> { + LogAssertKt.assertRunnableLogsWtfs(() -> { dispatchBuild(); runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); }); @@ -1993,7 +1992,7 @@ public class ShadeListBuilderTest extends SysuiTestCase { addNotif(0, PACKAGE_2); invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1); - LogAssertKt.assertLogsWtfs(() -> { + LogAssertKt.assertRunnableLogsWtfs(() -> { Assert.assertThrows(IllegalStateException.class, () -> { dispatchBuild(); runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index cd2ea7d25699..4315c0f638ac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -73,6 +73,7 @@ import com.android.systemui.statusbar.notification.FeedbackIcon; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.headsup.PinnedStatus; +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; @@ -936,11 +937,13 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) public void isExpanded_sensitivePromotedNotification_notExpanded() throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - row.setPromotedOngoing(true); + NotificationEntry entry = mock(NotificationEntry.class); + when(entry.isPromotedOngoing()).thenReturn(true); + row.setEntry(entry); row.setSensitive(/* sensitive= */true, /* hideSensitive= */false); row.setHideSensitiveForIntrinsicHeight(/* hideSensitive= */true); @@ -949,11 +952,13 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) public void isExpanded_promotedNotificationNotOnKeyguard_expanded() throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - row.setPromotedOngoing(true); + NotificationEntry entry = mock(NotificationEntry.class); + when(entry.isPromotedOngoing()).thenReturn(true); + row.setEntry(entry); row.setOnKeyguard(false); // THEN @@ -961,11 +966,13 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) public void isExpanded_promotedNotificationAllowOnKeyguard_expanded() throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - row.setPromotedOngoing(true); + NotificationEntry entry = mock(NotificationEntry.class); + when(entry.isPromotedOngoing()).thenReturn(true); + row.setEntry(entry); row.setOnKeyguard(true); // THEN @@ -973,12 +980,14 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) public void isExpanded_promotedNotificationIgnoreLockscreenConstraints_expanded() throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - row.setPromotedOngoing(true); + NotificationEntry entry = mock(NotificationEntry.class); + when(entry.isPromotedOngoing()).thenReturn(true); + row.setEntry(entry); row.setOnKeyguard(true); row.setIgnoreLockscreenConstraints(true); @@ -987,12 +996,14 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) public void isExpanded_promotedNotificationSaveSpaceOnLockScreen_notExpanded() throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - row.setPromotedOngoing(true); + NotificationEntry entry = mock(NotificationEntry.class); + when(entry.isPromotedOngoing()).thenReturn(true); + row.setEntry(entry); row.setOnKeyguard(true); row.setSaveSpaceOnLockscreen(true); @@ -1001,12 +1012,14 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME) + @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME}) public void isExpanded_promotedNotificationNotSaveSpaceOnLockScreen_expanded() throws Exception { // GIVEN final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); - row.setPromotedOngoing(true); + NotificationEntry entry = mock(NotificationEntry.class); + when(entry.isPromotedOngoing()).thenReturn(true); + row.setEntry(entry); row.setOnKeyguard(true); row.setSaveSpaceOnLockscreen(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index 89b7bee7f286..a3616d20e11f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -128,6 +128,7 @@ import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.kosmos.KosmosJavaAdapter; +import com.android.systemui.media.NotificationMediaManager; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.notetask.NoteTaskController; import com.android.systemui.plugins.ActivityStarter; @@ -164,7 +165,6 @@ import com.android.systemui.statusbar.KeyguardIndicationController; import com.android.systemui.statusbar.LightRevealScrim; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.NotificationLockscreenUserManager; -import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShadeDepthController; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java index ffe7750dadfa..4a0445d5543a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java @@ -269,14 +269,14 @@ public class RemoteInputViewTest extends SysuiTestCase { when(viewRoot.getOnBackInvokedDispatcher()).thenReturn(backInvokedDispatcher); view.setViewRootImpl(viewRoot); - /* verify that predictive back callback registered when RemoteInputView becomes visible */ - view.onVisibilityAggregated(true); + /* verify that predictive back callback registered when RemoteInputView gains focus */ + view.focus(); verify(backInvokedDispatcher).registerOnBackInvokedCallback( eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY), onBackInvokedCallbackCaptor.capture()); - /* verify that same callback unregistered when RemoteInputView becomes invisible */ - view.onVisibilityAggregated(false); + /* verify that same callback unregistered when RemoteInputView loses focus */ + view.onDefocus(false, false, null); verify(backInvokedDispatcher).unregisterOnBackInvokedCallback( eq(onBackInvokedCallbackCaptor.getValue())); } @@ -299,13 +299,12 @@ public class RemoteInputViewTest extends SysuiTestCase { view.onVisibilityAggregated(true); view.setEditTextReferenceToSelf(); + view.focus(); /* capture the callback during registration */ verify(backInvokedDispatcher).registerOnBackInvokedCallback( eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY), onBackInvokedCallbackCaptor.capture()); - view.focus(); - /* invoke the captured callback */ onBackInvokedCallbackCaptor.getValue().onBackInvoked(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 341bd3a38999..8de931a7af40 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -33,6 +33,8 @@ import static com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR; import static com.google.common.truth.Truth.assertThat; +import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -55,8 +57,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; - import android.app.ActivityManager; import android.app.IActivityManager; import android.app.INotificationManager; @@ -136,7 +136,6 @@ import com.android.systemui.statusbar.RankingBuilder; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.collection.GroupEntry; -import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; @@ -166,7 +165,6 @@ import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvision import com.android.systemui.user.domain.interactor.SelectedUserInteractor; import com.android.systemui.util.FakeEventLog; import com.android.systemui.util.settings.FakeGlobalSettings; -import com.android.systemui.util.settings.FakeSettings; import com.android.systemui.util.settings.SystemSettings; import com.android.systemui.util.time.SystemClock; import com.android.wm.shell.Flags; @@ -206,8 +204,6 @@ import com.android.wm.shell.taskview.TaskViewRepository; import com.android.wm.shell.taskview.TaskViewTransitions; import com.android.wm.shell.transition.Transitions; -import kotlin.Lazy; - import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -218,9 +214,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; -import platform.test.runner.parameterized.ParameterizedAndroidJunit4; -import platform.test.runner.parameterized.Parameters; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -229,6 +222,10 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.Executor; +import kotlin.Lazy; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + @SmallTest @RunWith(ParameterizedAndroidJunit4.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -451,7 +448,6 @@ public class BubblesTest extends SysuiTestCase { () -> mSelectedUserInteractor, mUserTracker, mNotificationShadeWindowModel, - new FakeSettings(), mKosmos::getCommunalInteractor, mKosmos.getShadeLayoutParams() ); @@ -605,14 +601,19 @@ public class BubblesTest extends SysuiTestCase { // Get a reference to KeyguardStateController.Callback verify(mKeyguardStateController, atLeastOnce()) .addCallback(mKeyguardStateControllerCallbackCaptor.capture()); + + // Make sure mocks are set up for current user + switchUser(ActivityManager.getCurrentUser()); } @After public void tearDown() throws Exception { - ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles()); - for (int i = 0; i < bubbles.size(); i++) { - mBubbleController.removeBubble(bubbles.get(i).getKey(), - Bubbles.DISMISS_NO_LONGER_BUBBLE); + if (mBubbleData != null) { + ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles()); + for (int i = 0; i < bubbles.size(); i++) { + mBubbleController.removeBubble(bubbles.get(i).getKey(), + Bubbles.DISMISS_NO_LONGER_BUBBLE); + } } mTestableLooper.processAllMessages(); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt index 0ba7c8574d85..a3d01355ac4c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt @@ -47,6 +47,7 @@ import com.android.systemui.log.dagger.BiometricLog import com.android.systemui.log.dagger.BroadcastDispatcherLog import com.android.systemui.log.dagger.FaceAuthLog import com.android.systemui.log.dagger.SceneFrameworkLog +import com.android.systemui.media.NotificationMediaManager import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.model.SysUiState import com.android.systemui.plugins.ActivityStarter @@ -57,7 +58,6 @@ import com.android.systemui.shared.system.ActivityManagerWrapper import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.NotificationListener import com.android.systemui.statusbar.NotificationLockscreenUserManager -import com.android.systemui.statusbar.NotificationMediaManager import com.android.systemui.statusbar.NotificationShadeDepthController import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt index 6f570a86b19e..cd4b09c5267a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt @@ -21,7 +21,6 @@ import com.android.systemui.biometrics.data.repository.fingerprintPropertyReposi import com.android.systemui.dump.dumpManager import com.android.systemui.keyevent.domain.interactor.keyEventInteractor import com.android.systemui.keyguard.data.repository.biometricSettingsRepository -import com.android.systemui.keyguard.domain.interactor.keyguardBypassInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.util.time.systemClock @@ -31,8 +30,6 @@ val Kosmos.deviceEntryHapticsInteractor by DeviceEntryHapticsInteractor( biometricSettingsRepository = biometricSettingsRepository, deviceEntryBiometricAuthInteractor = deviceEntryBiometricAuthInteractor, - deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor, - keyguardBypassInteractor = keyguardBypassInteractor, deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, deviceEntrySourceInteractor = deviceEntrySourceInteractor, fingerprintPropertyRepository = fingerprintPropertyRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt index 97dab4987f6c..c9e25c31faa0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt @@ -27,9 +27,16 @@ class FakeKeyEventRepository @Inject constructor() : KeyEventRepository { private val _isPowerButtonDown = MutableStateFlow(false) override val isPowerButtonDown: Flow<Boolean> = _isPowerButtonDown.asStateFlow() + private val _isPowerButtonLongPressed = MutableStateFlow(false) + override val isPowerButtonLongPressed = _isPowerButtonLongPressed.asStateFlow() + fun setPowerButtonDown(isDown: Boolean) { _isPowerButtonDown.value = isDown } + + fun setPowerButtonLongPressed(isLongPressed: Boolean) { + _isPowerButtonLongPressed.value = isLongPressed + } } @Module diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt index 255a780a84be..113059222aa2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.domain.interactor import android.content.applicationContext +import android.os.powerManager import android.view.accessibility.accessibilityManagerWrapper import com.android.internal.logging.uiEventLogger import com.android.systemui.broadcast.broadcastDispatcher @@ -26,6 +27,8 @@ import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.shade.pulsingGestureListener +import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository +import com.android.systemui.util.time.fakeSystemClock val Kosmos.keyguardTouchHandlingInteractor by Kosmos.Fixture { @@ -40,5 +43,8 @@ val Kosmos.keyguardTouchHandlingInteractor by accessibilityManager = accessibilityManagerWrapper, pulsingGestureListener = pulsingGestureListener, faceAuthInteractor = deviceEntryFaceAuthInteractor, + secureSettingsRepository = userAwareSecureSettingsRepository, + powerManager = powerManager, + systemClock = fakeSystemClock, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt index 2797b4409ff0..bf456978d983 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt @@ -48,5 +48,6 @@ val Kosmos.deviceEntryBackgroundViewModel by Fixture { primaryBouncerToLockscreenTransitionViewModel, lockscreenToDozingTransitionViewModel = lockscreenToDozingTransitionViewModel, glanceableHubToAodTransitionViewModel = glanceableHubToAodTransitionViewModel, + glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel, ) } 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 27ca0f867855..a9aa8cd5a7f9 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 @@ -84,6 +84,7 @@ val Kosmos.keyguardRootViewModel by Fixture { occludedToAodTransitionViewModel = occludedToAodTransitionViewModel, occludedToDozingTransitionViewModel = occludedToDozingTransitionViewModel, occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel, + occludedToPrimaryBouncerTransitionViewModel = occludedToPrimaryBouncerTransitionViewModel, offToLockscreenTransitionViewModel = offToLockscreenTransitionViewModel, primaryBouncerToAodTransitionViewModel = primaryBouncerToAodTransitionViewModel, primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/util/KeyguardTransitionRepositorySpySubject.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/util/KeyguardTransitionRepositorySpySubject.kt index 11f0c19ffa67..7093a941485a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/util/KeyguardTransitionRepositorySpySubject.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/util/KeyguardTransitionRepositorySpySubject.kt @@ -115,14 +115,14 @@ private constructor( fun assertThat( repository: KeyguardTransitionRepository ): KeyguardTransitionRepositorySpySubject = - assertAbout { failureMetadata, repository: KeyguardTransitionRepository -> + assertAbout { failureMetadata, repository: KeyguardTransitionRepository? -> if (!Mockito.mockingDetails(repository).isSpy) { fail( "Cannot assert on a non-spy KeyguardTransitionRepository. " + "Use Mockito.spy(keyguardTransitionRepository)." ) } - KeyguardTransitionRepositorySpySubject(failureMetadata, repository) + KeyguardTransitionRepositorySpySubject(failureMetadata, repository!!) } .that(repository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt index 5e67182d7353..b41ceff5f581 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt @@ -13,89 +13,103 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.android.systemui.log import android.util.Log import android.util.Log.TerribleFailureHandler -import junit.framework.Assert +import com.google.common.truth.Truth.assertWithMessage +import java.util.concurrent.Callable -/** Asserts that the given block does not make a call to Log.wtf */ -fun assertDoesNotLogWtf( +/** Asserts that [notLoggingBlock] does not make a call to [Log.wtf] */ +fun <T> assertDoesNotLogWtf( message: String = "Expected Log.wtf not to be called", - notLoggingBlock: () -> Unit, -) { + notLoggingBlock: () -> T, +): T { var caught: TerribleFailureLog? = null val newHandler = TerribleFailureHandler { tag, failure, system -> caught = TerribleFailureLog(tag, failure, system) } val oldHandler = Log.setWtfHandler(newHandler) - try { - notLoggingBlock() - } finally { - Log.setWtfHandler(oldHandler) - } + val result = + try { + notLoggingBlock() + } finally { + Log.setWtfHandler(oldHandler) + } caught?.let { throw AssertionError("$message: $it", it.failure) } + return result } -fun assertDoesNotLogWtf( - message: String = "Expected Log.wtf not to be called", - notLoggingRunnable: Runnable, -) = assertDoesNotLogWtf(message = message) { notLoggingRunnable.run() } - -/** - * Assert that the given block makes a call to Log.wtf - * - * @return the details of the log - */ -fun assertLogsWtf( +/** Assert that [loggingBlock] makes a call to [Log.wtf] */ +@JvmOverloads +fun <T> assertLogsWtf( message: String = "Expected Log.wtf to be called", allowMultiple: Boolean = false, - loggingBlock: () -> Unit, -): TerribleFailureLog { - var caught: TerribleFailureLog? = null - var count = 0 + loggingBlock: () -> T, +): WtfBlockResult<T> { + val caught = mutableListOf<TerribleFailureLog>() val newHandler = TerribleFailureHandler { tag, failure, system -> - if (caught == null) { - caught = TerribleFailureLog(tag, failure, system) - } - count++ + caught.add(TerribleFailureLog(tag, failure, system)) } val oldHandler = Log.setWtfHandler(newHandler) - try { - loggingBlock() - } finally { - Log.setWtfHandler(oldHandler) - } - Assert.assertNotNull(message, caught) - if (!allowMultiple && count != 1) { - Assert.fail("Unexpectedly caught Log.Wtf $count times; expected only 1. First: $caught") + val result = + try { + loggingBlock() + } finally { + Log.setWtfHandler(oldHandler) + } + assertWithMessage(message).that(caught).isNotEmpty() + if (!allowMultiple) { + assertWithMessage("Unexpectedly caught Log.Wtf multiple times").that(caught).hasSize(1) } - return caught!! + return WtfBlockResult(caught, result) } +/** Assert that [loggingCallable] makes a call to [Log.wtf] */ @JvmOverloads -fun assertLogsWtf( +fun <T> assertLogsWtf( message: String = "Expected Log.wtf to be called", allowMultiple: Boolean = false, - loggingRunnable: Runnable, -): TerribleFailureLog = - assertLogsWtf(message = message, allowMultiple = allowMultiple) { loggingRunnable.run() } + loggingCallable: Callable<T>, +): WtfBlockResult<T> = + assertLogsWtf(message = message, allowMultiple = allowMultiple, loggingCallable::call) -fun assertLogsWtfs( +/** Assert that [loggingBlock] makes at least one call to [Log.wtf] */ +@JvmOverloads +fun <T> assertLogsWtfs( message: String = "Expected Log.wtf to be called once or more", - loggingBlock: () -> Unit, -): TerribleFailureLog = assertLogsWtf(message, allowMultiple = true, loggingBlock) + loggingBlock: () -> T, +): WtfBlockResult<T> = assertLogsWtf(message, allowMultiple = true, loggingBlock) +/** Assert that [loggingCallable] makes at least one call to [Log.wtf] */ @JvmOverloads -fun assertLogsWtfs( +fun <T> assertLogsWtfs( message: String = "Expected Log.wtf to be called once or more", - loggingRunnable: Runnable, -): TerribleFailureLog = assertLogsWtfs(message) { loggingRunnable.run() } + loggingCallable: Callable<T>, +): WtfBlockResult<T> = assertLogsWtf(message, allowMultiple = true, loggingCallable) /** The data passed to [TerribleFailureHandler.onTerribleFailure] */ data class TerribleFailureLog( val tag: String, val failure: Log.TerribleFailure, - val system: Boolean + val system: Boolean, ) + +/** The [Log.wtf] logs and return value of the block */ +data class WtfBlockResult<T>(val logs: List<TerribleFailureLog>, val result: T) + +/** Assert that [loggingRunnable] makes a call to [Log.wtf] */ +@JvmOverloads +fun assertRunnableLogsWtf( + message: String = "Expected Log.wtf to be called", + allowMultiple: Boolean = false, + loggingRunnable: Runnable, +): WtfBlockResult<Unit> = + assertLogsWtf(message = message, allowMultiple = allowMultiple) { loggingRunnable.run() } + +/** Assert that [loggingRunnable] makes at least one call to [Log.wtf] */ +@JvmOverloads +fun assertRunnableLogsWtfs( + message: String = "Expected Log.wtf to be called once or more", + loggingRunnable: Runnable, +): WtfBlockResult<Unit> = assertRunnableLogsWtf(message, allowMultiple = true, loggingRunnable) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt index e639326bd7a1..0e348c88f058 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogWtfHandlerRule.kt @@ -24,21 +24,23 @@ import org.junit.runners.model.Statement class LogWtfHandlerRule : TestRule { - private var started = false - private var handler = ThrowAndFailAtEnd + private var failureLogExemptions = mutableListOf<FailureLogExemption>() override fun apply(base: Statement, description: Description): Statement { return object : Statement() { override fun evaluate() { - started = true + val handler = TerribleFailureTestHandler() val originalWtfHandler = Log.setWtfHandler(handler) var failure: Throwable? = null try { base.evaluate() } catch (ex: Throwable) { - failure = ex.runAndAddSuppressed { handler.onTestFailure(ex) } + failure = ex } finally { - failure = failure.runAndAddSuppressed { handler.onTestFinished() } + failure = + runAndAddSuppressed(failure) { + handler.onTestFinished(failureLogExemptions) + } Log.setWtfHandler(originalWtfHandler) } if (failure != null) { @@ -48,74 +50,52 @@ class LogWtfHandlerRule : TestRule { } } - fun Throwable?.runAndAddSuppressed(block: () -> Unit): Throwable? { + /** Adds a log failure exemption. Exemptions are evaluated at the end of the test. */ + fun addFailureLogExemption(exemption: FailureLogExemption) { + failureLogExemptions.add(exemption) + } + + /** Clears and sets exemptions. Exemptions are evaluated at the end of the test. */ + fun resetFailureLogExemptions(vararg exemptions: FailureLogExemption) { + failureLogExemptions = exemptions.toMutableList() + } + + private fun runAndAddSuppressed(currentError: Throwable?, block: () -> Unit): Throwable? { try { block() } catch (t: Throwable) { - if (this == null) { + if (currentError == null) { return t } - addSuppressed(t) + currentError.addSuppressed(t) } - return this + return currentError } - fun setWtfHandler(handler: TerribleFailureTestHandler) { - check(!started) { "Should only be called before the test starts" } - this.handler = handler - } - - fun interface TerribleFailureTestHandler : TerribleFailureHandler { - fun onTestFailure(failure: Throwable) {} - fun onTestFinished() {} - } - - companion object Handlers { - val ThrowAndFailAtEnd - get() = - object : TerribleFailureTestHandler { - val failures = mutableListOf<Log.TerribleFailure>() - - override fun onTerribleFailure( - tag: String, - what: Log.TerribleFailure, - system: Boolean - ) { - failures.add(what) - throw what - } + private class TerribleFailureTestHandler : TerribleFailureHandler { + private val failureLogs = mutableListOf<FailureLog>() - override fun onTestFailure(failure: Throwable) { - super.onTestFailure(failure) - } + override fun onTerribleFailure(tag: String, what: Log.TerribleFailure, system: Boolean) { + failureLogs.add(FailureLog(tag = tag, failure = what, system = system)) + } - override fun onTestFinished() { - if (failures.isNotEmpty()) { - throw AssertionError("Unexpected Log.wtf calls: $failures", failures[0]) - } - } + fun onTestFinished(exemptions: List<FailureLogExemption>) { + val failures = + failureLogs.filter { failureLog -> + !exemptions.any { it.isFailureLogExempt(failureLog) } } + if (failures.isNotEmpty()) { + throw AssertionError("Unexpected Log.wtf calls: $failures", failures[0].failure) + } + } + } - val JustThrow = TerribleFailureTestHandler { _, what, _ -> throw what } - - val JustFailAtEnd - get() = - object : TerribleFailureTestHandler { - val failures = mutableListOf<Log.TerribleFailure>() - - override fun onTerribleFailure( - tag: String, - what: Log.TerribleFailure, - system: Boolean - ) { - failures.add(what) - } + /** All the information from a call to [Log.wtf] that was handed to [TerribleFailureHandler] */ + data class FailureLog(val tag: String, val failure: Log.TerribleFailure, val system: Boolean) - override fun onTestFinished() { - if (failures.isNotEmpty()) { - throw AssertionError("Unexpected Log.wtf calls: $failures", failures[0]) - } - } - } + /** An interface for exempting a [FailureLog] from causing a test failure. */ + fun interface FailureLogExemption { + /** Determines whether a log should be except from failing the test. */ + fun isFailureLogExempt(log: FailureLog): Boolean } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt index 4f8d5a14e390..9457de18b3b9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt @@ -18,18 +18,14 @@ package com.android.systemui.qs import com.android.systemui.plugins.qs.TileDetailsViewModel -class FakeTileDetailsViewModel(var tileSpec: String?) : TileDetailsViewModel() { +class FakeTileDetailsViewModel(var tileSpec: String?) : TileDetailsViewModel { private var _clickOnSettingsButton = 0 override fun clickOnSettingsButton() { _clickOnSettingsButton++ } - override fun getTitle(): String { - return tileSpec ?: " Fake title" - } + override val title = tileSpec ?: " Fake title" - override fun getSubTitle(): String { - return tileSpec ?: "Fake sub title" - } + override val subTitle = tileSpec ?: "Fake sub title" } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt index 75ca311689ce..4aa4a2b1a73a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.qs.panels.ui.viewmodel.toolbar import android.content.applicationContext import com.android.systemui.classifier.domain.interactor.falsingInteractor +import com.android.systemui.development.ui.viewmodel.buildNumberViewModelFactory import com.android.systemui.globalactions.globalActionsDialogLite import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.footerActionsInteractor @@ -29,6 +30,7 @@ val Kosmos.toolbarViewModelFactory by override fun create(): ToolbarViewModel { return ToolbarViewModel( editModeButtonViewModelFactory, + buildNumberViewModelFactory, footerActionsInteractor, { globalActionsDialogLite }, falsingInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerSubject.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerSubject.kt index e09f2806ce5c..c1e689c0165c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerSubject.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandlerSubject.kt @@ -68,8 +68,8 @@ private constructor( ): QSTileIntentUserInputHandlerSubject = Truth.assertAbout { failureMetadata: FailureMetadata, - subject: FakeQSTileIntentUserInputHandler -> - QSTileIntentUserInputHandlerSubject(failureMetadata, subject) + subject: FakeQSTileIntentUserInputHandler? -> + QSTileIntentUserInputHandlerSubject(failureMetadata, subject!!) } .that(handler) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt index aa29808bd9ee..657a95a3261e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt @@ -63,7 +63,7 @@ private constructor(failureMetadata: FailureMetadata, subject: QSTileState?) : companion object { /** Returns a factory to be used with [Truth.assertAbout]. */ - fun states(): Factory<QSTileStateSubject, QSTileState?> { + fun states(): Factory<QSTileStateSubject, QSTileState> { return Factory { failureMetadata: FailureMetadata, subject: QSTileState? -> QSTileStateSubject(failureMetadata, subject) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/TileSubject.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/TileSubject.kt index d2351dc8ae18..cc8b44b0dade 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/TileSubject.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/TileSubject.kt @@ -59,7 +59,7 @@ class TileSubject private constructor(failureMetadata: FailureMetadata, subject: companion object { /** Returns a factory to be used with [Truth.assertAbout]. */ - fun tiles(): Factory<TileSubject, Tile?> { + fun tiles(): Factory<TileSubject, Tile> { return Factory { failureMetadata: FailureMetadata, subject: Tile? -> TileSubject(failureMetadata, subject) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt index e99f61e7cd13..067e420b89c3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt @@ -25,12 +25,13 @@ import com.android.systemui.statusbar.notification.people.peopleNotificationIden import com.android.systemui.statusbar.notification.row.icon.notificationIconStyleProvider val Kosmos.entryAdapterFactory by -Kosmos.Fixture { - EntryAdapterFactoryImpl( - mockNotificationActivityStarter, - metricsLogger, - peopleNotificationIdentifier, - notificationIconStyleProvider, - visualStabilityCoordinator, - ) -}
\ No newline at end of file + Kosmos.Fixture { + EntryAdapterFactoryImpl( + mockNotificationActivityStarter, + metricsLogger, + peopleNotificationIdentifier, + notificationIconStyleProvider, + visualStabilityCoordinator, + mockNotificationActionClickManager, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt index ff4fbf9f0e94..6a674ca29ca4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt @@ -37,6 +37,7 @@ import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.media.NotificationMediaManager import com.android.systemui.media.controls.util.MediaFeatureFlag import com.android.systemui.media.dialog.MediaOutputDialogManager import com.android.systemui.plugins.ActivityStarter @@ -45,7 +46,6 @@ import com.android.systemui.settings.UserTracker import com.android.systemui.shared.system.ActivityManagerWrapper import com.android.systemui.shared.system.DevicePolicyManagerWrapper import com.android.systemui.shared.system.PackageManagerWrapper -import com.android.systemui.statusbar.NotificationMediaManager import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.RankingBuilder @@ -375,6 +375,7 @@ class ExpandableNotificationRowBuilder( Mockito.mock(PeopleNotificationIdentifier::class.java), Mockito.mock(NotificationIconStyleProvider::class.java), Mockito.mock(VisualStabilityCoordinator::class.java), + Mockito.mock(NotificationActionClickManager::class.java), ) .create(entry) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManagerKosmos.kt new file mode 100644 index 000000000000..8e62ae8825f3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManagerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import com.android.systemui.kosmos.Kosmos +import org.mockito.kotlin.mock + +var Kosmos.mockNotificationActionClickManager: NotificationActionClickManager by + Kosmos.Fixture { mock<NotificationActionClickManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt index 8ff7c7d01fb3..3e96fd7c729f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.phone.ongoingcall.shared.model import android.app.PendingIntent +import com.android.systemui.activity.data.repository.activityManagerRepository +import com.android.systemui.activity.data.repository.fake import com.android.systemui.kosmos.Kosmos import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.core.StatusBarConnectedDisplays @@ -38,6 +40,7 @@ fun inCallModel( notificationKey: String = "test", appName: String = "", promotedContent: PromotedNotificationContentModel? = null, + isAppVisible: Boolean = false, ) = OngoingCallModel.InCall( startTimeMs, @@ -46,6 +49,7 @@ fun inCallModel( notificationKey, appName, promotedContent, + isAppVisible, ) object OngoingCallTestHelper { @@ -77,8 +81,10 @@ object OngoingCallTestHelper { contentIntent: PendingIntent? = null, uid: Int = DEFAULT_UID, appName: String = "Fake name", + isAppVisible: Boolean = false, ) { if (StatusBarChipsModernization.isEnabled) { + activityManagerRepository.fake.startingIsAppVisibleValue = isAppVisible activeNotificationListRepository.addNotif( activeNotificationModel( key = key, @@ -100,6 +106,7 @@ object OngoingCallTestHelper { notificationKey = key, appName = appName, promotedContent = promotedContent, + isAppVisible = isAppVisible, ) ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileDomainInteractorKairosKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileDomainInteractorKairosKosmos.kt new file mode 100644 index 000000000000..d9a327b99c7f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileDomainInteractorKairosKosmos.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.domain.interactor + +import android.content.applicationContext +import com.android.systemui.flags.featureFlagsClassic +import com.android.systemui.kairos.ActivatedKairosFixture +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.log.table.logcatTableLogBuffer +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepositoryKairos +import com.android.systemui.statusbar.pipeline.shared.data.repository.connectivityRepository +import com.android.systemui.statusbar.policy.data.repository.userSetupRepository +import com.android.systemui.util.carrierConfigTracker + +@ExperimentalKairosApi +val Kosmos.mobileIconsInteractorKairos by ActivatedKairosFixture { + MobileIconsInteractorKairosImpl( + mobileConnectionsRepositoryKairos, + carrierConfigTracker, + logcatTableLogBuffer(this), + connectivityRepository, + userSetupRepository, + applicationContext, + featureFlagsClassic, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosKosmos.kt new file mode 100644 index 000000000000..3ee33802e9d5 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.mobile.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.stackedMobileIconViewModelKairos by + Kosmos.Fixture { StackedMobileIconViewModelKairos(mobileIconsViewModel) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/FakeSqueezeEffectRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/FakeSqueezeEffectRepository.kt new file mode 100644 index 000000000000..2d2a81586515 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/FakeSqueezeEffectRepository.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.topwindoweffects.data.repository + +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeSqueezeEffectRepository : SqueezeEffectRepository { + override val isSqueezeEffectEnabled = MutableStateFlow(false) +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt new file mode 100644 index 000000000000..aa8bb6b1e104 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.topwindoweffects.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.fakeSqueezeEffectRepository by Kosmos.Fixture { FakeSqueezeEffectRepository() }
\ No newline at end of file diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/FakeUiEvent.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/FakeUiEvent.kt index 48cd345b1f68..6c10526a17fe 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/FakeUiEvent.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/FakeUiEvent.kt @@ -24,7 +24,7 @@ import com.google.common.truth.Correspondence object FakeUiEvent { val EVENT_ID = Correspondence.transforming<FakeUiEvent, Int>( - { it?.eventId }, + { it.eventId }, "has a eventId of", ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/LogMaker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/LogMaker.kt index 3f0a95248d9c..9664bf3d6cc9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/LogMaker.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/truth/correspondence/LogMaker.kt @@ -23,7 +23,7 @@ import com.google.common.truth.Correspondence object LogMaker { val CATEGORY = Correspondence.transforming<LogMaker, Int>( - { it?.category }, + { it.category }, "has a category of", ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/CarrierConfigTrackerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/CarrierConfigTrackerKosmos.kt new file mode 100644 index 000000000000..58473693954c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/CarrierConfigTrackerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mockFixture + +var Kosmos.carrierConfigTracker: CarrierConfigTracker by mockFixture() diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index ccbc46fdb03b..5424ac3bd897 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -16,7 +16,7 @@ filegroup { srcs: [ "texts/ravenwood-common-policies.txt", ], - visibility: ["//visibility:private"], + visibility: [":__subpackages__"], } filegroup { @@ -44,6 +44,22 @@ filegroup { } filegroup { + name: "ravenwood-standard-annotations", + srcs: [ + "texts/ravenwood-standard-annotations.txt", + ], + visibility: [":__subpackages__"], +} + +filegroup { + name: "ravenizer-standard-options", + srcs: [ + "texts/ravenizer-standard-options.txt", + ], + visibility: [":__subpackages__"], +} + +filegroup { name: "ravenwood-annotation-allowed-classes", srcs: [ "texts/ravenwood-annotation-allowed-classes.txt", diff --git a/ravenwood/Framework.bp b/ravenwood/Framework.bp index e36677189e02..f5b075b17fd4 100644 --- a/ravenwood/Framework.bp +++ b/ravenwood/Framework.bp @@ -33,6 +33,7 @@ genrule_defaults { ":ravenwood-common-policies", ":ravenwood-framework-policies", ":ravenwood-standard-options", + ":ravenwood-standard-annotations", ":ravenwood-annotation-allowed-classes", ], out: [ @@ -44,6 +45,7 @@ genrule_defaults { framework_minus_apex_cmd = "$(location hoststubgen) " + "@$(location :ravenwood-standard-options) " + + "@$(location :ravenwood-standard-annotations) " + "--debug-log $(location hoststubgen_framework-minus-apex.log) " + "--out-jar $(location ravenwood.jar) " + "--in-jar $(location :framework-minus-apex-for-host) " + @@ -178,6 +180,7 @@ java_genrule { tools: ["hoststubgen"], cmd: "$(location hoststubgen) " + "@$(location :ravenwood-standard-options) " + + "@$(location :ravenwood-standard-annotations) " + "--debug-log $(location hoststubgen_services.core.log) " + "--stats-file $(location hoststubgen_services.core_stats.csv) " + @@ -196,6 +199,7 @@ java_genrule { ":ravenwood-common-policies", ":ravenwood-services-policies", ":ravenwood-standard-options", + ":ravenwood-standard-annotations", ":ravenwood-annotation-allowed-classes", ], out: [ @@ -247,6 +251,7 @@ java_genrule { tools: ["hoststubgen"], cmd: "$(location hoststubgen) " + "@$(location :ravenwood-standard-options) " + + "@$(location :ravenwood-standard-annotations) " + "--debug-log $(location hoststubgen_core-icu4j-for-host.log) " + "--stats-file $(location hoststubgen_core-icu4j-for-host_stats.csv) " + @@ -265,6 +270,7 @@ java_genrule { ":ravenwood-common-policies", ":icu-ravenwood-policies", ":ravenwood-standard-options", + ":ravenwood-standard-annotations", ], out: [ "ravenwood.jar", @@ -301,6 +307,7 @@ java_genrule { tools: ["hoststubgen"], cmd: "$(location hoststubgen) " + "@$(location :ravenwood-standard-options) " + + "@$(location :ravenwood-standard-annotations) " + "--debug-log $(location framework-configinfrastructure.log) " + "--stats-file $(location framework-configinfrastructure_stats.csv) " + @@ -319,6 +326,7 @@ java_genrule { ":ravenwood-common-policies", ":framework-configinfrastructure-ravenwood-policies", ":ravenwood-standard-options", + ":ravenwood-standard-annotations", ], out: [ "ravenwood.jar", @@ -355,6 +363,7 @@ java_genrule { tools: ["hoststubgen"], cmd: "$(location hoststubgen) " + "@$(location :ravenwood-standard-options) " + + "@$(location :ravenwood-standard-annotations) " + "--debug-log $(location framework-statsd.log) " + "--stats-file $(location framework-statsd_stats.csv) " + @@ -373,6 +382,7 @@ java_genrule { ":ravenwood-common-policies", ":framework-statsd-ravenwood-policies", ":ravenwood-standard-options", + ":ravenwood-standard-annotations", ], out: [ "ravenwood.jar", @@ -409,6 +419,7 @@ java_genrule { tools: ["hoststubgen"], cmd: "$(location hoststubgen) " + "@$(location :ravenwood-standard-options) " + + "@$(location :ravenwood-standard-annotations) " + "--debug-log $(location framework-graphics.log) " + "--stats-file $(location framework-graphics_stats.csv) " + @@ -427,6 +438,7 @@ java_genrule { ":ravenwood-common-policies", ":framework-graphics-ravenwood-policies", ":ravenwood-standard-options", + ":ravenwood-standard-annotations", ], out: [ "ravenwood.jar", diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING index df63cb9dfc50..1148539187ac 100644 --- a/ravenwood/TEST_MAPPING +++ b/ravenwood/TEST_MAPPING @@ -150,6 +150,10 @@ "host": true }, { + "name": "RavenwoodCoreTest-light", + "host": true + }, + { "name": "RavenwoodMinimumTest", "host": true }, @@ -168,6 +172,10 @@ { "name": "RavenwoodServicesTest", "host": true + }, + { + "name": "UinputTestsRavenwood", + "host": true } // AUTO-GENERATED-END ], diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java index 3e2c4051b792..f25ae6a34242 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java @@ -128,28 +128,6 @@ public class RavenwoodUtils { runOnHandlerSync(getMainHandler(), r); } - public static class MockitoHelper { - private MockitoHelper() { - } - - /** - * Allow verifyZeroInteractions to work on ravenwood. It was replaced with a different - * method on. (Maybe we should do it in Ravenizer.) - */ - public static void verifyZeroInteractions(Object... mocks) { - if (RavenwoodRule.isOnRavenwood()) { - // Mockito 4 or later - reflectMethod("org.mockito.Mockito", "verifyNoInteractions", Object[].class) - .callStatic(new Object[]{mocks}); - } else { - // Mockito 2 - reflectMethod("org.mockito.Mockito", "verifyZeroInteractions", Object[].class) - .callStatic(new Object[]{mocks}); - } - } - } - - /** * Wrap the given {@link Supplier} to become memoized. * diff --git a/ravenwood/scripts/extract-last-soong-commands.py b/ravenwood/scripts/extract-last-soong-commands.py index 0629b77029e0..b8d6f2042389 100755 --- a/ravenwood/scripts/extract-last-soong-commands.py +++ b/ravenwood/scripts/extract-last-soong-commands.py @@ -55,7 +55,7 @@ def main(args): if s.startswith("verbose"): continue - if re.match('^\[.*bootstrap blueprint', s): + if re.match('^\\[.*bootstrap blueprint', s): continue s = s.rstrip() diff --git a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java new file mode 100644 index 000000000000..cdfd4a877f43 --- /dev/null +++ b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ravenwoodtest.bivalenttest.ravenizer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; + +import org.junit.Test; + +import java.io.FileDescriptor; +import java.util.LinkedHashMap; +import java.util.regex.Pattern; + +public class RavenwoodJdkPatchTest { + + @Test + public void testUnicodeRegex() { + var pattern = Pattern.compile("\\w+"); + assertTrue(pattern.matcher("über").matches()); + } + + @Test + public void testLinkedHashMapEldest() { + var map = new LinkedHashMap<String, String>(); + map.put("a", "b"); + map.put("x", "y"); + assertEquals(map.entrySet().iterator().next(), map.eldest()); + } + + @Test + public void testFileDescriptorGetSetInt() throws ErrnoException { + FileDescriptor fd = Os.open("/dev/zero", OsConstants.O_RDONLY, 0); + try { + int fdRaw = fd.getInt$(); + assertNotEquals(-1, fdRaw); + fd.setInt$(-1); + assertEquals(-1, fd.getInt$()); + fd.setInt$(fdRaw); + Os.close(fd); + assertEquals(-1, fd.getInt$()); + } finally { + Os.close(fd); + } + } +} diff --git a/ravenwood/texts/ravenizer-standard-options.txt b/ravenwood/texts/ravenizer-standard-options.txt new file mode 100644 index 000000000000..cef736f87e72 --- /dev/null +++ b/ravenwood/texts/ravenizer-standard-options.txt @@ -0,0 +1,13 @@ +# File containing standard options to Ravenizer for Ravenwood + +# Keep all classes / methods / fields in tests and its target +--default-keep + +--delete-finals + +# Include standard annotations +@jar:texts/ravenwood-standard-annotations.txt + +# Apply common policies +--policy-override-file + jar:texts/ravenwood-common-policies.txt diff --git a/ravenwood/texts/ravenwood-standard-annotations.txt b/ravenwood/texts/ravenwood-standard-annotations.txt new file mode 100644 index 000000000000..75ec5cadb6fc --- /dev/null +++ b/ravenwood/texts/ravenwood-standard-annotations.txt @@ -0,0 +1,37 @@ +# Standard annotations. +# Note, each line is a single argument, so we need newlines after each `--xxx-annotation`. +--keep-annotation + android.ravenwood.annotation.RavenwoodKeep + +--keep-annotation + android.ravenwood.annotation.RavenwoodKeepPartialClass + +--keep-class-annotation + android.ravenwood.annotation.RavenwoodKeepWholeClass + +--throw-annotation + android.ravenwood.annotation.RavenwoodThrow + +--remove-annotation + android.ravenwood.annotation.RavenwoodRemove + +--ignore-annotation + android.ravenwood.annotation.RavenwoodIgnore + +--partially-allowed-annotation + android.ravenwood.annotation.RavenwoodPartiallyAllowlisted + +--substitute-annotation + android.ravenwood.annotation.RavenwoodReplace + +--redirect-annotation + android.ravenwood.annotation.RavenwoodRedirect + +--redirection-class-annotation + android.ravenwood.annotation.RavenwoodRedirectionClass + +--class-load-hook-annotation + android.ravenwood.annotation.RavenwoodClassLoadHook + +--keep-static-initializer-annotation + android.ravenwood.annotation.RavenwoodKeepStaticInitializer diff --git a/ravenwood/texts/ravenwood-standard-options.txt b/ravenwood/texts/ravenwood-standard-options.txt index 233657557747..0a650254a71f 100644 --- a/ravenwood/texts/ravenwood-standard-options.txt +++ b/ravenwood/texts/ravenwood-standard-options.txt @@ -15,41 +15,3 @@ #--default-class-load-hook # com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded - -# Standard annotations. -# Note, each line is a single argument, so we need newlines after each `--xxx-annotation`. ---keep-annotation - android.ravenwood.annotation.RavenwoodKeep - ---keep-annotation - android.ravenwood.annotation.RavenwoodKeepPartialClass - ---keep-class-annotation - android.ravenwood.annotation.RavenwoodKeepWholeClass - ---throw-annotation - android.ravenwood.annotation.RavenwoodThrow - ---remove-annotation - android.ravenwood.annotation.RavenwoodRemove - ---ignore-annotation - android.ravenwood.annotation.RavenwoodIgnore - ---partially-allowed-annotation - android.ravenwood.annotation.RavenwoodPartiallyAllowlisted - ---substitute-annotation - android.ravenwood.annotation.RavenwoodReplace - ---redirect-annotation - android.ravenwood.annotation.RavenwoodRedirect - ---redirection-class-annotation - android.ravenwood.annotation.RavenwoodRedirectionClass - ---class-load-hook-annotation - android.ravenwood.annotation.RavenwoodClassLoadHook - ---keep-static-initializer-annotation - android.ravenwood.annotation.RavenwoodKeepStaticInitializer diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt index f59e143c1e4e..ae0a00855650 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt @@ -15,8 +15,6 @@ */ package com.android.hoststubgen -import java.io.File - /** * We will not print the stack trace for exceptions implementing it. */ @@ -64,9 +62,6 @@ class DuplicateAnnotationException(annotationName: String?) : class InputFileNotFoundException(filename: String) : ArgumentsException("File '$filename' not found") -fun String.ensureFileExists(): String { - if (!File(this).exists()) { - throw InputFileNotFoundException(this) - } - return this -} +/** Thrown when a JAR resource does not exist. */ +class JarResourceNotFoundException(path: String) : + ArgumentsException("JAR resource '$path' not found") diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt index 4fe21eac6972..98f96a89d889 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt @@ -16,6 +16,7 @@ package com.android.hoststubgen import com.android.hoststubgen.asm.ClassNodes +import com.android.hoststubgen.asm.findAnyAnnotation import com.android.hoststubgen.filters.AnnotationBasedFilter import com.android.hoststubgen.filters.ClassWidePolicyPropagatingFilter import com.android.hoststubgen.filters.ConstantFilter @@ -26,21 +27,25 @@ import com.android.hoststubgen.filters.KeepNativeFilter import com.android.hoststubgen.filters.OutputFilter import com.android.hoststubgen.filters.SanitizationFilter import com.android.hoststubgen.filters.TextFileFilterPolicyBuilder +import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep import com.android.hoststubgen.utils.ClassPredicate import com.android.hoststubgen.visitors.BaseAdapter +import com.android.hoststubgen.visitors.ImplGeneratingAdapter import com.android.hoststubgen.visitors.PackageRedirectRemapper +import java.io.PrintWriter import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassVisitor import org.objectweb.asm.ClassWriter import org.objectweb.asm.commons.ClassRemapper import org.objectweb.asm.util.CheckClassAdapter +import org.objectweb.asm.util.TraceClassVisitor /** * This class implements bytecode transformation of HostStubGen. */ class HostStubGenClassProcessor( private val options: HostStubGenClassProcessorOptions, - private val allClasses: ClassNodes, + val allClasses: ClassNodes, private val errors: HostStubGenErrors = HostStubGenErrors(), private val stats: HostStubGenStats? = null, ) { @@ -48,6 +53,7 @@ class HostStubGenClassProcessor( val remapper = FilterRemapper(filter) private val packageRedirector = PackageRedirectRemapper(options.packageRedirects) + private val processedAnnotation = setOf(HostStubGenProcessedAsKeep.CLASS_DESCRIPTOR) /** * Build the filter, which decides what classes/methods/fields should be put in stub or impl @@ -130,15 +136,10 @@ class HostStubGenClassProcessor( return filter } - fun processClassBytecode(bytecode: ByteArray): ByteArray { - val cr = ClassReader(bytecode) - - // COMPUTE_FRAMES wouldn't be happy if code uses - val flags = ClassWriter.COMPUTE_MAXS // or ClassWriter.COMPUTE_FRAMES - val cw = ClassWriter(flags) + private fun buildVisitor(base: ClassVisitor, className: String): ClassVisitor { + // Connect to the base visitor + var outVisitor: ClassVisitor = base - // Connect to the class writer - var outVisitor: ClassVisitor = cw if (options.enableClassChecker.get) { outVisitor = CheckClassAdapter(outVisitor) } @@ -149,15 +150,59 @@ class HostStubGenClassProcessor( val visitorOptions = BaseAdapter.Options( errors = errors, stats = stats, - enablePreTrace = options.enablePreTrace.get, - enablePostTrace = options.enablePostTrace.get, deleteClassFinals = options.deleteFinals.get, deleteMethodFinals = options.deleteFinals.get, ) - outVisitor = BaseAdapter.getVisitor( - cr.className, allClasses, outVisitor, filter, - packageRedirector, visitorOptions - ) + + val verbosePrinter = PrintWriter(log.getWriter(LogLevel.Verbose)) + + // Inject TraceClassVisitor for debugging. + if (options.enablePostTrace.get) { + outVisitor = TraceClassVisitor(outVisitor, verbosePrinter) + } + + // Handle --package-redirect + if (!packageRedirector.isEmpty) { + // Don't apply the remapper on redirect-from classes. + // Otherwise, if the target jar actually contains the "from" classes (which + // may or may not be the case) they'd be renamed. + // But we update all references in other places, so, a method call to a "from" class + // would be replaced with the "to" class. All type references (e.g. variable types) + // will be updated too. + if (!packageRedirector.isTarget(className)) { + outVisitor = ClassRemapper(outVisitor, packageRedirector) + } else { + log.v( + "Class $className is a redirect-from class, not applying" + + " --package-redirect" + ) + } + } + + outVisitor = ImplGeneratingAdapter(allClasses, outVisitor, filter, visitorOptions) + + // Inject TraceClassVisitor for debugging. + if (options.enablePreTrace.get) { + outVisitor = TraceClassVisitor(outVisitor, verbosePrinter) + } + + return outVisitor + } + + fun processClassBytecode(bytecode: ByteArray): ByteArray { + val cr = ClassReader(bytecode) + + // If the class was already processed previously, skip + val clz = allClasses.getClass(cr.className) + if (clz.findAnyAnnotation(processedAnnotation) != null) { + return bytecode + } + + // COMPUTE_FRAMES wouldn't be happy if code uses + val flags = ClassWriter.COMPUTE_MAXS // or ClassWriter.COMPUTE_FRAMES + val cw = ClassWriter(flags) + + val outVisitor = buildVisitor(cw, cr.className) cr.accept(outVisitor, ClassReader.EXPAND_FRAMES) return cw.toByteArray() diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt index c7c45e65a8b6..e7166f11f597 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt @@ -18,6 +18,7 @@ package com.android.hoststubgen import com.android.hoststubgen.filters.FilterPolicy import com.android.hoststubgen.utils.ArgIterator import com.android.hoststubgen.utils.BaseOptions +import com.android.hoststubgen.utils.FileOrResource import com.android.hoststubgen.utils.SetOnce private fun parsePackageRedirect(fromColonTo: String): Pair<String, String> { @@ -53,7 +54,7 @@ open class HostStubGenClassProcessorOptions( var defaultClassLoadHook: SetOnce<String?> = SetOnce(null), var defaultMethodCallHook: SetOnce<String?> = SetOnce(null), - var policyOverrideFiles: MutableList<String> = mutableListOf(), + var policyOverrideFiles: MutableList<FileOrResource> = mutableListOf(), var defaultPolicy: SetOnce<FilterPolicy> = SetOnce(FilterPolicy.Remove), @@ -73,15 +74,14 @@ open class HostStubGenClassProcessorOptions( return name } - override fun parseOption(option: String, ai: ArgIterator): Boolean { + override fun parseOption(option: String, args: ArgIterator): Boolean { // Define some shorthands... - fun nextArg(): String = ai.nextArgRequired(option) + fun nextArg(): String = args.nextArgRequired(option) fun MutableSet<String>.addUniqueAnnotationArg(): String = nextArg().also { this += ensureUniqueAnnotation(it) } when (option) { - "--policy-override-file" -> - policyOverrideFiles.add(nextArg().ensureFileExists()) + "--policy-override-file" -> policyOverrideFiles.add(FileOrResource(nextArg())) "--default-remove" -> defaultPolicy.set(FilterPolicy.Remove) "--default-throw" -> defaultPolicy.set(FilterPolicy.Throw) diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt index b41ce0f65017..112ef01e20cb 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt @@ -217,7 +217,7 @@ private val numericalInnerClassName = """.*\$\d+$""".toRegex() fun isAnonymousInnerClass(cn: ClassNode): Boolean { // TODO: Is there a better way? - return cn.name.matches(numericalInnerClassName) + return cn.outerClass != null && cn.name.matches(numericalInnerClassName) } /** diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt index b8b0d8a31268..7f36aca33eee 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt @@ -19,9 +19,9 @@ package com.android.hoststubgen.filters * Base class for an [OutputFilter] that uses another filter as a fallback. */ abstract class DelegatingFilter( - // fallback shouldn't be used by subclasses directly, so make it private. - // They should instead be calling into `super` or `outermostFilter`. - private val fallback: OutputFilter + // fallback shouldn't be used by subclasses directly, so make it private. + // They should instead be calling into `super` or `outermostFilter`. + private val fallback: OutputFilter ) : OutputFilter() { init { fallback.outermostFilter = this @@ -50,24 +50,24 @@ abstract class DelegatingFilter( } override fun getPolicyForField( - className: String, - fieldName: String + className: String, + fieldName: String ): FilterPolicyWithReason { return fallback.getPolicyForField(className, fieldName) } override fun getPolicyForMethod( - className: String, - methodName: String, - descriptor: String + className: String, + methodName: String, + descriptor: String ): FilterPolicyWithReason { return fallback.getPolicyForMethod(className, methodName, descriptor) } override fun getRenameTo( - className: String, - methodName: String, - descriptor: String + className: String, + methodName: String, + descriptor: String ): String? { return fallback.getRenameTo(className, methodName, descriptor) } @@ -97,13 +97,12 @@ abstract class DelegatingFilter( } override fun getMethodCallReplaceTo( - callerClassName: String, - callerMethodName: String, className: String, methodName: String, descriptor: String, ): MethodReplaceTarget? { return fallback.getMethodCallReplaceTo( - callerClassName, callerMethodName, className, methodName, descriptor) + className, methodName, descriptor + ) } } diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt index 474da6dfa1b9..d44d016f7c5b 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt @@ -16,7 +16,6 @@ package com.android.hoststubgen.filters import com.android.hoststubgen.HostStubGenErrors -import com.android.hoststubgen.HostStubGenInternalException import com.android.hoststubgen.asm.CLASS_INITIALIZER_DESC import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME import com.android.hoststubgen.asm.ClassNodes @@ -37,19 +36,15 @@ import org.objectweb.asm.tree.ClassNode * TODO: Do we need a way to make anonymous class methods and lambdas "throw"? */ class ImplicitOutputFilter( - private val errors: HostStubGenErrors, - private val classes: ClassNodes, - fallback: OutputFilter + private val errors: HostStubGenErrors, + private val classes: ClassNodes, + fallback: OutputFilter ) : DelegatingFilter(fallback) { - private fun getClassImplicitPolicy(className: String, cn: ClassNode): FilterPolicyWithReason? { + private fun getClassImplicitPolicy(cn: ClassNode): FilterPolicyWithReason? { if (isAnonymousInnerClass(cn)) { log.forDebug { // log.d(" anon-inner class: ${className} outer: ${cn.outerClass} ") } - if (cn.outerClass == null) { - throw HostStubGenInternalException( - "outerClass is null for anonymous inner class") - } // If the outer class needs to be in impl, it should be in impl too. val outerPolicy = outermostFilter.getPolicyForClass(cn.outerClass) if (outerPolicy.policy.needsInOutput) { @@ -65,15 +60,15 @@ class ImplicitOutputFilter( val cn = classes.getClass(className) // Use the implicit policy, if any. - getClassImplicitPolicy(className, cn)?.let { return it } + getClassImplicitPolicy(cn)?.let { return it } return fallback } override fun getPolicyForMethod( - className: String, - methodName: String, - descriptor: String + className: String, + methodName: String, + descriptor: String ): FilterPolicyWithReason { val fallback = super.getPolicyForMethod(className, methodName, descriptor) val classPolicy = outermostFilter.getPolicyForClass(className) @@ -84,12 +79,14 @@ class ImplicitOutputFilter( // "keep" instead. // Unless it's an enum -- in that case, the below code would handle it. if (!cn.isEnum() && - fallback.policy == FilterPolicy.Throw && - methodName == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC) { + fallback.policy == FilterPolicy.Throw && + methodName == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC + ) { // TODO Maybe show a warning?? But that'd be too noisy with --default-throw. return FilterPolicy.Ignore.withReason( "'throw' on static initializer is handled as 'ignore'" + - " [original throw reason: ${fallback.reason}]") + " [original throw reason: ${fallback.reason}]" + ) } log.d("Class ${cn.name} Class policy: $classPolicy") @@ -120,7 +117,8 @@ class ImplicitOutputFilter( // For synthetic methods (such as lambdas), let's just inherit the class's // policy. return memberPolicy.withReason(classPolicy.reason).wrapReason( - "is-synthetic-method") + "is-synthetic-method" + ) } } } @@ -129,8 +127,8 @@ class ImplicitOutputFilter( } override fun getPolicyForField( - className: String, - fieldName: String + className: String, + fieldName: String ): FilterPolicyWithReason { val fallback = super.getPolicyForField(className, fieldName) @@ -161,4 +159,4 @@ class ImplicitOutputFilter( return fallback } -}
\ No newline at end of file +} diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt index fc885d6f463b..59da3da99ea5 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt @@ -28,10 +28,12 @@ class InMemoryOutputFilter( private val classes: ClassNodes, fallback: OutputFilter, ) : DelegatingFilter(fallback) { - private val mPolicies: MutableMap<String, FilterPolicyWithReason> = mutableMapOf() - private val mRenames: MutableMap<String, String> = mutableMapOf() - private val mRedirectionClasses: MutableMap<String, String> = mutableMapOf() - private val mClassLoadHooks: MutableMap<String, String> = mutableMapOf() + private val mPolicies = mutableMapOf<String, FilterPolicyWithReason>() + private val mRenames = mutableMapOf<String, String>() + private val mRedirectionClasses = mutableMapOf<String, String>() + private val mClassLoadHooks = mutableMapOf<String, String>() + private val mMethodCallReplaceSpecs = mutableListOf<MethodCallReplaceSpec>() + private val mTypeRenameSpecs = mutableListOf<TypeRenameSpec>() private fun getClassKey(className: String): String { return className.toHumanReadableClassName() @@ -45,10 +47,6 @@ class InMemoryOutputFilter( return getClassKey(className) + "." + methodName + ";" + signature } - override fun getPolicyForClass(className: String): FilterPolicyWithReason { - return mPolicies[getClassKey(className)] ?: super.getPolicyForClass(className) - } - private fun checkClass(className: String) { if (classes.findClass(className) == null) { log.w("Unknown class $className") @@ -74,6 +72,10 @@ class InMemoryOutputFilter( } } + override fun getPolicyForClass(className: String): FilterPolicyWithReason { + return mPolicies[getClassKey(className)] ?: super.getPolicyForClass(className) + } + fun setPolicyForClass(className: String, policy: FilterPolicyWithReason) { checkClass(className) mPolicies[getClassKey(className)] = policy @@ -81,7 +83,7 @@ class InMemoryOutputFilter( override fun getPolicyForField(className: String, fieldName: String): FilterPolicyWithReason { return mPolicies[getFieldKey(className, fieldName)] - ?: super.getPolicyForField(className, fieldName) + ?: super.getPolicyForField(className, fieldName) } fun setPolicyForField(className: String, fieldName: String, policy: FilterPolicyWithReason) { @@ -90,21 +92,21 @@ class InMemoryOutputFilter( } override fun getPolicyForMethod( - className: String, - methodName: String, - descriptor: String, - ): FilterPolicyWithReason { + className: String, + methodName: String, + descriptor: String, + ): FilterPolicyWithReason { return mPolicies[getMethodKey(className, methodName, descriptor)] ?: mPolicies[getMethodKey(className, methodName, "*")] ?: super.getPolicyForMethod(className, methodName, descriptor) } fun setPolicyForMethod( - className: String, - methodName: String, - descriptor: String, - policy: FilterPolicyWithReason, - ) { + className: String, + methodName: String, + descriptor: String, + policy: FilterPolicyWithReason, + ) { checkMethod(className, methodName, descriptor) mPolicies[getMethodKey(className, methodName, descriptor)] = policy } @@ -123,7 +125,7 @@ class InMemoryOutputFilter( override fun getRedirectionClass(className: String): String? { return mRedirectionClasses[getClassKey(className)] - ?: super.getRedirectionClass(className) + ?: super.getRedirectionClass(className) } fun setRedirectionClass(from: String, to: String) { @@ -135,11 +137,52 @@ class InMemoryOutputFilter( } override fun getClassLoadHooks(className: String): List<String> { - return addNonNullElement(super.getClassLoadHooks(className), - mClassLoadHooks[getClassKey(className)]) + return addNonNullElement( + super.getClassLoadHooks(className), + mClassLoadHooks[getClassKey(className)] + ) } fun setClassLoadHook(className: String, methodName: String) { mClassLoadHooks[getClassKey(className)] = methodName.toHumanReadableMethodName() } + + override fun hasAnyMethodCallReplace(): Boolean { + return mMethodCallReplaceSpecs.isNotEmpty() || super.hasAnyMethodCallReplace() + } + + override fun getMethodCallReplaceTo( + className: String, + methodName: String, + descriptor: String, + ): MethodReplaceTarget? { + // Maybe use 'Tri' if we end up having too many replacements. + mMethodCallReplaceSpecs.forEach { + if (className == it.fromClass && + methodName == it.fromMethod + ) { + if (it.fromDescriptor == "*" || descriptor == it.fromDescriptor) { + return MethodReplaceTarget(it.toClass, it.toMethod) + } + } + } + return super.getMethodCallReplaceTo(className, methodName, descriptor) + } + + fun setMethodCallReplaceSpec(spec: MethodCallReplaceSpec) { + mMethodCallReplaceSpecs.add(spec) + } + + override fun remapType(className: String): String? { + mTypeRenameSpecs.forEach { + if (it.typeInternalNamePattern.matcher(className).matches()) { + return it.typeInternalNamePrefix + className + } + } + return super.remapType(className) + } + + fun setRemapTypeSpec(spec: TypeRenameSpec) { + mTypeRenameSpecs.add(spec) + } } diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt index f99ce906240a..c47bb302920f 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt @@ -41,10 +41,10 @@ abstract class OutputFilter { abstract fun getPolicyForField(className: String, fieldName: String): FilterPolicyWithReason abstract fun getPolicyForMethod( - className: String, - methodName: String, - descriptor: String, - ): FilterPolicyWithReason + className: String, + methodName: String, + descriptor: String, + ): FilterPolicyWithReason /** * If a given method is a substitute-from method, return the substitute-to method name. @@ -108,8 +108,6 @@ abstract class OutputFilter { * If a method call should be forwarded to another method, return the target's class / method. */ open fun getMethodCallReplaceTo( - callerClassName: String, - callerMethodName: String, className: String, methodName: String, descriptor: String, diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt index dd353e9caeff..97fc35302528 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt @@ -22,13 +22,13 @@ import com.android.hoststubgen.asm.toHumanReadableClassName import com.android.hoststubgen.asm.toJvmClassName import com.android.hoststubgen.log import com.android.hoststubgen.normalizeTextLine +import com.android.hoststubgen.utils.FileOrResource import com.android.hoststubgen.whitespaceRegex -import org.objectweb.asm.tree.ClassNode import java.io.BufferedReader -import java.io.FileReader import java.io.PrintWriter import java.io.Reader import java.util.regex.Pattern +import org.objectweb.asm.tree.ClassNode /** * Print a class node as a "keep" policy. @@ -58,6 +58,23 @@ enum class SpecialClass { RFile, } +data class MethodCallReplaceSpec( + val fromClass: String, + val fromMethod: String, + val fromDescriptor: String, + val toClass: String, + val toMethod: String, +) + +/** + * When a package name matches [typeInternalNamePattern], we prepend [typeInternalNamePrefix] + * to it. + */ +data class TypeRenameSpec( + val typeInternalNamePattern: Pattern, + val typeInternalNamePrefix: String, +) + /** * This receives [TextFileFilterPolicyBuilder] parsing result. */ @@ -99,7 +116,7 @@ interface PolicyFileProcessor { className: String, methodName: String, methodDesc: String, - replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec, + replaceSpec: MethodCallReplaceSpec, ) } @@ -116,9 +133,6 @@ class TextFileFilterPolicyBuilder( private var featureFlagsPolicy: FilterPolicyWithReason? = null private var syspropsPolicy: FilterPolicyWithReason? = null private var rFilePolicy: FilterPolicyWithReason? = null - private val typeRenameSpec = mutableListOf<TextFilePolicyRemapperFilter.TypeRenameSpec>() - private val methodReplaceSpec = - mutableListOf<TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec>() /** * Fields for a filter chain used for "partial allowlisting", which are used by @@ -126,47 +140,34 @@ class TextFileFilterPolicyBuilder( */ private val annotationAllowedInMemoryFilter: InMemoryOutputFilter val annotationAllowedMembersFilter: OutputFilter + get() = annotationAllowedInMemoryFilter private val annotationAllowedPolicy = FilterPolicy.AnnotationAllowed.withReason(FILTER_REASON) init { // Create a filter that checks "partial allowlisting". - var aaf: OutputFilter = ConstantFilter(FilterPolicy.Remove, "default disallowed") - - aaf = InMemoryOutputFilter(classes, aaf) - annotationAllowedInMemoryFilter = aaf - - annotationAllowedMembersFilter = annotationAllowedInMemoryFilter + val filter = ConstantFilter(FilterPolicy.Remove, "default disallowed") + annotationAllowedInMemoryFilter = InMemoryOutputFilter(classes, filter) } /** * Parse a given policy file. This method can be called multiple times to read from * multiple files. To get the resulting filter, use [createOutputFilter] */ - fun parse(file: String) { + fun parse(file: FileOrResource) { // We may parse multiple files, but we reuse the same parser, because the parser // will make sure there'll be no dupplicating "special class" policies. - parser.parse(FileReader(file), file, Processor()) + parser.parse(file.open(), file.path, Processor()) } /** * Generate the resulting [OutputFilter]. */ fun createOutputFilter(): OutputFilter { - var ret: OutputFilter = imf - if (typeRenameSpec.isNotEmpty()) { - ret = TextFilePolicyRemapperFilter(typeRenameSpec, ret) - } - if (methodReplaceSpec.isNotEmpty()) { - ret = TextFilePolicyMethodReplaceFilter(methodReplaceSpec, classes, ret) - } - // Wrap the in-memory-filter with AHF. - ret = AndroidHeuristicsFilter( - classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, ret + return AndroidHeuristicsFilter( + classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, imf ) - - return ret } private inner class Processor : PolicyFileProcessor { @@ -180,9 +181,7 @@ class TextFileFilterPolicyBuilder( } override fun onRename(pattern: Pattern, prefix: String) { - typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec( - pattern, prefix - ) + imf.setRemapTypeSpec(TypeRenameSpec(pattern, prefix)) } override fun onClassStart(className: String) { @@ -284,12 +283,12 @@ class TextFileFilterPolicyBuilder( className: String, methodName: String, methodDesc: String, - replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec, + replaceSpec: MethodCallReplaceSpec, ) { // Keep the source method, because the target method may call it. imf.setPolicyForMethod(className, methodName, methodDesc, FilterPolicy.Keep.withReason(FILTER_REASON)) - methodReplaceSpec.add(replaceSpec) + imf.setMethodCallReplaceSpec(replaceSpec) } } } @@ -630,13 +629,13 @@ class TextFileFilterPolicyParser { if (classAndMethod != null) { // If the substitution target contains a ".", then // it's a method call redirect. - val spec = TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec( - currentClassName!!.toJvmClassName(), - methodName, - signature, - classAndMethod.first.toJvmClassName(), - classAndMethod.second, - ) + val spec = MethodCallReplaceSpec( + className.toJvmClassName(), + methodName, + signature, + classAndMethod.first.toJvmClassName(), + classAndMethod.second, + ) processor.onMethodOutClassReplace( className, methodName, diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt deleted file mode 100644 index a3f934cacc2c..000000000000 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.hoststubgen.filters - -import com.android.hoststubgen.asm.ClassNodes - -/** - * Filter used by TextFileFilterPolicyParser for "method call relacement". - */ -class TextFilePolicyMethodReplaceFilter( - val spec: List<MethodCallReplaceSpec>, - val classes: ClassNodes, - val fallback: OutputFilter, -) : DelegatingFilter(fallback) { - - data class MethodCallReplaceSpec( - val fromClass: String, - val fromMethod: String, - val fromDescriptor: String, - val toClass: String, - val toMethod: String, - ) - - override fun hasAnyMethodCallReplace(): Boolean { - return true - } - - override fun getMethodCallReplaceTo( - callerClassName: String, - callerMethodName: String, - className: String, - methodName: String, - descriptor: String, - ): MethodReplaceTarget? { - // Maybe use 'Tri' if we end up having too many replacements. - spec.forEach { - if (className == it.fromClass && - methodName == it.fromMethod - ) { - if (it.fromDescriptor == "*" || descriptor == it.fromDescriptor) { - return MethodReplaceTarget(it.toClass, it.toMethod) - } - } - } - return null - } -} diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt deleted file mode 100644 index bc90d1248322..000000000000 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.hoststubgen.filters - -import java.util.regex.Pattern - -/** - * A filter that provides a simple "jarjar" functionality via [mapType] - */ -class TextFilePolicyRemapperFilter( - val typeRenameSpecs: List<TypeRenameSpec>, - fallback: OutputFilter, -) : DelegatingFilter(fallback) { - /** - * When a package name matches [typeInternalNamePattern], we prepend [typeInternalNamePrefix] - * to it. - */ - data class TypeRenameSpec( - val typeInternalNamePattern: Pattern, - val typeInternalNamePrefix: String, - ) - - override fun remapType(className: String): String? { - typeRenameSpecs.forEach { - if (it.typeInternalNamePattern.matcher(className).matches()) { - return it.typeInternalNamePrefix + className - } - } - return null - } -} diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt index 0b17879b862c..d0869929edfb 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt @@ -16,11 +16,16 @@ package com.android.hoststubgen.utils import com.android.hoststubgen.ArgumentsException -import com.android.hoststubgen.ensureFileExists +import com.android.hoststubgen.InputFileNotFoundException +import com.android.hoststubgen.JarResourceNotFoundException import com.android.hoststubgen.log import com.android.hoststubgen.normalizeTextLine -import java.io.BufferedReader +import java.io.File import java.io.FileReader +import java.io.InputStreamReader +import java.io.Reader + +const val JAR_RESOURCE_PREFIX = "jar:" /** * Base class for parsing arguments from commandline. @@ -73,7 +78,7 @@ abstract class BaseOptions { * * Subclasses override/extend this method to support more options. */ - abstract fun parseOption(option: String, ai: ArgIterator): Boolean + abstract fun parseOption(option: String, args: ArgIterator): Boolean abstract fun dumpFields(): String } @@ -112,7 +117,9 @@ class ArgIterator( companion object { fun withAtFiles(args: List<String>): ArgIterator { - return ArgIterator(expandAtFiles(args)) + val expanded = mutableListOf<String>() + expandAtFiles(args.asSequence(), expanded) + return ArgIterator(expanded) } /** @@ -125,34 +132,30 @@ class ArgIterator( * * The file can contain '#' as comments. */ - private fun expandAtFiles(args: List<String>): List<String> { - val ret = mutableListOf<String>() - + private fun expandAtFiles(args: Sequence<String>, out: MutableList<String>) { args.forEach { arg -> if (arg.startsWith("@@")) { - ret += arg.substring(1) + out.add(arg.substring(1)) return@forEach } else if (!arg.startsWith('@')) { - ret += arg + out.add(arg) return@forEach } + // Read from the file, and add each line to the result. - val filename = arg.substring(1).ensureFileExists() + val file = FileOrResource(arg.substring(1)) - log.v("Expanding options file $filename") + log.v("Expanding options file ${file.path}") - BufferedReader(FileReader(filename)).use { reader -> - while (true) { - var line = reader.readLine() ?: break // EOF + val fileArgs = file + .open() + .buffered() + .lineSequence() + .map(::normalizeTextLine) + .filter(CharSequence::isNotEmpty) - line = normalizeTextLine(line) - if (line.isNotEmpty()) { - ret += line - } - } - } + expandAtFiles(fileArgs, out) } - return ret } } } @@ -204,3 +207,37 @@ class IntSetOnce(value: Int) : SetOnce<Int>(value) { } } } + +/** + * A path either points to a file in filesystem, or an entry in the JAR. + */ +class FileOrResource(val path: String) { + init { + path.ensureFileExists() + } + + /** + * Either read from filesystem, or read from JAR resources. + */ + fun open(): Reader { + return if (path.startsWith(JAR_RESOURCE_PREFIX)) { + val path = path.removePrefix(JAR_RESOURCE_PREFIX) + InputStreamReader(this::class.java.classLoader.getResourceAsStream(path)!!) + } else { + FileReader(path) + } + } +} + +fun String.ensureFileExists(): String { + if (this.startsWith(JAR_RESOURCE_PREFIX)) { + val cl = FileOrResource::class.java.classLoader + val path = this.removePrefix(JAR_RESOURCE_PREFIX) + if (cl.getResource(path) == null) { + throw JarResourceNotFoundException(path) + } + } else if (!File(this).exists()) { + throw InputFileNotFoundException(this) + } + return this +} diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt index a08d1d605949..769b769d7a20 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt @@ -17,7 +17,6 @@ package com.android.hoststubgen.visitors import com.android.hoststubgen.HostStubGenErrors import com.android.hoststubgen.HostStubGenStats -import com.android.hoststubgen.LogLevel import com.android.hoststubgen.asm.ClassNodes import com.android.hoststubgen.asm.UnifiedVisitor import com.android.hoststubgen.asm.getPackageNameFromFullClassName @@ -26,13 +25,10 @@ import com.android.hoststubgen.filters.FilterPolicyWithReason import com.android.hoststubgen.filters.OutputFilter import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep import com.android.hoststubgen.log -import java.io.PrintWriter import org.objectweb.asm.ClassVisitor import org.objectweb.asm.FieldVisitor import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes -import org.objectweb.asm.commons.ClassRemapper -import org.objectweb.asm.util.TraceClassVisitor const val OPCODE_VERSION = Opcodes.ASM9 @@ -49,8 +45,6 @@ abstract class BaseAdapter( data class Options( val errors: HostStubGenErrors, val stats: HostStubGenStats?, - val enablePreTrace: Boolean, - val enablePostTrace: Boolean, val deleteClassFinals: Boolean, val deleteMethodFinals: Boolean, // We don't remove finals from fields, because final fields have a stronger memory @@ -253,50 +247,4 @@ abstract class BaseAdapter( substituted: Boolean, superVisitor: MethodVisitor?, ): MethodVisitor? - - companion object { - fun getVisitor( - classInternalName: String, - classes: ClassNodes, - nextVisitor: ClassVisitor, - filter: OutputFilter, - packageRedirector: PackageRedirectRemapper, - options: Options, - ): ClassVisitor { - var next = nextVisitor - - val verbosePrinter = PrintWriter(log.getWriter(LogLevel.Verbose)) - - // Inject TraceClassVisitor for debugging. - if (options.enablePostTrace) { - next = TraceClassVisitor(next, verbosePrinter) - } - - // Handle --package-redirect - if (!packageRedirector.isEmpty) { - // Don't apply the remapper on redirect-from classes. - // Otherwise, if the target jar actually contains the "from" classes (which - // may or may not be the case) they'd be renamed. - // But we update all references in other places, so, a method call to a "from" class - // would be replaced with the "to" class. All type references (e.g. variable types) - // will be updated too. - if (!packageRedirector.isTarget(classInternalName)) { - next = ClassRemapper(next, packageRedirector) - } else { - log.v( - "Class $classInternalName is a redirect-from class, not applying" + - " --package-redirect" - ) - } - } - - next = ImplGeneratingAdapter(classes, next, filter, options) - - // Inject TraceClassVisitor for debugging. - if (options.enablePreTrace) { - next = TraceClassVisitor(next, verbosePrinter) - } - return next - } - } } diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt index b8a357668c2b..617385ad438e 100644 --- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt +++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt @@ -396,7 +396,7 @@ class ImplGeneratingAdapter( } val to = filter.getMethodCallReplaceTo( - currentClassName, callerMethodName, owner, name, descriptor + owner, name, descriptor ) if (to == null diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt index 8bb454fa12e7..d9cc54aebf51 100644 --- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt +++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt @@ -18,6 +18,7 @@ package com.android.hoststubgen import com.android.hoststubgen.utils.ArgIterator import com.android.hoststubgen.utils.IntSetOnce import com.android.hoststubgen.utils.SetOnce +import com.android.hoststubgen.utils.ensureFileExists /** * Options that can be set from command line arguments. @@ -61,9 +62,9 @@ class HostStubGenOptions( } } - override fun parseOption(option: String, ai: ArgIterator): Boolean { + override fun parseOption(option: String, args: ArgIterator): Boolean { // Define some shorthands... - fun nextArg(): String = ai.nextArgRequired(option) + fun nextArg(): String = args.nextArgRequired(option) when (option) { // TODO: Write help @@ -94,7 +95,7 @@ class HostStubGenOptions( } } - else -> return super.parseOption(option, ai) + else -> return super.parseOption(option, args) } return true diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt index f7fd0804c151..58bd9e987fd1 100644 --- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt +++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt @@ -16,10 +16,10 @@ package com.android.platform.test.ravenwood.ravenhelper.policytoannot import com.android.hoststubgen.ArgumentsException -import com.android.hoststubgen.ensureFileExists import com.android.hoststubgen.utils.ArgIterator import com.android.hoststubgen.utils.BaseOptions import com.android.hoststubgen.utils.SetOnce +import com.android.hoststubgen.utils.ensureFileExists /** * Options for the "ravenhelper pta" subcommand. @@ -41,8 +41,8 @@ class PtaOptions( var dumpOperations: SetOnce<Boolean> = SetOnce(false), ) : BaseOptions() { - override fun parseOption(option: String, ai: ArgIterator): Boolean { - fun nextArg(): String = ai.nextArgRequired(option) + override fun parseOption(option: String, args: ArgIterator): Boolean { + fun nextArg(): String = args.nextArgRequired(option) when (option) { // TODO: Write help diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt index fd6f732a06ce..5ce9a23e6e05 100644 --- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt +++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt @@ -19,10 +19,10 @@ import com.android.hoststubgen.LogLevel import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME import com.android.hoststubgen.asm.toJvmClassName import com.android.hoststubgen.filters.FilterPolicyWithReason +import com.android.hoststubgen.filters.MethodCallReplaceSpec import com.android.hoststubgen.filters.PolicyFileProcessor import com.android.hoststubgen.filters.SpecialClass import com.android.hoststubgen.filters.TextFileFilterPolicyParser -import com.android.hoststubgen.filters.TextFilePolicyMethodReplaceFilter import com.android.hoststubgen.log import com.android.hoststubgen.utils.ClassPredicate import com.android.platform.test.ravenwood.ravenhelper.SubcommandHandler @@ -448,7 +448,7 @@ private class TextPolicyToAnnotationConverter( className: String, methodName: String, methodDesc: String, - replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec, + replaceSpec: MethodCallReplaceSpec, ) { // This can't be converted to an annotation. classHasMember = true diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt index 8b95843f08a6..6e0b7b89cf13 100644 --- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt +++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt @@ -16,10 +16,10 @@ package com.android.platform.test.ravenwood.ravenhelper.sourcemap import com.android.hoststubgen.ArgumentsException -import com.android.hoststubgen.ensureFileExists import com.android.hoststubgen.utils.ArgIterator import com.android.hoststubgen.utils.BaseOptions import com.android.hoststubgen.utils.SetOnce +import com.android.hoststubgen.utils.ensureFileExists /** * Options for the "ravenhelper map" subcommand. @@ -38,8 +38,8 @@ class MapOptions( var text: SetOnce<String?> = SetOnce(null), ) : BaseOptions() { - override fun parseOption(option: String, ai: ArgIterator): Boolean { - fun nextArg(): String = ai.nextArgRequired(option) + override fun parseOption(option: String, args: ArgIterator): Boolean { + fun nextArg(): String = args.nextArgRequired(option) when (option) { // TODO: Write help diff --git a/ravenwood/tools/ravenizer/Android.bp b/ravenwood/tools/ravenizer/Android.bp index 957e20647d44..93cda4e3c4c9 100644 --- a/ravenwood/tools/ravenizer/Android.bp +++ b/ravenwood/tools/ravenizer/Android.bp @@ -15,5 +15,10 @@ java_binary_host { "hoststubgen-lib", "ravenwood-junit-for-ravenizer", ], + java_resources: [ + ":ravenizer-standard-options", + ":ravenwood-standard-annotations", + ":ravenwood-common-policies", + ], visibility: ["//visibility:public"], } diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt index e67c730df069..04e3bda2ba27 100644 --- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt @@ -16,23 +16,22 @@ package com.android.platform.test.ravenwood.ravenizer import com.android.hoststubgen.GeneralUserErrorException +import com.android.hoststubgen.HostStubGenClassProcessor import com.android.hoststubgen.asm.ClassNodes import com.android.hoststubgen.asm.zipEntryNameToClassName import com.android.hoststubgen.executableName import com.android.hoststubgen.log import com.android.platform.test.ravenwood.ravenizer.adapter.RunnerRewritingAdapter -import org.objectweb.asm.ClassReader -import org.objectweb.asm.ClassVisitor -import org.objectweb.asm.ClassWriter -import org.objectweb.asm.util.CheckClassAdapter import java.io.BufferedInputStream import java.io.BufferedOutputStream import java.io.FileOutputStream -import java.io.InputStream -import java.io.OutputStream import java.util.zip.ZipEntry import java.util.zip.ZipFile import java.util.zip.ZipOutputStream +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.util.CheckClassAdapter /** * Various stats on Ravenizer. @@ -41,7 +40,7 @@ data class RavenizerStats( /** Total end-to-end time. */ var totalTime: Double = .0, - /** Time took to build [ClasNodes] */ + /** Time took to build [ClassNodes] */ var loadStructureTime: Double = .0, /** Time took to validate the classes */ @@ -50,14 +49,17 @@ data class RavenizerStats( /** Total real time spent for converting the jar file */ var totalProcessTime: Double = .0, - /** Total real time spent for converting class files (except for I/O time). */ - var totalConversionTime: Double = .0, + /** Total real time spent for ravenizing class files (excluding I/O time). */ + var totalRavenizeTime: Double = .0, + + /** Total real time spent for processing class files HSG style (excluding I/O time). */ + var totalHostStubGenTime: Double = .0, /** Total real time spent for copying class files without modification. */ var totalCopyTime: Double = .0, /** # of entries in the input jar file */ - var totalEntiries: Int = 0, + var totalEntries: Int = 0, /** # of *.class files in the input jar file */ var totalClasses: Int = 0, @@ -67,14 +69,15 @@ data class RavenizerStats( ) { override fun toString(): String { return """ - RavenizerStats{ + RavenizerStats { totalTime=$totalTime, loadStructureTime=$loadStructureTime, validationTime=$validationTime, totalProcessTime=$totalProcessTime, - totalConversionTime=$totalConversionTime, + totalRavenizeTime=$totalRavenizeTime, + totalHostStubGenTime=$totalHostStubGenTime, totalCopyTime=$totalCopyTime, - totalEntiries=$totalEntiries, + totalEntries=$totalEntries, totalClasses=$totalClasses, processedClasses=$processedClasses, } @@ -90,12 +93,18 @@ class Ravenizer { val stats = RavenizerStats() stats.totalTime = log.nTime { + val allClasses = ClassNodes.loadClassStructures(options.inJar.get) { + stats.loadStructureTime = it + } + val processor = HostStubGenClassProcessor(options, allClasses) + process( options.inJar.get, options.outJar.get, options.enableValidation.get, options.fatalValidation.get, options.stripMockito.get, + processor, stats, ) } @@ -108,15 +117,13 @@ class Ravenizer { enableValidation: Boolean, fatalValidation: Boolean, stripMockito: Boolean, + processor: HostStubGenClassProcessor, stats: RavenizerStats, ) { - var allClasses = ClassNodes.loadClassStructures(inJar) { - time -> stats.loadStructureTime = time - } if (enableValidation) { stats.validationTime = log.iTime("Validating classes") { - if (!validateClasses(allClasses)) { - var message = "Invalid test class(es) detected." + + if (!validateClasses(processor.allClasses)) { + val message = "Invalid test class(es) detected." + " See error log for details." if (fatalValidation) { throw RavenizerInvalidTestException(message) @@ -126,7 +133,7 @@ class Ravenizer { } } } - if (includeUnsupportedMockito(allClasses)) { + if (includeUnsupportedMockito(processor.allClasses)) { log.w("Unsupported Mockito detected in $inJar!") } @@ -134,7 +141,7 @@ class Ravenizer { ZipFile(inJar).use { inZip -> val inEntries = inZip.entries() - stats.totalEntiries = inZip.size() + stats.totalEntries = inZip.size() ZipOutputStream(BufferedOutputStream(FileOutputStream(outJar))).use { outZip -> while (inEntries.hasMoreElements()) { @@ -159,9 +166,9 @@ class Ravenizer { stats.totalClasses += 1 } - if (className != null && shouldProcessClass(allClasses, className)) { - stats.processedClasses += 1 - processSingleClass(inZip, entry, outZip, allClasses, stats) + if (className != null && + shouldProcessClass(processor.allClasses, className)) { + processSingleClass(inZip, entry, outZip, processor, stats) } else { // Too slow, let's use merge_zips to bring back the original classes. copyZipEntry(inZip, entry, outZip, stats) @@ -201,14 +208,22 @@ class Ravenizer { inZip: ZipFile, entry: ZipEntry, outZip: ZipOutputStream, - allClasses: ClassNodes, + processor: HostStubGenClassProcessor, stats: RavenizerStats, ) { + stats.processedClasses += 1 val newEntry = ZipEntry(entry.name) outZip.putNextEntry(newEntry) BufferedInputStream(inZip.getInputStream(entry)).use { bis -> - processSingleClass(entry, bis, outZip, allClasses, stats) + var classBytes = bis.readBytes() + stats.totalRavenizeTime += log.vTime("Ravenize ${entry.name}") { + classBytes = ravenizeSingleClass(entry, classBytes, processor.allClasses) + } + stats.totalHostStubGenTime += log.vTime("HostStubGen ${entry.name}") { + classBytes = processor.processClassBytecode(classBytes) + } + outZip.write(classBytes) } outZip.closeEntry() } @@ -217,41 +232,34 @@ class Ravenizer { * Whether a class needs to be processed. This must be kept in sync with [processSingleClass]. */ private fun shouldProcessClass(classes: ClassNodes, classInternalName: String): Boolean { - return !classInternalName.shouldByBypassed() + return !classInternalName.shouldBypass() && RunnerRewritingAdapter.shouldProcess(classes, classInternalName) } - private fun processSingleClass( + private fun ravenizeSingleClass( entry: ZipEntry, - input: InputStream, - output: OutputStream, + input: ByteArray, allClasses: ClassNodes, - stats: RavenizerStats, - ) { - val cr = ClassReader(input) - - lateinit var data: ByteArray - stats.totalConversionTime += log.vTime("Modify ${entry.name}") { + ): ByteArray { + val classInternalName = zipEntryNameToClassName(entry.name) + ?: throw RavenizerInternalException("Unexpected zip entry name: ${entry.name}") - val classInternalName = zipEntryNameToClassName(entry.name) - ?: throw RavenizerInternalException("Unexpected zip entry name: ${entry.name}") - val flags = ClassWriter.COMPUTE_MAXS - val cw = ClassWriter(flags) - var outVisitor: ClassVisitor = cw + val flags = ClassWriter.COMPUTE_MAXS + val cw = ClassWriter(flags) + var outVisitor: ClassVisitor = cw - val enableChecker = false - if (enableChecker) { - outVisitor = CheckClassAdapter(outVisitor) - } + val enableChecker = false + if (enableChecker) { + outVisitor = CheckClassAdapter(outVisitor) + } - // This must be kept in sync with shouldProcessClass. - outVisitor = RunnerRewritingAdapter.maybeApply( - classInternalName, allClasses, outVisitor) + // This must be kept in sync with shouldProcessClass. + outVisitor = RunnerRewritingAdapter.maybeApply( + classInternalName, allClasses, outVisitor) - cr.accept(outVisitor, ClassReader.EXPAND_FRAMES) + val cr = ClassReader(input) + cr.accept(outVisitor, ClassReader.EXPAND_FRAMES) - data = cw.toByteArray() - } - output.write(data) + return cw.toByteArray() } } diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt index 7f4829ec6127..8a09e6d533b8 100644 --- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt @@ -21,6 +21,7 @@ import com.android.hoststubgen.LogLevel import com.android.hoststubgen.executableName import com.android.hoststubgen.log import com.android.hoststubgen.runMainWithBoilerplate +import com.android.hoststubgen.utils.JAR_RESOURCE_PREFIX import java.nio.file.Paths import kotlin.io.path.exists @@ -36,6 +37,10 @@ import kotlin.io.path.exists */ private val RAVENIZER_DOTFILE = System.getenv("HOME") + "/.ravenizer-unsafe" +/** + * This is the name of the standard option text file embedded inside ravenizer.jar. + */ +private const val RAVENIZER_STANDARD_OPTIONS = "texts/ravenizer-standard-options.txt" /** * Entry point. @@ -45,12 +50,12 @@ fun main(args: Array<String>) { log.setConsoleLogLevel(LogLevel.Info) runMainWithBoilerplate { - var newArgs = args.asList() + val newArgs = args.toMutableList() + newArgs.add(0, "@$JAR_RESOURCE_PREFIX$RAVENIZER_STANDARD_OPTIONS") + if (Paths.get(RAVENIZER_DOTFILE).exists()) { log.i("Reading options from $RAVENIZER_DOTFILE") - newArgs = args.toMutableList().apply { - add(0, "@$RAVENIZER_DOTFILE") - } + newArgs.add(0, "@$RAVENIZER_DOTFILE") } val options = RavenizerOptions().apply { parseArgs(newArgs) } diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt index 2c0365404ab6..5d278bb046ae 100644 --- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt @@ -16,10 +16,10 @@ package com.android.platform.test.ravenwood.ravenizer import com.android.hoststubgen.ArgumentsException -import com.android.hoststubgen.ensureFileExists +import com.android.hoststubgen.HostStubGenClassProcessorOptions import com.android.hoststubgen.utils.ArgIterator -import com.android.hoststubgen.utils.BaseOptions import com.android.hoststubgen.utils.SetOnce +import com.android.hoststubgen.utils.ensureFileExists class RavenizerOptions( /** Input jar file*/ @@ -36,10 +36,10 @@ class RavenizerOptions( /** Whether to remove mockito and dexmaker classes. */ var stripMockito: SetOnce<Boolean> = SetOnce(false), -) : BaseOptions() { +) : HostStubGenClassProcessorOptions() { - override fun parseOption(option: String, ai: ArgIterator): Boolean { - fun nextArg(): String = ai.nextArgRequired(option) + override fun parseOption(option: String, args: ArgIterator): Boolean { + fun nextArg(): String = args.nextArgRequired(option) when (option) { // TODO: Write help @@ -57,7 +57,7 @@ class RavenizerOptions( "--strip-mockito" -> stripMockito.set(true) "--no-strip-mockito" -> stripMockito.set(false) - else -> return false + else -> return super.parseOption(option, args) } return true @@ -79,6 +79,6 @@ class RavenizerOptions( enableValidation=$enableValidation, fatalValidation=$fatalValidation, stripMockito=$stripMockito, - """.trimIndent() + """.trimIndent() + '\n' + super.dumpFields() } } diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt index 6092fcc9402d..b394a761c7ae 100644 --- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt @@ -15,8 +15,8 @@ */ package com.android.platform.test.ravenwood.ravenizer -import android.platform.test.annotations.internal.InnerRunner import android.platform.test.annotations.NoRavenizer +import android.platform.test.annotations.internal.InnerRunner import android.platform.test.ravenwood.RavenwoodAwareTestRunner import com.android.hoststubgen.asm.ClassNodes import com.android.hoststubgen.asm.findAnyAnnotation @@ -85,7 +85,7 @@ fun String.isRavenwoodClass(): Boolean { /** * Classes that should never be modified. */ -fun String.shouldByBypassed(): Boolean { +fun String.shouldBypass(): Boolean { if (this.isRavenwoodClass()) { return true } diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt index 61e254b225c3..d252b4dc8ab6 100644 --- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt @@ -20,8 +20,8 @@ import com.android.hoststubgen.asm.isAbstract import com.android.hoststubgen.asm.startsWithAny import com.android.hoststubgen.asm.toHumanReadableClassName import com.android.hoststubgen.log -import org.objectweb.asm.tree.ClassNode import java.util.regex.Pattern +import org.objectweb.asm.tree.ClassNode fun validateClasses(classes: ClassNodes): Boolean { var allOk = true @@ -37,7 +37,7 @@ fun validateClasses(classes: ClassNodes): Boolean { * */ fun checkClass(cn: ClassNode, classes: ClassNodes): Boolean { - if (cn.name.shouldByBypassed()) { + if (cn.name.shouldBypass()) { // Class doesn't need to be checked. return true } @@ -145,4 +145,4 @@ com.android.server.power.stats.BatteryStatsTimerTest private fun isAllowListedLegacyTest(targetClass: ClassNode): Boolean { return allowListedLegacyTests.contains(targetClass.name) -}
\ No newline at end of file +} diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index b52b3dabd47d..64a4a4ae3c0d 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -260,6 +260,16 @@ flag { } flag { + name: "pointer_up_motion_event_in_touch_exploration" + namespace: "accessibility" + description: "Allows POINTER_UP motionEvents to trigger during touch exploration." + bug: "374930391" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "proxy_use_apps_on_virtual_device_listener" namespace: "accessibility" description: "Fixes race condition described in b/286587811" diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index c49151dd5e30..573c591cb504 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -521,15 +521,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub @Nullable IBinder focusedToken) { return AccessibilityManagerService.this.handleKeyGestureEvent(event); } - - @Override - public boolean isKeyGestureSupported(int gestureType) { - return switch (gestureType) { - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION, - KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK -> true; - default -> false; - }; - } }; @VisibleForTesting diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java index 0f6f86b39458..cc93d0887d89 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -25,6 +25,7 @@ import static android.view.accessibility.AccessibilityManager.AUTOCLICK_IGNORE_M import static com.android.server.accessibility.autoclick.AutoclickIndicatorView.SHOW_INDICATOR_DELAY_TIME; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK; +import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AutoclickType; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.ClickPanelControllerInterface; @@ -87,6 +88,7 @@ public class AutoclickController extends BaseEventStreamTransformation { @VisibleForTesting AutoclickIndicatorScheduler mAutoclickIndicatorScheduler; @VisibleForTesting AutoclickIndicatorView mAutoclickIndicatorView; @VisibleForTesting AutoclickTypePanel mAutoclickTypePanel; + @VisibleForTesting AutoclickScrollPanel mAutoclickScrollPanel; private WindowManager mWindowManager; // Default click type is left-click. @@ -98,6 +100,11 @@ public class AutoclickController extends BaseEventStreamTransformation { @Override public void handleAutoclickTypeChange(@AutoclickType int clickType) { mActiveClickType = clickType; + + // Hide scroll panel when type is not scroll. + if (clickType != AUTOCLICK_TYPE_SCROLL && mAutoclickScrollPanel != null) { + mAutoclickScrollPanel.hide(); + } } @Override @@ -161,6 +168,7 @@ public class AutoclickController extends BaseEventStreamTransformation { mWindowManager = mContext.getSystemService(WindowManager.class); mAutoclickTypePanel = new AutoclickTypePanel(mContext, mWindowManager, mUserId, clickPanelController); + mAutoclickScrollPanel = new AutoclickScrollPanel(mContext, mWindowManager); mAutoclickTypePanel.show(); mWindowManager.addView(mAutoclickIndicatorView, mAutoclickIndicatorView.getLayoutParams()); @@ -189,6 +197,10 @@ public class AutoclickController extends BaseEventStreamTransformation { mClickScheduler.cancel(); } + if (mAutoclickScrollPanel != null) { + mAutoclickScrollPanel.hide(); + } + super.clearEvents(inputSource); } @@ -209,6 +221,11 @@ public class AutoclickController extends BaseEventStreamTransformation { mWindowManager.removeView(mAutoclickIndicatorView); mAutoclickTypePanel.hide(); } + + if (mAutoclickScrollPanel != null) { + mAutoclickScrollPanel.hide(); + mAutoclickScrollPanel = null; + } } private void handleMouseMotion(MotionEvent event, int policyFlags) { @@ -231,7 +248,11 @@ public class AutoclickController extends BaseEventStreamTransformation { private boolean isPaused() { return Flags.enableAutoclickIndicator() && mAutoclickTypePanel.isPaused() - && !mAutoclickTypePanel.isHovered(); + && !isHovered(); + } + + private boolean isHovered() { + return Flags.enableAutoclickIndicator() && mAutoclickTypePanel.isHovered(); } private void cancelPendingClick() { @@ -478,6 +499,8 @@ public class AutoclickController extends BaseEventStreamTransformation { private int mEventPolicyFlags; /** Current meta state. This value will be used as meta state for click event sequence. */ private int mMetaState; + /** Last observed panel hovered state when click was scheduled. */ + private boolean mHoveredState; /** * The current anchor's coordinates. Should be ignored if #mLastMotionEvent is null. @@ -631,6 +654,7 @@ public class AutoclickController extends BaseEventStreamTransformation { } mLastMotionEvent = MotionEvent.obtain(event); mEventPolicyFlags = policyFlags; + mHoveredState = isHovered(); if (useAsAnchor) { final int pointerIndex = mLastMotionEvent.getActionIndex(); @@ -687,6 +711,14 @@ public class AutoclickController extends BaseEventStreamTransformation { return; } + // Handle scroll type specially, show scroll panel instead of sending click events. + if (mActiveClickType == AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL) { + if (mAutoclickScrollPanel != null) { + mAutoclickScrollPanel.show(); + } + return; + } + final int pointerIndex = mLastMotionEvent.getActionIndex(); if (mTempPointerProperties == null) { @@ -704,14 +736,18 @@ public class AutoclickController extends BaseEventStreamTransformation { final long now = SystemClock.uptimeMillis(); - // TODO(b/395094903): always triggers left-click when the cursor hovers over the - // autoclick type panel, to always allow users to change a different click type. - // Otherwise, if one chooses the right-click, this user won't be able to rely on - // autoclick to select other click types. - final int actionButton = - mActiveClickType == AUTOCLICK_TYPE_RIGHT_CLICK - ? BUTTON_SECONDARY - : BUTTON_PRIMARY; + int actionButton; + if (mHoveredState) { + // Always triggers left-click when the cursor hovers over the autoclick type + // panel, to always allow users to change a different click type. Otherwise, if + // one chooses the right-click, this user won't be able to rely on autoclick to + // select other click types. + actionButton = BUTTON_PRIMARY; + } else { + actionButton = mActiveClickType == AUTOCLICK_TYPE_RIGHT_CLICK + ? BUTTON_SECONDARY + : BUTTON_PRIMARY; + } MotionEvent downEvent = MotionEvent.obtain( diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java new file mode 100644 index 000000000000..86f79a83ea28 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java @@ -0,0 +1,105 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.accessibility.autoclick; + +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + +import android.content.Context; +import android.graphics.PixelFormat; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowInsets; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.android.internal.R; + +public class AutoclickScrollPanel { + private final Context mContext; + private final View mContentView; + private final WindowManager mWindowManager; + private boolean mInScrollMode = false; + + public AutoclickScrollPanel(Context context, WindowManager windowManager) { + mContext = context; + mWindowManager = windowManager; + mContentView = LayoutInflater.from(context).inflate( + R.layout.accessibility_autoclick_scroll_panel, null); + } + + /** + * Shows the autoclick scroll panel. + */ + public void show() { + if (mInScrollMode) { + return; + } + mWindowManager.addView(mContentView, getLayoutParams()); + mInScrollMode = true; + } + + /** + * Hides the autoclick scroll panel. + */ + public void hide() { + if (!mInScrollMode) { + return; + } + mWindowManager.removeView(mContentView); + mInScrollMode = false; + } + + /** + * Retrieves the layout params for AutoclickScrollPanel, used when it's added to the Window + * Manager. + */ + @NonNull + private WindowManager.LayoutParams getLayoutParams() { + final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); + layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; + layoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + layoutParams.setFitInsetsTypes(WindowInsets.Type.statusBars()); + layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + layoutParams.format = PixelFormat.TRANSLUCENT; + layoutParams.setTitle(AutoclickScrollPanel.class.getSimpleName()); + layoutParams.accessibilityTitle = + mContext.getString(R.string.accessibility_autoclick_scroll_panel_title); + layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT; + layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; + layoutParams.gravity = Gravity.CENTER; + return layoutParams; + } + + @VisibleForTesting + public boolean isVisible() { + return mInScrollMode; + } + + @VisibleForTesting + public View getContentViewForTesting() { + return mContentView; + } + + @VisibleForTesting + public WindowManager.LayoutParams getLayoutParamsForTesting() { + return getLayoutParams(); + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java index fb329430acb2..b02fe2752a62 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java @@ -653,6 +653,14 @@ public class TouchExplorer extends BaseEventStreamTransformation case ACTION_UP: handleActionUp(event, rawEvent, policyFlags); break; + case ACTION_POINTER_UP: + if (com.android.server.accessibility.Flags + .pointerUpMotionEventInTouchExploration()) { + if (mState.isServiceDetectingGestures()) { + mAms.sendMotionEventToListeningServices(rawEvent); + } + } + break; default: break; } diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java index cd46b38272c2..568abd196735 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java @@ -26,6 +26,8 @@ import android.view.Display; import android.view.MotionEvent; import android.view.accessibility.AccessibilityEvent; +import androidx.annotation.VisibleForTesting; + import com.android.server.accessibility.AccessibilityManagerService; /** @@ -73,7 +75,8 @@ public class TouchState { private int mState = STATE_CLEAR; // Helper class to track received pointers. // Todo: collapse or hide this class so multiple classes don't modify it. - private final ReceivedPointerTracker mReceivedPointerTracker; + @VisibleForTesting + public final ReceivedPointerTracker mReceivedPointerTracker; // The most recently received motion event. private MotionEvent mLastReceivedEvent; // The accompanying raw event without any transformations. @@ -219,8 +222,19 @@ public class TouchState { startTouchInteracting(); break; case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END: - setState(STATE_CLEAR); - // We will clear when we actually handle the next ACTION_DOWN. + // When interaction ends, check if there are still down pointers. + // If there are any down pointers, go directly to TouchExploring instead. + if (com.android.server.accessibility.Flags + .pointerUpMotionEventInTouchExploration()) { + if (mReceivedPointerTracker.mReceivedPointersDown > 0) { + startTouchExploring(); + } else { + setState(STATE_CLEAR); + // We will clear when we actually handle the next ACTION_DOWN. + } + } else { + setState(STATE_CLEAR); + } break; case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START: startTouchExploring(); @@ -419,7 +433,8 @@ public class TouchState { private final PointerDownInfo[] mReceivedPointers = new PointerDownInfo[MAX_POINTER_COUNT]; // Which pointers are down. - private int mReceivedPointersDown; + @VisibleForTesting + public int mReceivedPointersDown; // The edge flags of the last received down event. private int mLastReceivedDownEdgeFlags; diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java index 6ccf5e47ca6c..59566677b1fc 100644 --- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java +++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java @@ -39,6 +39,14 @@ import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_ import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__INLINE; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__MENU; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__UNKNOWN_AUTOFILL_DISPLAY_PRESENTATION_TYPE; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_DELAY_AFTER_ANIMATION_END; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_FILL_DIALOG_DISABLED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_LAST_TRIGGERED_ID_CHANGED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_SCREEN_HAS_CREDMAN_FIELD; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_AFTER_DELAY; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_SINCE_IME_ANIMATED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_UNKNOWN; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_WAIT_FOR_IME_ANIMATION; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__ANY_SHOWN; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_ACTIVITY_FINISHED; import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_FILL_REQUEST_FAILED; @@ -157,8 +165,24 @@ public final class PresentationStatsEventLogger { DETECTION_PREFER_PCC }) @Retention(RetentionPolicy.SOURCE) - public @interface DetectionPreference { - } + public @interface DetectionPreference {} + + /** + * The fill dialog not shown reason. These are wrappers around + * {@link com.android.os.AtomsProto.AutofillPresentationEventReported.FillDialogNotShownReason}. + */ + @IntDef(prefix = {"FILL_DIALOG_NOT_SHOWN_REASON"}, value = { + FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN, + FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED, + FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD, + FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED, + FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION, + FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED, + FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END, + FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FillDialogNotShownReason {} public static final int NOT_SHOWN_REASON_ANY_SHOWN = AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__ANY_SHOWN; @@ -219,6 +243,25 @@ public final class PresentationStatsEventLogger { public static final int DETECTION_PREFER_PCC = AUTOFILL_FILL_RESPONSE_REPORTED__DETECTION_PREFERENCE__DETECTION_PREFER_PCC; + // Values for AutofillFillResponseReported.fill_dialog_not_shown_reason + public static final int FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN = + AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_UNKNOWN; + public static final int FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED = + AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_FILL_DIALOG_DISABLED; + public static final int FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD = + AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_SCREEN_HAS_CREDMAN_FIELD; + public static final int FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED = + AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_LAST_TRIGGERED_ID_CHANGED; + public static final int FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION = + AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_WAIT_FOR_IME_ANIMATION; + public static final int FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED = + AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_SINCE_IME_ANIMATED; + public static final int FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END = + AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_DELAY_AFTER_ANIMATION_END; + public static final int FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY = + AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_AFTER_DELAY; + + private static final int DEFAULT_VALUE_INT = -1; private final int mSessionId; @@ -871,6 +914,43 @@ public final class PresentationStatsEventLogger { } /** + * Set fill_dialog_not_shown_reason + * @param reason + */ + public void maybeSetFillDialogNotShownReason(@FillDialogNotShownReason int reason) { + mEventInternal.ifPresent(event -> { + if ((event.mFillDialogNotShownReason + == FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END + || event.mFillDialogNotShownReason + == FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION) && reason + == FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED) { + event.mFillDialogNotShownReason = FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY; + } else { + event.mFillDialogNotShownReason = reason; + } + }); + } + + /** + * Set fill_dialog_ready_to_show_ms + * @param val + */ + public void maybeSetFillDialogReadyToShowMs(long val) { + mEventInternal.ifPresent(event -> { + event.mFillDialogReadyToShowMs = (int) (val - mSessionStartTimestamp); + }); + } + + /** + * Set ime_animation_finish_ms + * @param val + */ + public void maybeSetImeAnimationFinishMs(long val) { + mEventInternal.ifPresent(event -> { + event.mImeAnimationFinishMs = (int) (val - mSessionStartTimestamp); + }); + } + /** * Set the log contains relayout metrics. * This is being added as a temporary measure to add logging. * In future, when we map Session's old view states to the new autofill id's as part of fixing @@ -959,7 +1039,13 @@ public final class PresentationStatsEventLogger { + " event.notExpiringResponseDuringAuthCount=" + event.mFixExpireResponseDuringAuthCount + " event.notifyViewEnteredIgnoredDuringAuthCount=" - + event.mNotifyViewEnteredIgnoredDuringAuthCount); + + event.mNotifyViewEnteredIgnoredDuringAuthCount + + " event.fillDialogNotShownReason=" + + event.mFillDialogNotShownReason + + " event.fillDialogReadyToShowMs=" + + event.mFillDialogReadyToShowMs + + " event.imeAnimationFinishMs=" + + event.mImeAnimationFinishMs); } // TODO(b/234185326): Distinguish empty responses from other no presentation reasons. @@ -1020,7 +1106,10 @@ public final class PresentationStatsEventLogger { event.mViewFilledSuccessfullyOnRefillCount, event.mViewFailedOnRefillCount, event.mFixExpireResponseDuringAuthCount, - event.mNotifyViewEnteredIgnoredDuringAuthCount); + event.mNotifyViewEnteredIgnoredDuringAuthCount, + event.mFillDialogNotShownReason, + event.mFillDialogReadyToShowMs, + event.mImeAnimationFinishMs); mEventInternal = Optional.empty(); } @@ -1087,6 +1176,9 @@ public final class PresentationStatsEventLogger { // Following are not logged and used only for internal logic boolean shouldResetShownCount = false; boolean mHasRelayoutLog = false; + @FillDialogNotShownReason int mFillDialogNotShownReason = FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN; + int mFillDialogReadyToShowMs = DEFAULT_VALUE_INT; + int mImeAnimationFinishMs = DEFAULT_VALUE_INT; PresentationStatsEventInternal() {} } diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 6fdb2b6b83f7..ff3bf2acb080 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -64,6 +64,7 @@ import static com.android.server.autofill.FillResponseEventLogger.DETECTION_PREF import static com.android.server.autofill.FillResponseEventLogger.DETECTION_PREFER_PCC; import static com.android.server.autofill.FillResponseEventLogger.DETECTION_PREFER_UNKNOWN; import static com.android.server.autofill.FillResponseEventLogger.HAVE_SAVE_TRIGGER_ID; +import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_CANCELLED; import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_FAILURE; import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_SESSION_DESTROYED; import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_SUCCESS; @@ -80,6 +81,13 @@ import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTIC import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_RESULT_SUCCESS; import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_TYPE_DATASET_AUTHENTICATION; import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_TYPE_FULL_AUTHENTICATION; +import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END; +import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED; +import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED; +import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD; +import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED; +import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN; +import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION; import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_ANY_SHOWN; import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_NO_FOCUS; import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_REQUEST_FAILED; @@ -1416,6 +1424,15 @@ final class Session // Remove the FillContext as there will never be a response for the service if (canceledRequest != INVALID_REQUEST_ID && mContexts != null) { + // Start a new FillResponse logger for the cancellation case. + mFillResponseEventLogger.startLogForNewResponse(); + mFillResponseEventLogger.maybeSetRequestId(canceledRequest); + mFillResponseEventLogger.maybeSetAppPackageUid(uid); + mFillResponseEventLogger.maybeSetResponseStatus(RESPONSE_STATUS_CANCELLED); + mFillResponseEventLogger.maybeSetLatencyFillResponseReceivedMillis( + (int) (SystemClock.elapsedRealtime() - mLatencyBaseTime)); + mFillResponseEventLogger.logAndEndEvent(); + final int numContexts = mContexts.size(); // It is most likely the last context, hence search backwards @@ -5612,6 +5629,10 @@ final class Session synchronized (mLock) { final ViewState currentView = mViewStates.get(mCurrentViewId); currentView.setState(ViewState.STATE_FILL_DIALOG_SHOWN); + // Set fill_dialog_not_shown_reason to unknown (a.k.a shown). It is needed due + // to possible SHOW_FILL_DIALOG_WAIT. + mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason( + FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN); } // Just show fill dialog once per fill request, so disabled after shown. // Note: Cannot disable before requestShowFillDialog() because the method @@ -5715,6 +5736,15 @@ final class Session private boolean isFillDialogUiEnabled() { synchronized (mLock) { + if (mSessionFlags.mFillDialogDisabled) { + mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason( + FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED); + } + if (mSessionFlags.mScreenHasCredmanField) { + // Prefer to log "HAS_CREDMAN_FIELD" over "FILL_DIALOG_DISABLED". + mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason( + FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD); + } return !mSessionFlags.mFillDialogDisabled && !mSessionFlags.mScreenHasCredmanField; } } @@ -5779,6 +5809,8 @@ final class Session || !ArrayUtils.contains(mLastFillDialogTriggerIds, filledId)) { // Last fill dialog triggered ids are changed. if (sDebug) Log.w(TAG, "Last fill dialog triggered ids are changed."); + mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason( + FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED); return SHOW_FILL_DIALOG_NO; } @@ -5805,6 +5837,8 @@ final class Session // we need to wait for animation to happen. We can't return from here yet. // This is the situation #2 described above. Log.d(TAG, "Waiting for ime animation to complete before showing fill dialog"); + mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason( + FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION); mFillDialogRunnable = createFillDialogEvalRunnable( response, filledId, filterText, flags); return SHOW_FILL_DIALOG_WAIT; @@ -5814,9 +5848,15 @@ final class Session // max of start input time or the ime finish time long effectiveDuration = currentTimestampMs - Math.max(mLastInputStartTime, mImeAnimationFinishTimeMs); + mPresentationStatsEventLogger.maybeSetFillDialogReadyToShowMs( + currentTimestampMs); + mPresentationStatsEventLogger.maybeSetImeAnimationFinishMs( + Math.max(mLastInputStartTime, mImeAnimationFinishTimeMs)); if (effectiveDuration >= mFillDialogTimeoutMs) { Log.d(TAG, "Fill dialog not shown since IME has been up for more time than " + mFillDialogTimeoutMs + "ms"); + mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason( + FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED); return SHOW_FILL_DIALOG_NO; } else if (effectiveDuration < mFillDialogMinWaitAfterImeAnimationMs) { // we need to wait for some time after animation ends @@ -5824,6 +5864,8 @@ final class Session response, filledId, filterText, flags); mHandler.postDelayed(runnable, mFillDialogMinWaitAfterImeAnimationMs - effectiveDuration); + mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason( + FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END); return SHOW_FILL_DIALOG_WAIT; } } diff --git a/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java b/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java index 9a353fbc45bf..867cd51e1c2b 100644 --- a/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java +++ b/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java @@ -44,6 +44,7 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.LocalServices; +import com.android.server.backup.BackupRestoreTask.CancellationReason; import com.android.server.backup.internal.LifecycleOperationStorage; import java.util.Set; @@ -298,20 +299,22 @@ public class BackupAgentConnectionManager { // Offload operation cancellation off the main thread as the cancellation callbacks // might call out to BackupTransport. Other operations started on the same package // before the cancellation callback has executed will also be cancelled by the callback. - Runnable cancellationRunnable = () -> { - // handleCancel() causes the PerformFullTransportBackupTask to go on to - // tearDownAgentAndKill: that will unbindBackupAgent in the Activity Manager, so - // that the package being backed up doesn't get stuck in restricted mode until the - // backup time-out elapses. - for (int token : mOperationStorage.operationTokensForPackage(packageName)) { - if (DEBUG) { - Slog.d(TAG, - mUserIdMsg + "agentDisconnected: will handleCancel(all) for token:" - + Integer.toHexString(token)); - } - mUserBackupManagerService.handleCancel(token, true /* cancelAll */); - } - }; + Runnable cancellationRunnable = + () -> { + // On handleCancel(), the operation will call unbindAgent() which will make + // sure the app doesn't get stuck in restricted mode. + for (int token : mOperationStorage.operationTokensForPackage(packageName)) { + if (DEBUG) { + Slog.d( + TAG, + mUserIdMsg + + "agentDisconnected: cancelling for token:" + + Integer.toHexString(token)); + } + mUserBackupManagerService.handleCancel( + token, CancellationReason.AGENT_DISCONNECTED); + } + }; getThreadForCancellation(cancellationRunnable).start(); mAgentConnectLock.notifyAll(); diff --git a/services/backup/java/com/android/server/backup/BackupRestoreTask.java b/services/backup/java/com/android/server/backup/BackupRestoreTask.java index acaab0c54191..7ec5f0d786ed 100644 --- a/services/backup/java/com/android/server/backup/BackupRestoreTask.java +++ b/services/backup/java/com/android/server/backup/BackupRestoreTask.java @@ -16,9 +16,12 @@ package com.android.server.backup; -/** - * Interface and methods used by the asynchronous-with-timeout backup/restore operations. - */ +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Interface and methods used by the asynchronous-with-timeout backup/restore operations. */ public interface BackupRestoreTask { // Execute one tick of whatever state machine the task implements @@ -27,6 +30,24 @@ public interface BackupRestoreTask { // An operation that wanted a callback has completed void operationComplete(long result); - // An operation that wanted a callback has timed out - void handleCancel(boolean cancelAll); + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + CancellationReason.TIMEOUT, + CancellationReason.AGENT_DISCONNECTED, + CancellationReason.EXTERNAL, + CancellationReason.SCHEDULED_JOB_STOPPED, + }) + @interface CancellationReason { + // The task timed out. + int TIMEOUT = 0; + // The agent went away before the task was able to finish (e.g. due to an app crash). + int AGENT_DISCONNECTED = 1; + // An external caller cancelled the operation (e.g. via BackupManager#cancelBackups). + int EXTERNAL = 2; + // The job scheduler has stopped an ongoing scheduled backup pass. + int SCHEDULED_JOB_STOPPED = 3; + } + + /** The task is cancelled for the given {@link CancellationReason}. */ + void handleCancel(@CancellationReason int cancellationReason); } diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java index 2143aaaa4cd6..b3af444ff9bd 100644 --- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java +++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java @@ -102,6 +102,7 @@ import com.android.internal.util.Preconditions; import com.android.server.AppWidgetBackupBridge; import com.android.server.EventLogTags; import com.android.server.LocalServices; +import com.android.server.backup.BackupRestoreTask.CancellationReason; import com.android.server.backup.OperationStorage.OpState; import com.android.server.backup.OperationStorage.OpType; import com.android.server.backup.fullbackup.FullBackupEntry; @@ -168,6 +169,7 @@ import java.util.Random; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntConsumer; /** System service that performs backup/restore operations. */ public class UserBackupManagerService { @@ -1816,11 +1818,9 @@ public class UserBackupManagerService { for (Integer token : operationsToCancel) { mOperationStorage.cancelOperation( - token, /* cancelAll */ - true, - operationType -> { - /* no callback needed here */ - }); + token, + operationType -> {}, // no callback needed here + CancellationReason.EXTERNAL); } // We don't want the backup jobs to kick in any time soon. // Reschedules them to run in the distant future. @@ -1897,19 +1897,17 @@ public class UserBackupManagerService { } /** Cancel the operation associated with {@code token}. */ - public void handleCancel(int token, boolean cancelAll) { + public void handleCancel(int token, @CancellationReason int cancellationReason) { // Remove all pending timeout messages of types OpType.BACKUP_WAIT and // OpType.RESTORE_WAIT. On the other hand, OP_TYPE_BACKUP cannot time out and // doesn't require cancellation. - mOperationStorage.cancelOperation( - token, - cancelAll, - operationType -> { - if (operationType == OpType.BACKUP_WAIT - || operationType == OpType.RESTORE_WAIT) { - mBackupHandler.removeMessages(getMessageIdForOperationType(operationType)); + IntConsumer timeoutCallback = + opType -> { + if (opType == OpType.BACKUP_WAIT || opType == OpType.RESTORE_WAIT) { + mBackupHandler.removeMessages(getMessageIdForOperationType(opType)); } - }); + }; + mOperationStorage.cancelOperation(token, timeoutCallback, cancellationReason); } /** Returns {@code true} if a backup is currently running, else returns {@code false}. */ @@ -2219,20 +2217,17 @@ public class UserBackupManagerService { // offload the mRunningFullBackupTask.handleCancel() call to another thread, // as we might have to wait for mCancelLock Runnable endFullBackupRunnable = - new Runnable() { - @Override - public void run() { - PerformFullTransportBackupTask pftbt = null; - synchronized (mQueueLock) { - if (mRunningFullBackupTask != null) { - pftbt = mRunningFullBackupTask; - } - } - if (pftbt != null) { - Slog.i(TAG, mLogIdMsg + "Telling running backup to stop"); - pftbt.handleCancel(true); + () -> { + PerformFullTransportBackupTask pftbt = null; + synchronized (mQueueLock) { + if (mRunningFullBackupTask != null) { + pftbt = mRunningFullBackupTask; } } + if (pftbt != null) { + Slog.i(TAG, mLogIdMsg + "Telling running backup to stop"); + pftbt.handleCancel(CancellationReason.SCHEDULED_JOB_STOPPED); + } }; new Thread(endFullBackupRunnable, "end-full-backup").start(); } diff --git a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java index ebb1194c7c4a..b173f76e5f6f 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java +++ b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java @@ -25,6 +25,7 @@ import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_A import android.annotation.UserIdInt; import android.app.ApplicationThreadConstants; import android.app.IBackupAgent; +import android.app.backup.BackupManagerMonitor; import android.app.backup.BackupTransport; import android.app.backup.FullBackupDataOutput; import android.content.pm.ApplicationInfo; @@ -268,6 +269,12 @@ public class FullBackupEngine { mBackupManagerMonitorEventSender.monitorAgentLoggingResults(mPkg, mAgent); } catch (IOException e) { Slog.e(TAG, "Error backing up " + mPkg.packageName + ": " + e.getMessage()); + // This is likely due to the app process dying. + mBackupManagerMonitorEventSender.monitorEvent( + BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN, + mPkg, + BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, + /* extras= */ null); result = BackupTransport.AGENT_ERROR; } finally { try { diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java index 0d4364e14e03..7fc9ed3e0213 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java +++ b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java @@ -484,7 +484,7 @@ public class PerformAdbBackupTask extends FullBackupTask implements BackupRestor } @Override - public void handleCancel(boolean cancelAll) { + public void handleCancel(@CancellationReason int cancellationReason) { final PackageInfo target = mCurrentTarget; Slog.w(TAG, "adb backup cancel of " + target); if (target != null) { diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java index c182c2618fdf..48d21c3a222f 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java +++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java @@ -162,7 +162,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba // This is true when a backup operation for some package is in progress. private volatile boolean mIsDoingBackup; - private volatile boolean mCancelAll; + private volatile boolean mCancelled; private final int mCurrentOpToken; private final BackupAgentTimeoutParameters mAgentTimeoutParameters; private final BackupEligibilityRules mBackupEligibilityRules; @@ -199,7 +199,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba if (backupManagerService.isBackupOperationInProgress()) { Slog.d(TAG, "Skipping full backup. A backup is already in progress."); - mCancelAll = true; + mCancelled = true; return; } @@ -287,25 +287,31 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } @Override - public void handleCancel(boolean cancelAll) { + public void handleCancel(@CancellationReason int cancellationReason) { synchronized (mCancelLock) { - // We only support 'cancelAll = true' case for this task. Cancelling of a single package - - // due to timeout is handled by SinglePackageBackupRunner and + // This callback is only used for cancelling the entire backup operation. Cancelling of + // a single package due to timeout is handled by SinglePackageBackupRunner and // SinglePackageBackupPreflight. + if (cancellationReason == CancellationReason.TIMEOUT) { + Slog.wtf(TAG, "This task cannot time out"); + return; + } - if (!cancelAll) { - Slog.wtf(TAG, "Expected cancelAll to be true."); + // We don't cancel the entire operation if a single agent is disconnected unexpectedly. + // SinglePackageBackupRunner and SinglePackageBackupPreflight will receive the same + // callback and fail gracefully. The operation should then continue to the next package. + if (cancellationReason == CancellationReason.AGENT_DISCONNECTED) { + return; } - if (mCancelAll) { + if (mCancelled) { Slog.d(TAG, "Ignoring duplicate cancel call."); return; } - mCancelAll = true; + mCancelled = true; if (mIsDoingBackup) { - mUserBackupManagerService.handleCancel(mBackupRunnerOpToken, cancelAll); + mUserBackupManagerService.handleCancel(mBackupRunnerOpToken, cancellationReason); try { // If we're running a backup we should be connected to a transport BackupTransportClient transport = @@ -410,7 +416,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba int backupPackageStatus; long quota = Long.MAX_VALUE; synchronized (mCancelLock) { - if (mCancelAll) { + if (mCancelled) { break; } backupPackageStatus = transport.performFullBackup(currentPackage, @@ -478,7 +484,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba if (nRead > 0) { out.write(buffer, 0, nRead); synchronized (mCancelLock) { - if (!mCancelAll) { + if (!mCancelled) { backupPackageStatus = transport.sendBackupData(nRead); } } @@ -509,7 +515,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba synchronized (mCancelLock) { mIsDoingBackup = false; // If mCancelCurrent is true, we have already called cancelFullBackup(). - if (!mCancelAll) { + if (!mCancelled) { if (backupRunnerResult == BackupTransport.TRANSPORT_OK) { // If we were otherwise in a good state, now interpret the final // result based on what finishBackup() returns. If we're in a @@ -607,7 +613,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba .sendBackupOnPackageResult(mBackupObserver, packageName, BackupManager.ERROR_BACKUP_CANCELLED); Slog.w(TAG, "Backup cancelled. package=" + packageName + - ", cancelAll=" + mCancelAll); + ", entire session cancelled=" + mCancelled); EventLog.writeEvent(EventLogTags.FULL_BACKUP_CANCELLED, packageName); mUserBackupManagerService.getBackupAgentConnectionManager().unbindAgent( currentPackage.applicationInfo, /* allowKill= */ true); @@ -654,7 +660,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } finally { - if (mCancelAll) { + if (mCancelled) { backupRunStatus = BackupManager.ERROR_BACKUP_CANCELLED; } @@ -820,7 +826,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } @Override - public void handleCancel(boolean cancelAll) { + public void handleCancel(@CancellationReason int cancellationReason) { if (DEBUG) { Slog.i(TAG, "Preflight cancelled; failing"); } @@ -974,7 +980,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba public void operationComplete(long result) { /* intentionally empty */ } @Override - public void handleCancel(boolean cancelAll) { + public void handleCancel(@CancellationReason int cancellationReason) { Slog.w(TAG, "Full backup cancel of " + mTarget.packageName); mBackupManagerMonitorEventSender.monitorEvent( @@ -984,7 +990,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba /* extras= */ null); mIsCancelled = true; // Cancel tasks spun off by this task. - mUserBackupManagerService.handleCancel(mEphemeralToken, cancelAll); + mUserBackupManagerService.handleCancel(mEphemeralToken, cancellationReason); mUserBackupManagerService.getBackupAgentConnectionManager().unbindAgent( mTarget.applicationInfo, /* allowKill= */ true); // Free up everyone waiting on this task and its children. diff --git a/services/backup/java/com/android/server/backup/internal/BackupHandler.java b/services/backup/java/com/android/server/backup/internal/BackupHandler.java index 87cf8a313651..464dc2dfe1ec 100644 --- a/services/backup/java/com/android/server/backup/internal/BackupHandler.java +++ b/services/backup/java/com/android/server/backup/internal/BackupHandler.java @@ -34,6 +34,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.server.EventLogTags; import com.android.server.backup.BackupAgentTimeoutParameters; import com.android.server.backup.BackupRestoreTask; +import com.android.server.backup.BackupRestoreTask.CancellationReason; import com.android.server.backup.DataChangedJournal; import com.android.server.backup.OperationStorage; import com.android.server.backup.TransportManager; @@ -410,8 +411,8 @@ public class BackupHandler extends Handler { case MSG_BACKUP_OPERATION_TIMEOUT: case MSG_RESTORE_OPERATION_TIMEOUT: { - Slog.d(TAG, "Timeout message received for token=" + Integer.toHexString(msg.arg1)); - backupManagerService.handleCancel(msg.arg1, false); + Slog.d(TAG, "Timeout for token=" + Integer.toHexString(msg.arg1)); + backupManagerService.handleCancel(msg.arg1, CancellationReason.TIMEOUT); break; } diff --git a/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java b/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java index 0b974e2d0a8a..5aacb2f4f007 100644 --- a/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java +++ b/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java @@ -24,6 +24,7 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.server.backup.BackupRestoreTask; +import com.android.server.backup.BackupRestoreTask.CancellationReason; import com.android.server.backup.OperationStorage; import com.google.android.collect.Sets; @@ -296,20 +297,18 @@ public class LifecycleOperationStorage implements OperationStorage { } /** - * Cancel the operation associated with {@code token}. Cancellation may be - * propagated to the operation's callback (a {@link BackupRestoreTask}) if - * the operation has one, and the cancellation is due to the operation - * timing out. + * Cancel the operation associated with {@code token}. Cancellation may be propagated to the + * operation's callback (a {@link BackupRestoreTask}) if the operation has one, and the + * cancellation is due to the operation timing out. * * @param token the operation token specified when registering the operation - * @param cancelAll this is passed on when propagating the cancellation - * @param operationTimedOutCallback a lambda that is invoked with the - * operation type where the operation is - * cancelled due to timeout, allowing the - * caller to do type-specific clean-ups. + * @param operationTimedOutCallback a lambda that is invoked with the operation type where the + * operation is cancelled due to timeout, allowing the caller to do type-specific clean-ups. */ public void cancelOperation( - int token, boolean cancelAll, IntConsumer operationTimedOutCallback) { + int token, + IntConsumer operationTimedOutCallback, + @CancellationReason int cancellationReason) { // Notify any synchronous waiters Operation op = null; synchronized (mOperationsLock) { @@ -343,7 +342,7 @@ public class LifecycleOperationStorage implements OperationStorage { if (DEBUG) { Slog.v(TAG, "[UserID:" + mUserId + " Invoking cancel on " + op.callback); } - op.callback.handleCancel(cancelAll); + op.callback.handleCancel(cancellationReason); } } } diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java index 494b9d59a238..8e7a23ccbb25 100644 --- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java +++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java @@ -171,7 +171,7 @@ import java.util.concurrent.atomic.AtomicInteger; * complete backup should be performed. * * <p>This task is designed to run on a dedicated thread, with the exception of the {@link - * #handleCancel(boolean)} method, which can be called from any thread. + * BackupRestoreTask#handleCancel(int)} method, which can be called from any thread. */ // TODO: Stop poking into BMS state and doing things for it (e.g. synchronizing on public locks) // TODO: Consider having the caller responsible for some clean-up (like resetting state) @@ -1208,13 +1208,13 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { * * <p>Note: This method is inherently racy since there are no guarantees about how much of the * task will be executed after you made the call. - * - * @param cancelAll MUST be {@code true}. Will be removed. */ @Override - public void handleCancel(boolean cancelAll) { + public void handleCancel(@CancellationReason int cancellationReason) { // This is called in a thread different from the one that executes method run(). - Preconditions.checkArgument(cancelAll, "Can't partially cancel a key-value backup task"); + Preconditions.checkArgument( + cancellationReason != CancellationReason.TIMEOUT, + "Key-value backup task cannot time out"); markCancel(); waitCancel(); } diff --git a/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java b/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java index cb491c6f384e..f1829b6966a8 100644 --- a/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java +++ b/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java @@ -79,7 +79,7 @@ public class AdbRestoreFinishedLatch implements BackupRestoreTask { } @Override - public void handleCancel(boolean cancelAll) { + public void handleCancel(@CancellationReason int cancellationReason) { Slog.w(TAG, "adb onRestoreFinished() timed out"); mLatch.countDown(); mOperationStorage.removeOperation(mCurrentOpToken); diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java index 707ae03b3964..20f103cdfab4 100644 --- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java +++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java @@ -589,6 +589,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { monitoringExtras); Slog.e(TAG, "Failure getting next package name"); EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); + mStatus = BackupTransport.TRANSPORT_ERROR; nextState = UnifiedRestoreState.FINAL; return; } else if (mRestoreDescription == RestoreDescription.NO_MORE_PACKAGES) { @@ -1307,7 +1308,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // The app has timed out handling a restoring file @Override - public void handleCancel(boolean cancelAll) { + public void handleCancel(@CancellationReason int cancellationReason) { mOperationStorage.removeOperation(mEphemeralOpToken); Slog.w(TAG, "Full-data restore target timed out; shutting down"); Bundle monitoringExtras = addRestoreOperationTypeToEvent(/* extras= */ null); @@ -1555,7 +1556,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // A call to agent.doRestore() or agent.doRestoreFinished() has timed out @Override - public void handleCancel(boolean cancelAll) { + public void handleCancel(@CancellationReason int cancellationReason) { mOperationStorage.removeOperation(mEphemeralOpToken); Slog.e(TAG, "Timeout restoring application " + mCurrentPackage.packageName); Bundle monitoringExtras = addRestoreOperationTypeToEvent(/* extras= */ null); diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java index fad59d23a6dc..855c72acd7ca 100644 --- a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java +++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java @@ -389,6 +389,8 @@ public class BackupManagerMonitorDumpsysUtils { "Agent failure during restore"; case BackupManagerMonitor.LOG_EVENT_ID_FAILED_TO_READ_DATA_FROM_TRANSPORT -> "Failed to read data from Transport"; + case BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN -> + "LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN"; default -> "Unknown log event ID: " + code; }; return id; diff --git a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java index cd9285cdfe91..cbee8391458d 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java +++ b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java @@ -58,13 +58,16 @@ import android.os.Handler; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.UserHandle; +import android.util.ArraySet; import android.util.Slog; import com.android.internal.R; import com.android.server.companion.CompanionDeviceManagerService; import com.android.server.companion.utils.PackageUtils; +import java.util.Arrays; import java.util.List; +import java.util.Set; /** * Class responsible for handling incoming {@link AssociationRequest}s. @@ -130,6 +133,12 @@ public class AssociationRequestsProcessor { private static final int ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW = 5; private static final long ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS = 60 * 60 * 1000; // 60 min; + // Set of profiles for which the association dialog cannot be skipped. + private static final Set<String> DEVICE_PROFILES_WITH_REQUIRED_CONFIRMATION = new ArraySet<>( + Arrays.asList( + AssociationRequest.DEVICE_PROFILE_APP_STREAMING, + AssociationRequest.DEVICE_PROFILE_NEARBY_DEVICE_STREAMING)); + private final @NonNull Context mContext; private final @NonNull PackageManagerInternal mPackageManagerInternal; private final @NonNull AssociationStore mAssociationStore; @@ -174,6 +183,7 @@ public class AssociationRequestsProcessor { // 2a. Check if association can be created without launching UI (i.e. CDM needs NEITHER // to perform discovery NOR to collect user consent). if (request.isSelfManaged() && !request.isForceConfirmation() + && !DEVICE_PROFILES_WITH_REQUIRED_CONFIRMATION.contains(request.getDeviceProfile()) && !willAddRoleHolder(request, packageName, userId)) { // 2a.1. Create association right away. createAssociationAndNotifyApplication(request, packageName, userId, diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index 93b4de856463..caf535ce7a40 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -142,14 +142,6 @@ public class VirtualDeviceManagerService extends SystemService { @GuardedBy("mVirtualDeviceManagerLock") private ArrayMap<String, AssociationInfo> mActiveAssociations = new ArrayMap<>(); - private final CompanionDeviceManager.OnAssociationsChangedListener mCdmAssociationListener = - new CompanionDeviceManager.OnAssociationsChangedListener() { - @Override - public void onAssociationsChanged(@NonNull List<AssociationInfo> associations) { - syncVirtualDevicesToCdmAssociations(associations); - } - }; - private class StrongAuthTracker extends LockPatternUtils.StrongAuthTracker { final Set<Integer> mUsersInLockdown = new ArraySet<>(); @@ -348,33 +340,6 @@ public class VirtualDeviceManagerService extends SystemService { return true; } - private void syncVirtualDevicesToCdmAssociations(List<AssociationInfo> associations) { - Set<VirtualDeviceImpl> virtualDevicesToRemove = new HashSet<>(); - synchronized (mVirtualDeviceManagerLock) { - if (mVirtualDevices.size() == 0) { - return; - } - - Set<Integer> activeAssociationIds = new HashSet<>(associations.size()); - for (AssociationInfo association : associations) { - activeAssociationIds.add(association.getId()); - } - - for (int i = 0; i < mVirtualDevices.size(); i++) { - VirtualDeviceImpl virtualDevice = mVirtualDevices.valueAt(i); - int deviceAssociationId = virtualDevice.getAssociationId(); - if (deviceAssociationId != CDM_ASSOCIATION_ID_NONE - && !activeAssociationIds.contains(deviceAssociationId)) { - virtualDevicesToRemove.add(virtualDevice); - } - } - } - - for (VirtualDeviceImpl virtualDevice : virtualDevicesToRemove) { - virtualDevice.close(); - } - } - void onCdmAssociationsChanged(List<AssociationInfo> associations) { ArrayMap<String, AssociationInfo> vdmAssociations = new ArrayMap<>(); for (int i = 0; i < associations.size(); ++i) { diff --git a/services/core/Android.bp b/services/core/Android.bp index 14d9d3f0c0a1..decac40d20f8 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -122,10 +122,10 @@ genrule { } genrule { - name: "statslog-mediarouter-java-gen", - tools: ["stats-log-api-gen"], - cmd: "$(location stats-log-api-gen) --java $(out) --module mediarouter --javaPackage com.android.server.media --javaClass MediaRouterStatsLog", - out: ["com/android/server/media/MediaRouterStatsLog.java"], + name: "statslog-mediarouter-java-gen", + tools: ["stats-log-api-gen"], + cmd: "$(location stats-log-api-gen) --java $(out) --module mediarouter --javaPackage com.android.server.media --javaClass MediaRouterStatsLog", + out: ["com/android/server/media/MediaRouterStatsLog.java"], } java_library_static { @@ -138,6 +138,7 @@ java_library_static { "ondeviceintelligence_conditionally", ], srcs: [ + ":android.hardware.audio.effect-V1-java-source", ":android.hardware.tv.hdmi.connection-V1-java-source", ":android.hardware.tv.hdmi.earc-V1-java-source", ":android.hardware.tv.mediaquality-V1-java-source", diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java index 96b30d4e1285..d60c6c534871 100644 --- a/services/core/java/com/android/server/Watchdog.java +++ b/services/core/java/com/android/server/Watchdog.java @@ -165,7 +165,6 @@ public class Watchdog implements Dumpable { "android.hardware.sensors@1.0::ISensors", "android.hardware.sensors@2.0::ISensors", "android.hardware.sensors@2.1::ISensors", - "android.hardware.vibrator@1.0::IVibrator", "android.hardware.vr@1.0::IVr", "android.system.suspend@1.0::ISystemSuspend" ); diff --git a/services/core/java/com/android/server/adb/AdbDebuggingManager.java b/services/core/java/com/android/server/adb/AdbDebuggingManager.java index a73a991bc6a6..658ea4c27e4c 100644 --- a/services/core/java/com/android/server/adb/AdbDebuggingManager.java +++ b/services/core/java/com/android/server/adb/AdbDebuggingManager.java @@ -130,8 +130,6 @@ import java.util.concurrent.atomic.AtomicBoolean; */ public class AdbDebuggingManager { private static final String TAG = AdbDebuggingManager.class.getSimpleName(); - private static final boolean DEBUG = false; - private static final boolean MDNS_DEBUG = false; private static final String ADBD_SOCKET = "adbd"; private static final String ADB_DIRECTORY = "misc/adb"; @@ -156,8 +154,6 @@ public class AdbDebuggingManager { @Nullable private final File mUserKeyFile; @Nullable private final File mTempKeysFile; - private static final String WIFI_PERSISTENT_CONFIG_PROPERTY = - "persist.adb.tls_server.enable"; private static final String WIFI_PERSISTENT_GUID = "persist.adb.wifi.guid"; private static final int PAIRING_CODE_LENGTH = 6; @@ -261,12 +257,10 @@ public class AdbDebuggingManager { mHandler.sendMessage(msg); boolean paired = native_pairing_wait(); - if (DEBUG) { - if (mPublicKey != null) { - Slog.i(TAG, "Pairing succeeded key=" + mPublicKey); - } else { - Slog.i(TAG, "Pairing failed"); - } + if (mPublicKey != null) { + Slog.i(TAG, "Pairing succeeded key=" + mPublicKey); + } else { + Slog.i(TAG, "Pairing failed"); } mNsdManager.unregisterService(this); @@ -307,7 +301,7 @@ public class AdbDebuggingManager { @Override public void onServiceRegistered(NsdServiceInfo serviceInfo) { - if (MDNS_DEBUG) Slog.i(TAG, "Registered pairing service: " + serviceInfo); + Slog.i(TAG, "Registered pairing service: " + serviceInfo); } @Override @@ -319,7 +313,7 @@ public class AdbDebuggingManager { @Override public void onServiceUnregistered(NsdServiceInfo serviceInfo) { - if (MDNS_DEBUG) Slog.i(TAG, "Unregistered pairing service: " + serviceInfo); + Slog.i(TAG, "Unregistered pairing service: " + serviceInfo); } @Override @@ -354,7 +348,7 @@ public class AdbDebuggingManager { @Override public void run() { - if (DEBUG) Slog.d(TAG, "Starting adb port property poller"); + Slog.d(TAG, "Starting adb port property poller"); // Once adbwifi is enabled, we poll the service.adb.tls.port // system property until we get the port, or -1 on failure. // Let's also limit the polling to 10 seconds, just in case @@ -390,7 +384,7 @@ public class AdbDebuggingManager { class PortListenerImpl implements AdbConnectionPortListener { public void onPortReceived(int port) { - if (DEBUG) Slog.d(TAG, "Received tls port=" + port); + Slog.d(TAG, "Received tls port=" + port); Message msg = mHandler.obtainMessage(port > 0 ? AdbDebuggingHandler.MSG_SERVER_CONNECTED : AdbDebuggingHandler.MSG_SERVER_DISCONNECTED); @@ -419,11 +413,11 @@ public class AdbDebuggingManager { @Override public void run() { - if (DEBUG) Slog.d(TAG, "Entering thread"); + Slog.d(TAG, "Entering thread"); while (true) { synchronized (this) { if (mStopped) { - if (DEBUG) Slog.d(TAG, "Exiting thread"); + Slog.d(TAG, "Exiting thread"); return; } try { @@ -448,7 +442,7 @@ public class AdbDebuggingManager { LocalSocketAddress.Namespace.RESERVED); mInputStream = null; - if (DEBUG) Slog.d(TAG, "Creating socket"); + Slog.d(TAG, "Creating socket"); mSocket = new LocalSocket(LocalSocket.SOCKET_SEQPACKET); mSocket.connect(address); @@ -549,7 +543,7 @@ public class AdbDebuggingManager { } private void closeSocketLocked() { - if (DEBUG) Slog.d(TAG, "Closing socket"); + Slog.d(TAG, "Closing socket"); try { if (mOutputStream != null) { mOutputStream.close(); @@ -859,7 +853,7 @@ public class AdbDebuggingManager { private void startAdbDebuggingThread() { ++mAdbEnabledRefCount; - if (DEBUG) Slog.i(TAG, "startAdbDebuggingThread ref=" + mAdbEnabledRefCount); + Slog.i(TAG, "startAdbDebuggingThread ref=" + mAdbEnabledRefCount); if (mAdbEnabledRefCount > 1) { return; } @@ -875,7 +869,7 @@ public class AdbDebuggingManager { private void stopAdbDebuggingThread() { --mAdbEnabledRefCount; - if (DEBUG) Slog.i(TAG, "stopAdbDebuggingThread ref=" + mAdbEnabledRefCount); + Slog.i(TAG, "stopAdbDebuggingThread ref=" + mAdbEnabledRefCount); if (mAdbEnabledRefCount > 0) { return; } @@ -1093,7 +1087,7 @@ public class AdbDebuggingManager { intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); mContext.registerReceiver(mBroadcastReceiver, intentFilter); - SystemProperties.set(WIFI_PERSISTENT_CONFIG_PROPERTY, "1"); + SystemProperties.set(AdbService.WIFI_PERSISTENT_CONFIG_PROPERTY, "1"); mConnectionPortPoller = new AdbDebuggingManager.AdbConnectionPortPoller(mPortListener); mConnectionPortPoller.start(); @@ -1101,7 +1095,7 @@ public class AdbDebuggingManager { startAdbDebuggingThread(); mAdbWifiEnabled = true; - if (DEBUG) Slog.i(TAG, "adb start wireless adb"); + Slog.i(TAG, "adb start wireless adb"); break; } case MSG_ADBDWIFI_DISABLE: @@ -1143,7 +1137,7 @@ public class AdbDebuggingManager { intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); mContext.registerReceiver(mBroadcastReceiver, intentFilter); - SystemProperties.set(WIFI_PERSISTENT_CONFIG_PROPERTY, "1"); + SystemProperties.set(AdbService.WIFI_PERSISTENT_CONFIG_PROPERTY, "1"); mConnectionPortPoller = new AdbDebuggingManager.AdbConnectionPortPoller(mPortListener); mConnectionPortPoller.start(); @@ -1151,7 +1145,7 @@ public class AdbDebuggingManager { startAdbDebuggingThread(); mAdbWifiEnabled = true; - if (DEBUG) Slog.i(TAG, "adb start wireless adb"); + Slog.i(TAG, "adb start wireless adb"); break; case MSG_ADBWIFI_DENY: Settings.Global.putInt(mContentResolver, @@ -1259,7 +1253,7 @@ public class AdbDebuggingManager { break; } case MSG_ADBD_SOCKET_CONNECTED: { - if (DEBUG) Slog.d(TAG, "adbd socket connected"); + Slog.d(TAG, "adbd socket connected"); if (mAdbWifiEnabled) { // In scenarios where adbd is restarted, the tls port may change. mConnectionPortPoller = @@ -1269,7 +1263,7 @@ public class AdbDebuggingManager { break; } case MSG_ADBD_SOCKET_DISCONNECTED: { - if (DEBUG) Slog.d(TAG, "adbd socket disconnected"); + Slog.d(TAG, "adbd socket disconnected"); if (mConnectionPortPoller != null) { mConnectionPortPoller.cancelAndWait(); mConnectionPortPoller = null; @@ -1477,7 +1471,7 @@ public class AdbDebuggingManager { } private void updateUIPairCode(String code) { - if (DEBUG) Slog.i(TAG, "updateUIPairCode: " + code); + Slog.i(TAG, "updateUIPairCode: " + code); Intent intent = new Intent(AdbManager.WIRELESS_DEBUG_PAIRING_RESULT_ACTION); intent.putExtra(AdbManager.WIRELESS_PAIRING_CODE_EXTRA, code); diff --git a/services/core/java/com/android/server/adb/AdbService.java b/services/core/java/com/android/server/adb/AdbService.java index 55d8dba69626..aae48daa5dde 100644 --- a/services/core/java/com/android/server/adb/AdbService.java +++ b/services/core/java/com/android/server/adb/AdbService.java @@ -222,15 +222,14 @@ public class AdbService extends IAdbManager.Stub { } } - private static final String TAG = "AdbService"; - private static final boolean DEBUG = false; + private static final String TAG = AdbService.class.getSimpleName(); /** * The persistent property which stores whether adb is enabled or not. * May also contain vendor-specific default functions for testing purposes. */ private static final String USB_PERSISTENT_CONFIG_PROPERTY = "persist.sys.usb.config"; - private static final String WIFI_PERSISTENT_CONFIG_PROPERTY = "persist.adb.tls_server.enable"; + static final String WIFI_PERSISTENT_CONFIG_PROPERTY = "persist.adb.tls_server.enable"; private final Context mContext; private final ContentResolver mContentResolver; @@ -256,7 +255,7 @@ public class AdbService extends IAdbManager.Stub { * SystemServer}. */ public void systemReady() { - if (DEBUG) Slog.d(TAG, "systemReady"); + Slog.d(TAG, "systemReady"); /* * Use the normal bootmode persistent prop to maintain state of adb across @@ -287,7 +286,7 @@ public class AdbService extends IAdbManager.Stub { * Called in response to {@code SystemService.PHASE_BOOT_COMPLETED} from {@code SystemServer}. */ public void bootCompleted() { - if (DEBUG) Slog.d(TAG, "boot completed"); + Slog.d(TAG, "boot completed"); if (mDebuggingManager != null) { mDebuggingManager.setAdbEnabled(mIsAdbUsbEnabled, AdbTransportType.USB); mDebuggingManager.setAdbEnabled(mIsAdbWifiEnabled, AdbTransportType.WIFI); @@ -429,17 +428,13 @@ public class AdbService extends IAdbManager.Stub { @Override public void registerCallback(IAdbCallback callback) throws RemoteException { - if (DEBUG) { - Slog.d(TAG, "Registering callback " + callback); - } + Slog.d(TAG, "Registering callback " + callback); mCallbacks.register(callback); } @Override public void unregisterCallback(IAdbCallback callback) throws RemoteException { - if (DEBUG) { - Slog.d(TAG, "Unregistering callback " + callback); - } + Slog.d(TAG, "Unregistering callback " + callback); mCallbacks.unregister(callback); } /** @@ -500,11 +495,8 @@ public class AdbService extends IAdbManager.Stub { } private void setAdbEnabled(boolean enable, byte transportType) { - if (DEBUG) { - Slog.d(TAG, "setAdbEnabled(" + enable + "), mIsAdbUsbEnabled=" + mIsAdbUsbEnabled - + ", mIsAdbWifiEnabled=" + mIsAdbWifiEnabled + ", transportType=" - + transportType); - } + Slog.d(TAG, "setAdbEnabled(" + enable + "), mIsAdbUsbEnabled=" + mIsAdbUsbEnabled + + ", mIsAdbWifiEnabled=" + mIsAdbWifiEnabled + ", transportType=" + transportType); if (transportType == AdbTransportType.USB && enable != mIsAdbUsbEnabled) { mIsAdbUsbEnabled = enable; @@ -549,20 +541,14 @@ public class AdbService extends IAdbManager.Stub { mDebuggingManager.setAdbEnabled(enable, transportType); } - if (DEBUG) { - Slog.d(TAG, "Broadcasting enable = " + enable + ", type = " + transportType); - } + Slog.d(TAG, "Broadcasting enable = " + enable + ", type = " + transportType); mCallbacks.broadcast((callback) -> { - if (DEBUG) { - Slog.d(TAG, "Sending enable = " + enable + ", type = " + transportType - + " to " + callback); - } + Slog.d(TAG, "Sending enable = " + enable + ", type = " + transportType + " to " + + callback); try { callback.onDebuggingChanged(enable, transportType); } catch (RemoteException ex) { - if (DEBUG) { - Slog.d(TAG, "Unable to send onDebuggingChanged:", ex); - } + Slog.w(TAG, "Unable to send onDebuggingChanged:", ex); } }); } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b0b34d0ab9c4..76ba0054583b 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -16190,14 +16190,16 @@ public class ActivityManagerService extends IActivityManager.Stub return mUserController.switchUser(targetUserId); } + @Nullable @Override - public String getSwitchingFromUserMessage() { - return mUserController.getSwitchingFromSystemUserMessage(); + public String getSwitchingFromUserMessage(@UserIdInt int userId) { + return mUserController.getSwitchingFromUserMessage(userId); } + @Nullable @Override - public String getSwitchingToUserMessage() { - return mUserController.getSwitchingToSystemUserMessage(); + public String getSwitchingToUserMessage(@UserIdInt int userId) { + return mUserController.getSwitchingToUserMessage(userId); } @Override @@ -16938,13 +16940,13 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override - public void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage) { - mUserController.setSwitchingFromSystemUserMessage(switchingFromSystemUserMessage); + public void setSwitchingFromUserMessage(@UserIdInt int userId, @Nullable String message) { + mUserController.setSwitchingFromUserMessage(userId, message); } @Override - public void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage) { - mUserController.setSwitchingToSystemUserMessage(switchingToSystemUserMessage); + public void setSwitchingToUserMessage(@UserIdInt int userId, @Nullable String message) { + mUserController.setSwitchingToUserMessage(userId, message); } @Override diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index db0562f5750a..508c01802156 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -810,7 +810,7 @@ class BroadcastProcessQueue { * Return the broadcast being actively dispatched in this process. */ public @NonNull BroadcastRecord getActive() { - return Objects.requireNonNull(mActive); + return Objects.requireNonNull(mActive, toString()); } /** @@ -818,7 +818,7 @@ class BroadcastProcessQueue { * being actively dispatched in this process. */ public int getActiveIndex() { - Objects.requireNonNull(mActive); + Objects.requireNonNull(mActive, toString()); return mActiveIndex; } diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java index c76a0d0ac59a..d276b9a94791 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java @@ -606,8 +606,9 @@ class BroadcastQueueImpl extends BroadcastQueue { } else { mRunningColdStart.reEnqueueActiveBroadcast(); } - demoteFromRunningLocked(mRunningColdStart); + final BroadcastProcessQueue queue = mRunningColdStart; clearRunningColdStart(); + demoteFromRunningLocked(queue); enqueueUpdateRunningList(); } @@ -1527,6 +1528,15 @@ class BroadcastQueueImpl extends BroadcastQueue { final int cookie = traceBegin("demoteFromRunning"); // We've drained running broadcasts; maybe move back to runnable + if (mRunningColdStart == queue) { + // TODO: b/399020479 - Remove wtf log once we identify the case where mRunningColdStart + // is not getting cleared. + // If this queue is mRunningColdStart, then it should have been cleared before + // it is demoted. Log a wtf if this isn't the case. + Slog.wtf(TAG, "mRunningColdStart has not been cleared; mRunningColdStart.app: " + + mRunningColdStart.app + " , queue.app: " + queue.app, + new IllegalStateException()); + } queue.makeActiveIdle(); queue.traceProcessEnd(); @@ -2332,12 +2342,6 @@ class BroadcastQueueImpl extends BroadcastQueue { @VisibleForTesting @GuardedBy("mService") - @Nullable BroadcastProcessQueue removeProcessQueue(@NonNull ProcessRecord app) { - return removeProcessQueue(app.processName, app.info.uid); - } - - @VisibleForTesting - @GuardedBy("mService") @Nullable BroadcastProcessQueue removeProcessQueue(@NonNull String processName, int uid) { BroadcastProcessQueue prev = null; diff --git a/services/core/java/com/android/server/am/ConnectionRecord.java b/services/core/java/com/android/server/am/ConnectionRecord.java index 31704c442290..4e1d77c26129 100644 --- a/services/core/java/com/android/server/am/ConnectionRecord.java +++ b/services/core/java/com/android/server/am/ConnectionRecord.java @@ -142,6 +142,10 @@ final class ConnectionRecord implements OomAdjusterModernImpl.Connection{ | Context.BIND_BYPASS_USER_NETWORK_RESTRICTIONS); } + @Override + public boolean transmitsCpuTime() { + return !hasFlag(Context.BIND_ALLOW_FREEZE); + } public long getFlags() { return flags; @@ -273,6 +277,9 @@ final class ConnectionRecord implements OomAdjusterModernImpl.Connection{ if (hasFlag(Context.BIND_INCLUDE_CAPABILITIES)) { sb.append("CAPS "); } + if (hasFlag(Context.BIND_ALLOW_FREEZE)) { + sb.append("!CPU "); + } if (serviceDead) { sb.append("DEAD "); } diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 336a35e7a7e3..fa35da30bf4b 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -2802,7 +2802,7 @@ public class OomAdjuster { // we check the final procstate, and remove it if the procsate is below BFGS. capability |= getBfslCapabilityFromClient(client); - capability |= getCpuCapabilityFromClient(client); + capability |= getCpuCapabilityFromClient(cr, client); if (cr.notHasFlag(Context.BIND_WAIVE_PRIORITY)) { if (cr.hasFlag(Context.BIND_INCLUDE_CAPABILITIES)) { @@ -3259,7 +3259,7 @@ public class OomAdjuster { // we check the final procstate, and remove it if the procsate is below BFGS. capability |= getBfslCapabilityFromClient(client); - capability |= getCpuCapabilityFromClient(client); + capability |= getCpuCapabilityFromClient(conn, client); if (clientProcState >= PROCESS_STATE_CACHED_ACTIVITY) { // If the other app is cached for any reason, for purposes here @@ -3502,10 +3502,13 @@ public class OomAdjuster { /** * @return the CPU capability from a client (of a service binding or provider). */ - private static int getCpuCapabilityFromClient(ProcessRecord client) { - // Just grant CPU capability every time - // TODO(b/370817323): Populate with reasons to not propagate cpu capability across bindings. - return client.mState.getCurCapability() & PROCESS_CAPABILITY_CPU_TIME; + private static int getCpuCapabilityFromClient(OomAdjusterModernImpl.Connection conn, + ProcessRecord client) { + if (conn == null || conn.transmitsCpuTime()) { + return client.mState.getCurCapability() & PROCESS_CAPABILITY_CPU_TIME; + } else { + return 0; + } } /** diff --git a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java index 1b7e8f0bd244..7e7b5685cf13 100644 --- a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java +++ b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java @@ -635,6 +635,15 @@ public class OomAdjusterModernImpl extends OomAdjuster { * Returns true if this connection can propagate capabilities. */ boolean canAffectCapabilities(); + + /** + * Returns whether this connection transmits PROCESS_CAPABILITY_CPU_TIME to the host, if the + * client possesses it. + */ + default boolean transmitsCpuTime() { + // Always lend this capability by default. + return true; + } } /** diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 18f3500b2d56..40a9bbec3598 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -340,16 +340,16 @@ class UserController implements Handler.Callback { private volatile ArraySet<String> mCurWaitingUserSwitchCallbacks; /** - * Messages for switching from {@link android.os.UserHandle#SYSTEM}. + * Message shown when switching from a user. */ @GuardedBy("mLock") - private String mSwitchingFromSystemUserMessage; + private final SparseArray<String> mSwitchingFromUserMessage = new SparseArray<>(); /** - * Messages for switching to {@link android.os.UserHandle#SYSTEM}. + * Message shown when switching to a user. */ @GuardedBy("mLock") - private String mSwitchingToSystemUserMessage; + private final SparseArray<String> mSwitchingToUserMessage = new SparseArray<>(); /** * Callbacks that are still active after {@link #getUserSwitchTimeoutMs} @@ -2271,8 +2271,8 @@ class UserController implements Handler.Callback { private void showUserSwitchDialog(Pair<UserInfo, UserInfo> fromToUserPair) { // The dialog will show and then initiate the user switch by calling startUserInForeground mInjector.showUserSwitchingDialog(fromToUserPair.first, fromToUserPair.second, - getSwitchingFromSystemUserMessageUnchecked(), - getSwitchingToSystemUserMessageUnchecked(), + getSwitchingFromUserMessageUnchecked(fromToUserPair.first.id), + getSwitchingToUserMessageUnchecked(fromToUserPair.second.id), /* onShown= */ () -> sendStartUserSwitchFgMessage(fromToUserPair.second.id)); } @@ -3388,41 +3388,45 @@ class UserController implements Handler.Callback { return mLockPatternUtils.isLockScreenDisabled(userId); } - void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage) { + void setSwitchingFromUserMessage(@UserIdInt int user, @Nullable String message) { synchronized (mLock) { - mSwitchingFromSystemUserMessage = switchingFromSystemUserMessage; + mSwitchingFromUserMessage.put(user, message); } } - void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage) { + void setSwitchingToUserMessage(@UserIdInt int user, @Nullable String message) { synchronized (mLock) { - mSwitchingToSystemUserMessage = switchingToSystemUserMessage; + mSwitchingToUserMessage.put(user, message); } } // Called by AMS, must check permission - String getSwitchingFromSystemUserMessage() { - checkHasManageUsersPermission("getSwitchingFromSystemUserMessage()"); + @Nullable + String getSwitchingFromUserMessage(@UserIdInt int userId) { + checkHasManageUsersPermission("getSwitchingFromUserMessage()"); - return getSwitchingFromSystemUserMessageUnchecked(); + return getSwitchingFromUserMessageUnchecked(userId); } // Called by AMS, must check permission - String getSwitchingToSystemUserMessage() { - checkHasManageUsersPermission("getSwitchingToSystemUserMessage()"); + @Nullable + String getSwitchingToUserMessage(@UserIdInt int userId) { + checkHasManageUsersPermission("getSwitchingToUserMessage()"); - return getSwitchingToSystemUserMessageUnchecked(); + return getSwitchingToUserMessageUnchecked(userId); } - private String getSwitchingFromSystemUserMessageUnchecked() { + @Nullable + private String getSwitchingFromUserMessageUnchecked(@UserIdInt int userId) { synchronized (mLock) { - return mSwitchingFromSystemUserMessage; + return mSwitchingFromUserMessage.get(userId); } } - private String getSwitchingToSystemUserMessageUnchecked() { + @Nullable + private String getSwitchingToUserMessageUnchecked(@UserIdInt int userId) { synchronized (mLock) { - return mSwitchingToSystemUserMessage; + return mSwitchingToUserMessage.get(userId); } } @@ -3518,12 +3522,8 @@ class UserController implements Handler.Callback { + mIsBroadcastSentForSystemUserStarted); pw.println(" mIsBroadcastSentForSystemUserStarting:" + mIsBroadcastSentForSystemUserStarting); - if (mSwitchingFromSystemUserMessage != null) { - pw.println(" mSwitchingFromSystemUserMessage: " + mSwitchingFromSystemUserMessage); - } - if (mSwitchingToSystemUserMessage != null) { - pw.println(" mSwitchingToSystemUserMessage: " + mSwitchingToSystemUserMessage); - } + pw.println(" mSwitchingFromUserMessage:" + mSwitchingFromUserMessage); + pw.println(" mSwitchingToUserMessage:" + mSwitchingToUserMessage); pw.println(" mLastUserUnlockingUptime: " + mLastUserUnlockingUptime); } } @@ -4046,7 +4046,7 @@ class UserController implements Handler.Callback { } void showUserSwitchingDialog(UserInfo fromUser, UserInfo toUser, - String switchingFromSystemUserMessage, String switchingToSystemUserMessage, + @Nullable String switchingFromUserMessage, @Nullable String switchingToUserMessage, @NonNull Runnable onShown) { if (mService.mContext.getPackageManager() .hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) { @@ -4059,7 +4059,7 @@ class UserController implements Handler.Callback { synchronized (mUserSwitchingDialogLock) { dismissUserSwitchingDialog(null); mUserSwitchingDialog = new UserSwitchingDialog(mService.mContext, fromUser, toUser, - mHandler, switchingFromSystemUserMessage, switchingToSystemUserMessage); + mHandler, switchingFromUserMessage, switchingToUserMessage); mUserSwitchingDialog.show(onShown); } } diff --git a/services/core/java/com/android/server/am/UserSwitchingDialog.java b/services/core/java/com/android/server/am/UserSwitchingDialog.java index 223e0b79ec0b..f4e733a0c99f 100644 --- a/services/core/java/com/android/server/am/UserSwitchingDialog.java +++ b/services/core/java/com/android/server/am/UserSwitchingDialog.java @@ -52,6 +52,7 @@ import com.android.internal.R; import com.android.internal.util.ObjectUtils; import com.android.internal.util.UserIcons; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -75,21 +76,23 @@ class UserSwitchingDialog extends Dialog { protected final UserInfo mOldUser; protected final UserInfo mNewUser; - private final String mSwitchingFromSystemUserMessage; - private final String mSwitchingToSystemUserMessage; + @Nullable + private final String mSwitchingFromUserMessage; + @Nullable + private final String mSwitchingToUserMessage; protected final Context mContext; private final int mTraceCookie; UserSwitchingDialog(Context context, UserInfo oldUser, UserInfo newUser, Handler handler, - String switchingFromSystemUserMessage, String switchingToSystemUserMessage) { + @Nullable String switchingFromUserMessage, @Nullable String switchingToUserMessage) { super(context, R.style.Theme_Material_NoActionBar_Fullscreen); mContext = context; mOldUser = oldUser; mNewUser = newUser; mHandler = handler; - mSwitchingFromSystemUserMessage = switchingFromSystemUserMessage; - mSwitchingToSystemUserMessage = switchingToSystemUserMessage; + mSwitchingFromUserMessage = switchingFromUserMessage; + mSwitchingToUserMessage = switchingToUserMessage; mDisableAnimations = SystemProperties.getBoolean( "debug.usercontroller.disable_user_switching_dialog_animations", false); mTraceCookie = UserHandle.MAX_SECONDARY_USER_ID * oldUser.id + newUser.id; @@ -166,14 +169,14 @@ class UserSwitchingDialog extends Dialog { : R.string.demo_starting_message); } - final String message = - mOldUser.id == UserHandle.USER_SYSTEM ? mSwitchingFromSystemUserMessage - : mNewUser.id == UserHandle.USER_SYSTEM ? mSwitchingToSystemUserMessage : null; + if (mSwitchingFromUserMessage != null || mSwitchingToUserMessage != null) { + if (mSwitchingFromUserMessage != null && mSwitchingToUserMessage != null) { + return mSwitchingFromUserMessage + " " + mSwitchingToUserMessage; + } + return Objects.requireNonNullElse(mSwitchingFromUserMessage, mSwitchingToUserMessage); + } - return message != null ? message - // If switchingFromSystemUserMessage or switchingToSystemUserMessage is null, - // fallback to system message. - : res.getString(R.string.user_switching_message, mNewUser.name); + return res.getString(R.string.user_switching_message, mNewUser.name); } @Override diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java index 9dd09cef88f9..40085ed89294 100644 --- a/services/core/java/com/android/server/appop/AttributedOp.java +++ b/services/core/java/com/android/server/appop/AttributedOp.java @@ -113,7 +113,7 @@ final class AttributedOp { mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, parent.packageName, persistentDeviceId, tag, uidState, flags, accessTime, AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE, - DiscreteOpsRegistry.ACCESS_TYPE_NOTE_OP, accessCount); + accessCount); } /** @@ -257,8 +257,7 @@ final class AttributedOp { if (isStarted) { mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, parent.packageName, persistentDeviceId, tag, uidState, flags, startTime, - attributionFlags, attributionChainId, - DiscreteOpsRegistry.ACCESS_TYPE_START_OP, 1); + attributionFlags, attributionChainId, 1); } } @@ -344,9 +343,7 @@ final class AttributedOp { mAppOpsService.mHistoricalRegistry.increaseOpAccessDuration(parent.op, parent.uid, parent.packageName, persistentDeviceId, tag, event.getUidState(), event.getFlags(), finishedEvent.getNoteTime(), finishedEvent.getDuration(), - event.getAttributionFlags(), event.getAttributionChainId(), - isPausing ? DiscreteOpsRegistry.ACCESS_TYPE_PAUSE_OP - : DiscreteOpsRegistry.ACCESS_TYPE_FINISH_OP); + event.getAttributionFlags(), event.getAttributionChainId()); if (!isPausing) { mAppOpsService.mInProgressStartOpEventPool.release(event); @@ -454,7 +451,7 @@ final class AttributedOp { mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, parent.packageName, persistentDeviceId, tag, event.getUidState(), event.getFlags(), startTime, event.getAttributionFlags(), - event.getAttributionChainId(), DiscreteOpsRegistry.ACCESS_TYPE_RESUME_OP, 1); + event.getAttributionChainId(), 1); if (shouldSendActive) { mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid, parent.packageName, tag, event.getVirtualDeviceId(), true, diff --git a/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java b/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java index 86f5d9bd637f..c53e4bdc2205 100644 --- a/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java @@ -189,11 +189,11 @@ class DiscreteOpsDbHelper extends SQLiteOpenHelper { @AppOpsManager.HistoricalOpsRequestFilter int requestFilters, int uidFilter, @Nullable String packageNameFilter, @Nullable String attributionTagFilter, IntArray opCodesFilter, int opFlagsFilter, - long beginTime, long endTime, int limit, String orderByColumn) { + long beginTime, long endTime, int limit, String orderByColumn, boolean ascending) { List<SQLCondition> conditions = prepareConditions(beginTime, endTime, requestFilters, uidFilter, packageNameFilter, attributionTagFilter, opCodesFilter, opFlagsFilter); - String sql = buildSql(conditions, orderByColumn, limit); + String sql = buildSql(conditions, orderByColumn, ascending, limit); long startTime = 0; if (Flags.sqliteDiscreteOpEventLoggingEnabled()) { startTime = SystemClock.elapsedRealtime(); @@ -249,7 +249,8 @@ class DiscreteOpsDbHelper extends SQLiteOpenHelper { return results; } - private String buildSql(List<SQLCondition> conditions, String orderByColumn, int limit) { + private String buildSql(List<SQLCondition> conditions, String orderByColumn, boolean ascending, + int limit) { StringBuilder sql = new StringBuilder(DiscreteOpsTable.SELECT_TABLE_DATA); if (!conditions.isEmpty()) { sql.append(" WHERE "); @@ -264,6 +265,7 @@ class DiscreteOpsDbHelper extends SQLiteOpenHelper { if (orderByColumn != null) { sql.append(" ORDER BY ").append(orderByColumn); + sql.append(ascending ? " ASC " : " DESC "); } if (limit > 0) { sql.append(" LIMIT ").append(limit); diff --git a/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java b/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java index c38ee55b4f42..29e78be93da9 100644 --- a/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java @@ -40,7 +40,16 @@ public class DiscreteOpsMigrationHelper { static void migrateDiscreteOpsToXml(DiscreteOpsSqlRegistry sqlRegistry, DiscreteOpsXmlRegistry xmlRegistry) { List<DiscreteOpsSqlRegistry.DiscreteOp> sqlOps = sqlRegistry.getAllDiscreteOps(); - DiscreteOpsXmlRegistry.DiscreteOps xmlOps = getXmlDiscreteOps(sqlOps); + + // Only migrate configured discrete ops. Sqlite may contain all runtime ops, and more. + List<DiscreteOpsSqlRegistry.DiscreteOp> filteredList = new ArrayList<>(); + for (DiscreteOpsSqlRegistry.DiscreteOp opEvent: sqlOps) { + if (DiscreteOpsRegistry.isDiscreteOp(opEvent.getOpCode(), opEvent.getOpFlags())) { + filteredList.add(opEvent); + } + } + + DiscreteOpsXmlRegistry.DiscreteOps xmlOps = getXmlDiscreteOps(filteredList); xmlRegistry.migrateSqliteData(xmlOps); sqlRegistry.deleteDatabase(); } diff --git a/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java index 12c35ae92cbe..70b7016fbb90 100644 --- a/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java @@ -16,6 +16,9 @@ package com.android.server.appop; +import static android.app.AppOpsManager.OP_ACCESS_ACCESSIBILITY; +import static android.app.AppOpsManager.OP_ACCESS_NOTIFICATIONS; +import static android.app.AppOpsManager.OP_BIND_ACCESSIBILITY_SERVICE; import static android.app.AppOpsManager.OP_CAMERA; import static android.app.AppOpsManager.OP_COARSE_LOCATION; import static android.app.AppOpsManager.OP_EMERGENCY_LOCATION; @@ -23,30 +26,24 @@ import static android.app.AppOpsManager.OP_FINE_LOCATION; import static android.app.AppOpsManager.OP_FLAG_SELF; import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED; import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXY; +import static android.app.AppOpsManager.OP_GPS; import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION; import static android.app.AppOpsManager.OP_MONITOR_LOCATION; import static android.app.AppOpsManager.OP_PHONE_CALL_CAMERA; import static android.app.AppOpsManager.OP_PHONE_CALL_MICROPHONE; -import static android.app.AppOpsManager.OP_PROCESS_OUTGOING_CALLS; +import static android.app.AppOpsManager.OP_READ_DEVICE_IDENTIFIERS; import static android.app.AppOpsManager.OP_READ_HEART_RATE; -import static android.app.AppOpsManager.OP_READ_ICC_SMS; import static android.app.AppOpsManager.OP_READ_OXYGEN_SATURATION; import static android.app.AppOpsManager.OP_READ_SKIN_TEMPERATURE; -import static android.app.AppOpsManager.OP_READ_SMS; import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO; import static android.app.AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO; import static android.app.AppOpsManager.OP_RECORD_AUDIO; import static android.app.AppOpsManager.OP_RESERVED_FOR_TESTING; -import static android.app.AppOpsManager.OP_SEND_SMS; -import static android.app.AppOpsManager.OP_SMS_FINANCIAL_TRANSACTIONS; -import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW; -import static android.app.AppOpsManager.OP_WRITE_ICC_SMS; -import static android.app.AppOpsManager.OP_WRITE_SMS; +import static android.app.AppOpsManager.OP_RUN_IN_BACKGROUND; import static java.lang.Long.min; import static java.lang.Math.max; -import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AppOpsManager; @@ -54,16 +51,13 @@ import android.os.AsyncTask; import android.os.Build; import android.permission.flags.Flags; import android.provider.DeviceConfig; +import android.util.IntArray; import android.util.Slog; -import com.android.internal.util.ArrayUtils; -import com.android.internal.util.FrameworkStatsLog; - import java.io.PrintWriter; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.text.SimpleDateFormat; import java.time.Duration; +import java.util.Arrays; import java.util.Date; import java.util.Set; @@ -100,21 +94,37 @@ abstract class DiscreteOpsRegistry { static final String PROPERTY_DISCRETE_HISTORY_QUANTIZATION = "discrete_history_quantization_millis"; static final String PROPERTY_DISCRETE_FLAGS = "discrete_history_op_flags"; + // Comma separated app ops list config for testing i.e. "1,2,3,4" static final String PROPERTY_DISCRETE_OPS_LIST = "discrete_history_ops_cslist"; - static final String DEFAULT_DISCRETE_OPS = OP_FINE_LOCATION + "," + OP_COARSE_LOCATION + // These ops are deemed important for detecting a malicious app, and are recorded. + static final int[] IMPORTANT_OPS_FOR_SECURITY = new int[] { + OP_GPS, + OP_ACCESS_NOTIFICATIONS, + OP_RUN_IN_BACKGROUND, + OP_BIND_ACCESSIBILITY_SERVICE, + OP_ACCESS_ACCESSIBILITY, + OP_READ_DEVICE_IDENTIFIERS, + OP_MONITOR_HIGH_POWER_LOCATION, + OP_MONITOR_LOCATION + }; + + // These are additional ops, which are not backed by runtime permissions, but are recorded. + static final int[] ADDITIONAL_DISCRETE_OPS = new int[] { + OP_PHONE_CALL_MICROPHONE, + OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, + OP_RECEIVE_SANDBOX_TRIGGER_AUDIO, + OP_PHONE_CALL_CAMERA, + OP_EMERGENCY_LOCATION, + OP_RESERVED_FOR_TESTING + }; + + // Legacy ops captured in discrete database. + private static final String LEGACY_OPS = OP_FINE_LOCATION + "," + OP_COARSE_LOCATION + "," + OP_EMERGENCY_LOCATION + "," + OP_CAMERA + "," + OP_RECORD_AUDIO + "," + OP_PHONE_CALL_MICROPHONE + "," + OP_PHONE_CALL_CAMERA + "," + OP_RECEIVE_AMBIENT_TRIGGER_AUDIO + "," + OP_RECEIVE_SANDBOX_TRIGGER_AUDIO + "," + OP_READ_HEART_RATE + "," + OP_READ_OXYGEN_SATURATION + "," + OP_READ_SKIN_TEMPERATURE + "," + OP_RESERVED_FOR_TESTING; - static final int[] sDiscreteOpsToLog = - new int[]{OP_FINE_LOCATION, OP_COARSE_LOCATION, OP_EMERGENCY_LOCATION, OP_CAMERA, - OP_RECORD_AUDIO, OP_PHONE_CALL_MICROPHONE, OP_PHONE_CALL_CAMERA, - OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, OP_RECEIVE_SANDBOX_TRIGGER_AUDIO, OP_READ_SMS, - OP_WRITE_SMS, OP_SEND_SMS, OP_READ_ICC_SMS, OP_WRITE_ICC_SMS, - OP_SMS_FINANCIAL_TRANSACTIONS, OP_SYSTEM_ALERT_WINDOW, OP_MONITOR_LOCATION, - OP_MONITOR_HIGH_POWER_LOCATION, OP_PROCESS_OUTGOING_CALLS, - }; static final long DEFAULT_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(7).toMillis(); static final long MAXIMUM_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(30).toMillis(); @@ -126,7 +136,7 @@ abstract class DiscreteOpsRegistry { // in case of duplicate op events. static long sDiscreteHistoryQuantization; - static int[] sDiscreteOps; + static int[] sDiscreteOps = new int[0]; static int sDiscreteFlags; static final int OP_FLAGS_DISCRETE = OP_FLAG_SELF | OP_FLAG_TRUSTED_PROXIED @@ -134,27 +144,6 @@ abstract class DiscreteOpsRegistry { boolean mDebugMode = false; - static final int ACCESS_TYPE_NOTE_OP = - FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__NOTE_OP; - static final int ACCESS_TYPE_START_OP = - FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__START_OP; - static final int ACCESS_TYPE_FINISH_OP = - FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__FINISH_OP; - static final int ACCESS_TYPE_PAUSE_OP = - FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__PAUSE_OP; - static final int ACCESS_TYPE_RESUME_OP = - FrameworkStatsLog.APP_OP_ACCESS_TRACKED__ACCESS_TYPE__RESUME_OP; - - @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = {"ACCESS_TYPE_"}, value = { - ACCESS_TYPE_NOTE_OP, - ACCESS_TYPE_START_OP, - ACCESS_TYPE_FINISH_OP, - ACCESS_TYPE_PAUSE_OP, - ACCESS_TYPE_RESUME_OP - }) - @interface AccessType {} - void systemReady() { DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_PRIVACY, AsyncTask.THREAD_POOL_EXECUTOR, (DeviceConfig.Properties p) -> { @@ -166,8 +155,7 @@ abstract class DiscreteOpsRegistry { abstract void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op, @Nullable String attributionTag, @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState, long accessTime, long accessDuration, - @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId, - @DiscreteOpsRegistry.AccessType int accessType); + @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId); /** * A periodic callback from {@link AppOpsService} to flush the in memory events to disk. @@ -218,7 +206,7 @@ abstract class DiscreteOpsRegistry { } static boolean isDiscreteOp(int op, @AppOpsManager.OpFlags int flags) { - if (!ArrayUtils.contains(sDiscreteOps, op)) { + if (Arrays.binarySearch(sDiscreteOps, op) < 0) { return false; } if ((flags & (sDiscreteFlags)) == 0) { @@ -227,9 +215,6 @@ abstract class DiscreteOpsRegistry { return true; } - // could this be impl detail of discrete registry, just one test is using the method - // abstract DiscreteRegistry.DiscreteOps getAllDiscreteOps(); - private void setDiscreteHistoryParameters(DeviceConfig.Properties p) { if (p.getKeyset().contains(PROPERTY_DISCRETE_HISTORY_CUTOFF)) { sDiscreteHistoryCutoff = p.getLong(PROPERTY_DISCRETE_HISTORY_CUTOFF, @@ -251,11 +236,42 @@ abstract class DiscreteOpsRegistry { } else { sDiscreteHistoryQuantization = DEFAULT_DISCRETE_HISTORY_QUANTIZATION; } - sDiscreteFlags = p.getKeyset().contains(PROPERTY_DISCRETE_FLAGS) ? sDiscreteFlags = - p.getInt(PROPERTY_DISCRETE_FLAGS, OP_FLAGS_DISCRETE) : OP_FLAGS_DISCRETE; - sDiscreteOps = p.getKeyset().contains(PROPERTY_DISCRETE_OPS_LIST) ? parseOpsList( - p.getString(PROPERTY_DISCRETE_OPS_LIST, DEFAULT_DISCRETE_OPS)) : parseOpsList( - DEFAULT_DISCRETE_OPS); + sDiscreteFlags = p.getKeyset().contains(PROPERTY_DISCRETE_FLAGS) + ? p.getInt(PROPERTY_DISCRETE_FLAGS, OP_FLAGS_DISCRETE) : OP_FLAGS_DISCRETE; + String opsListConfig = p.getString(PROPERTY_DISCRETE_OPS_LIST, null); + sDiscreteOps = opsListConfig == null ? getDefaultOpsList() : parseOpsList(opsListConfig); + + Arrays.sort(sDiscreteOps); + } + + // App ops backed by runtime/dangerous permissions. + private static IntArray getRuntimePermissionOps() { + IntArray runtimeOps = new IntArray(); + for (int op = 0; op < AppOpsManager._NUM_OP; op++) { + if (AppOpsManager.opIsRuntimePermission(op)) { + runtimeOps.add(op); + } + } + return runtimeOps; + } + + /** + * @return an array of app ops captured into discrete database. + */ + private static int[] getDefaultOpsList() { + if (!(Flags.recordAllRuntimeAppopsSqlite() && Flags.enableSqliteAppopsAccesses())) { + return getDefaultLegacyOps(); + } + + IntArray discreteOpsArray = getRuntimePermissionOps(); + discreteOpsArray.addAll(IMPORTANT_OPS_FOR_SECURITY); + discreteOpsArray.addAll(ADDITIONAL_DISCRETE_OPS); + + return discreteOpsArray.toArray(); + } + + private static int[] getDefaultLegacyOps() { + return parseOpsList(LEGACY_OPS); } private static int[] parseOpsList(String opsList) { @@ -273,32 +289,8 @@ abstract class DiscreteOpsRegistry { } } catch (NumberFormatException e) { Slog.e(TAG, "Failed to parse Discrete ops list: " + e.getMessage()); - return parseOpsList(DEFAULT_DISCRETE_OPS); + return getDefaultOpsList(); } return result; } - - /** - * Whether app op access tacking is enabled and a metric event should be logged. - */ - static boolean shouldLogAccess(int op) { - return Flags.appopAccessTrackingLoggingEnabled() - && ArrayUtils.contains(sDiscreteOpsToLog, op); - } - - String getAttributionTag(String attributionTag, String packageName) { - if (attributionTag == null || packageName == null) { - return attributionTag; - } - int firstChar = 0; - if (attributionTag.startsWith(packageName)) { - firstChar = packageName.length(); - if (firstChar < attributionTag.length() && attributionTag.charAt(firstChar) - == '.') { - firstChar++; - } - } - return attributionTag.substring(firstChar); - } - } diff --git a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java index dc11be9aadb6..0e1fbf3a6d1a 100644 --- a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java @@ -36,7 +36,6 @@ import android.util.IntArray; import android.util.LongSparseArray; import android.util.Slog; -import com.android.internal.util.FrameworkStatsLog; import com.android.server.ServiceThread; import java.io.File; @@ -97,15 +96,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry { void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op, @Nullable String attributionTag, int flags, int uidState, - long accessTime, long accessDuration, int attributionFlags, int attributionChainId, - int accessType) { - if (shouldLogAccess(op)) { - FrameworkStatsLog.write(FrameworkStatsLog.APP_OP_ACCESS_TRACKED, uid, op, accessType, - uidState, flags, attributionFlags, - getAttributionTag(attributionTag, packageName), - attributionChainId); - } - + long accessTime, long accessDuration, int attributionFlags, int attributionChainId) { if (!isDiscreteOp(op, flags)) { return; } @@ -189,7 +180,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry { ChronoUnit.MILLIS).toEpochMilli()); List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter, packageNameFilter, attributionTagFilter, opCodes, opFlagsFilter, beginTimeMillis, - endTimeMillis, -1, null); + endTimeMillis, -1, null, false); LongSparseArray<AttributionChain> attributionChains = null; if (assembleChains) { @@ -222,14 +213,15 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry { @AppOpsManager.HistoricalOpsRequestFilter int filter, int dumpOp, @NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix, int nDiscreteOps) { - writeAndClearOldAccessHistory(); + // flush the cache into database before dump. + mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.getAllEventsAndClear()); IntArray opCodes = new IntArray(); if (dumpOp != AppOpsManager.OP_NONE) { opCodes.add(dumpOp); } List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter, packageNameFilter, attributionTagFilter, opCodes, 0, -1, - -1, nDiscreteOps, DiscreteOpsTable.Columns.ACCESS_TIME); + -1, nDiscreteOps, DiscreteOpsTable.Columns.ACCESS_TIME, false); pw.print(prefix); pw.print("Largest chain id: "); diff --git a/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java b/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java index 1523cca86607..909a04c44ae5 100644 --- a/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsTestingShim.java @@ -48,15 +48,13 @@ class DiscreteOpsTestingShim extends DiscreteOpsRegistry { @Override void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op, @Nullable String attributionTag, int flags, int uidState, long accessTime, - long accessDuration, int attributionFlags, int attributionChainId, int accessType) { + long accessDuration, int attributionFlags, int attributionChainId) { long start = SystemClock.uptimeMillis(); mXmlRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, attributionTag, flags, - uidState, accessTime, accessDuration, attributionFlags, attributionChainId, - accessType); + uidState, accessTime, accessDuration, attributionFlags, attributionChainId); long start2 = SystemClock.uptimeMillis(); mSqlRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, attributionTag, flags, - uidState, accessTime, accessDuration, attributionFlags, attributionChainId, - accessType); + uidState, accessTime, accessDuration, attributionFlags, attributionChainId); long end = SystemClock.uptimeMillis(); long xmlTimeTaken = start2 - start; long sqlTimeTaken = end - start2; diff --git a/services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java index a6e3fc7cc66a..20706b659ffb 100644 --- a/services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsXmlRegistry.java @@ -45,7 +45,6 @@ import android.util.Xml; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.ArrayUtils; -import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; @@ -159,15 +158,7 @@ class DiscreteOpsXmlRegistry extends DiscreteOpsRegistry { void recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op, @Nullable String attributionTag, @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState, long accessTime, long accessDuration, - @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId, - @AccessType int accessType) { - if (shouldLogAccess(op)) { - FrameworkStatsLog.write(FrameworkStatsLog.APP_OP_ACCESS_TRACKED, uid, op, accessType, - uidState, flags, attributionFlags, - getAttributionTag(attributionTag, packageName), - attributionChainId); - } - + @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) { if (!isDiscreteOp(op, flags)) { return; } diff --git a/services/core/java/com/android/server/appop/HistoricalRegistry.java b/services/core/java/com/android/server/appop/HistoricalRegistry.java index d267e0d9e536..06e43e8ec68d 100644 --- a/services/core/java/com/android/server/appop/HistoricalRegistry.java +++ b/services/core/java/com/android/server/appop/HistoricalRegistry.java @@ -497,7 +497,7 @@ final class HistoricalRegistry { @NonNull String deviceId, @Nullable String attributionTag, @UidState int uidState, @OpFlags int flags, long accessTime, @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId, - @DiscreteOpsRegistry.AccessType int accessType, int accessCount) { + int accessCount) { synchronized (mInMemoryLock) { if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) { if (!isPersistenceInitializedMLocked()) { @@ -510,7 +510,7 @@ final class HistoricalRegistry { mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, attributionTag, flags, uidState, accessTime, -1, attributionFlags, - attributionChainId, accessType); + attributionChainId); } } } @@ -533,8 +533,7 @@ final class HistoricalRegistry { void increaseOpAccessDuration(int op, int uid, @NonNull String packageName, @NonNull String deviceId, @Nullable String attributionTag, @UidState int uidState, @OpFlags int flags, long eventStartTime, long increment, - @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId, - @DiscreteOpsRegistry.AccessType int accessType) { + @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) { synchronized (mInMemoryLock) { if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) { if (!isPersistenceInitializedMLocked()) { @@ -546,7 +545,7 @@ final class HistoricalRegistry { attributionTag, uidState, flags, increment); mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, attributionTag, flags, uidState, eventStartTime, increment, - attributionFlags, attributionChainId, accessType); + attributionFlags, attributionChainId); } } } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index ada1cd73f775..766456134b20 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -4997,6 +4997,8 @@ public class AudioService extends IAudioService.Stub pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:" + disablePrescaleAbsoluteVolume()); pw.println("\tcom.android.media.audio.setStreamVolumeOrder - EOL"); + pw.println("\tandroid.media.audio.ringtoneUserUriCheck:" + + android.media.audio.Flags.ringtoneUserUriCheck()); pw.println("\tandroid.media.audio.roForegroundAudioControl:" + roForegroundAudioControl()); pw.println("\tandroid.media.audio.scoManagedByAudio:" diff --git a/services/core/java/com/android/server/display/DisplayControl.java b/services/core/java/com/android/server/display/DisplayControl.java index ddea285d3564..1d70953de6c0 100644 --- a/services/core/java/com/android/server/display/DisplayControl.java +++ b/services/core/java/com/android/server/display/DisplayControl.java @@ -29,7 +29,7 @@ import java.util.Objects; */ public class DisplayControl { private static native IBinder nativeCreateVirtualDisplay(String name, boolean secure, - String uniqueId, float requestedRefreshRate); + boolean optimizeForPower, String uniqueId, float requestedRefreshRate); private static native void nativeDestroyVirtualDisplay(IBinder displayToken); private static native void nativeOverrideHdrTypes(IBinder displayToken, int[] modes); private static native long[] nativeGetPhysicalDisplayIds(); @@ -49,7 +49,7 @@ public class DisplayControl { */ public static IBinder createVirtualDisplay(String name, boolean secure) { Objects.requireNonNull(name, "name must not be null"); - return nativeCreateVirtualDisplay(name, secure, "", 0.0f); + return nativeCreateVirtualDisplay(name, secure, true, "", 0.0f); } /** @@ -57,6 +57,10 @@ public class DisplayControl { * * @param name The name of the virtual display. * @param secure Whether this display is secure. + * @param optimizeForPower Whether SurfaceFlinger should optimize for power (instead of + * performance). Such displays will depend on another display for it to + * be shown and rendered, and that display will optimize for + * performance when it is on. * @param uniqueId The unique ID for the display. * @param requestedRefreshRate The requested refresh rate in frames per second. * For best results, specify a divisor of the physical refresh rate, e.g., 30 or 60 on @@ -66,10 +70,11 @@ public class DisplayControl { * @return The token reference for the display in SurfaceFlinger. */ public static IBinder createVirtualDisplay(String name, boolean secure, - String uniqueId, float requestedRefreshRate) { + boolean optimizeForPower, String uniqueId, float requestedRefreshRate) { Objects.requireNonNull(name, "name must not be null"); Objects.requireNonNull(uniqueId, "uniqueId must not be null"); - return nativeCreateVirtualDisplay(name, secure, uniqueId, requestedRefreshRate); + return nativeCreateVirtualDisplay(name, secure, optimizeForPower, uniqueId, + requestedRefreshRate); } /** diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 7016c11b69e7..a28069bbf050 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -674,9 +674,9 @@ public final class DisplayManagerService extends SystemService { mConfigParameterProvider = new DeviceConfigParameterProvider(DeviceConfigInterface.REAL); mExtraDisplayLoggingPackageName = DisplayProperties.debug_vri_package().orElse(null); mExtraDisplayEventLogging = !TextUtils.isEmpty(mExtraDisplayLoggingPackageName); - + // TODO(b/400384229): stats service needs to react to mirror-extended switch mExternalDisplayStatsService = new ExternalDisplayStatsService(mContext, mHandler, - this::isExtendedDisplayEnabled); + this::isExtendedDisplayAllowed); mDisplayNotificationManager = new DisplayNotificationManager(mFlags, mContext, mExternalDisplayStatsService); mExternalDisplayPolicy = new ExternalDisplayPolicy(new ExternalDisplayPolicyInjector()); @@ -690,7 +690,7 @@ public final class DisplayManagerService extends SystemService { deliverTopologyUpdate(update.first); }; mDisplayTopologyCoordinator = new DisplayTopologyCoordinator( - this::isExtendedDisplayEnabled, topologyChangedCallback, + this::isExtendedDisplayAllowed, topologyChangedCallback, new HandlerExecutor(mHandler), mSyncRoot, backupManager::dataChanged); } else { mDisplayTopologyCoordinator = null; @@ -2411,7 +2411,10 @@ public final class DisplayManagerService extends SystemService { updateLogicalDisplayState(display); } - private boolean isExtendedDisplayEnabled() { + private boolean isExtendedDisplayAllowed() { + if (mFlags.isDisplayContentModeManagementEnabled()) { + return true; + } try { return 0 != Settings.Global.getInt( mContext.getContentResolver(), @@ -6045,7 +6048,13 @@ public final class DisplayManagerService extends SystemService { return; } if (inTopology) { - mDisplayTopologyCoordinator.onDisplayAdded(getDisplayInfo(displayId)); + var info = getDisplayInfo(displayId); + if (info == null) { + Slog.w(TAG, "onDisplayBelongToTopologyChanged: cancelled displayId=" + + displayId + " info=null"); + return; + } + mDisplayTopologyCoordinator.onDisplayAdded(info); } else { mDisplayTopologyCoordinator.onDisplayRemoved(displayId); } diff --git a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java index 997fff58b952..b4df1f76dccb 100644 --- a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java +++ b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java @@ -69,9 +69,9 @@ class DisplayTopologyCoordinator { private final SparseArray<String> mDisplayIdToUniqueIdMapping = new SparseArray<>(); /** - * Check if extended displays are enabled. If not, a topology is not needed. + * Check if extended displays are allowed. If not, a topology is not needed. */ - private final BooleanSupplier mIsExtendedDisplayEnabled; + private final BooleanSupplier mIsExtendedDisplayAllowed; /** * Callback used to send topology updates. @@ -83,21 +83,21 @@ class DisplayTopologyCoordinator { private final DisplayManagerService.SyncRoot mSyncRoot; private final Runnable mTopologySavedCallback; - DisplayTopologyCoordinator(BooleanSupplier isExtendedDisplayEnabled, + DisplayTopologyCoordinator(BooleanSupplier isExtendedDisplayAllowed, Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> onTopologyChangedCallback, Executor topologyChangeExecutor, DisplayManagerService.SyncRoot syncRoot, Runnable topologySavedCallback) { - this(new Injector(), isExtendedDisplayEnabled, onTopologyChangedCallback, + this(new Injector(), isExtendedDisplayAllowed, onTopologyChangedCallback, topologyChangeExecutor, syncRoot, topologySavedCallback); } @VisibleForTesting - DisplayTopologyCoordinator(Injector injector, BooleanSupplier isExtendedDisplayEnabled, + DisplayTopologyCoordinator(Injector injector, BooleanSupplier isExtendedDisplayAllowed, Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> onTopologyChangedCallback, Executor topologyChangeExecutor, DisplayManagerService.SyncRoot syncRoot, Runnable topologySavedCallback) { mTopology = injector.getTopology(); - mIsExtendedDisplayEnabled = isExtendedDisplayEnabled; + mIsExtendedDisplayAllowed = isExtendedDisplayAllowed; mOnTopologyChangedCallback = onTopologyChangedCallback; mTopologyChangeExecutor = topologyChangeExecutor; mSyncRoot = syncRoot; @@ -262,9 +262,9 @@ class DisplayTopologyCoordinator { return false; } if ((info.type == Display.TYPE_EXTERNAL || info.type == Display.TYPE_OVERLAY) - && !mIsExtendedDisplayEnabled.getAsBoolean()) { + && !mIsExtendedDisplayAllowed.getAsBoolean()) { Slog.d(TAG, "Display " + info.displayId + " not allowed in topology because " - + "type is EXTERNAL or OVERLAY and !mIsExtendedDisplayEnabled"); + + "type is EXTERNAL or OVERLAY and !mIsExtendedDisplayAllowed"); return false; } return true; diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java index ac03a93ca9e1..c2eac8605851 100644 --- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java +++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java @@ -103,10 +103,10 @@ public class VirtualDisplayAdapter extends DisplayAdapter { Context context, Handler handler, Listener listener, DisplayManagerFlags featureFlags) { this(syncRoot, context, handler, listener, new SurfaceControlDisplayFactory() { @Override - public IBinder createDisplay(String name, boolean secure, String uniqueId, - float requestedRefreshRate) { - return DisplayControl.createVirtualDisplay(name, secure, uniqueId, - requestedRefreshRate); + public IBinder createDisplay(String name, boolean secure, boolean optimizeForPower, + String uniqueId, float requestedRefreshRate) { + return DisplayControl.createVirtualDisplay(name, secure, optimizeForPower, uniqueId, + requestedRefreshRate); } @Override @@ -182,9 +182,13 @@ public class VirtualDisplayAdapter extends DisplayAdapter { String name = virtualDisplayConfig.getName(); boolean secure = (flags & VIRTUAL_DISPLAY_FLAG_SECURE) != 0; + boolean neverBlank = isNeverBlank(flags); - IBinder displayToken = mSurfaceControlDisplayFactory.createDisplay(name, secure, uniqueId, - virtualDisplayConfig.getRequestedRefreshRate()); + // Never-blank displays are considered to be dependent on another display to be rendered. + // As a result, such displays should optimize for power instead of performance when it is + // powered on. + IBinder displayToken = mSurfaceControlDisplayFactory.createDisplay(name, secure, neverBlank, + uniqueId, virtualDisplayConfig.getRequestedRefreshRate()); MediaProjectionCallback mediaProjectionCallback = null; if (projection != null) { mediaProjectionCallback = new MediaProjectionCallback(appToken); @@ -318,6 +322,12 @@ public class VirtualDisplayAdapter extends DisplayAdapter { return mVirtualDisplayDevices.remove(appToken); } + private static boolean isNeverBlank(int flags) { + // Private non-mirror displays are never blank and always on. + return (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0 + && (flags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0; + } + private final class VirtualDisplayDevice extends DisplayDevice implements DeathRecipient { private static final int PENDING_SURFACE_CHANGE = 0x01; private static final int PENDING_RESIZE = 0x02; @@ -377,9 +387,7 @@ public class VirtualDisplayAdapter extends DisplayAdapter { mCallback = callback; mProjection = projection; mMediaProjectionCallback = mediaProjectionCallback; - // Private non-mirror displays are never blank and always on. - mNeverBlank = (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0 - && (flags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0; + mNeverBlank = isNeverBlank(flags); if (android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState() && !mNeverBlank) { // The display's power state depends on the power state of the state of its @@ -782,6 +790,10 @@ public class VirtualDisplayAdapter extends DisplayAdapter { * * @param name The name of the display. * @param secure Whether this display is secure. + * @param optimizeForPower Whether SurfaceFlinger should optimize for power (instead of + * performance). Such displays will depend on another display for + * it to be shown and rendered, and that display will optimize for + * performance when it is on. * @param uniqueId The unique ID for the display. * @param requestedRefreshRate * The refresh rate, frames per second, to request on the virtual display. @@ -791,8 +803,8 @@ public class VirtualDisplayAdapter extends DisplayAdapter { * the refresh rate of the leader physical display. * @return The token reference for the display in SurfaceFlinger. */ - IBinder createDisplay(String name, boolean secure, String uniqueId, - float requestedRefreshRate); + IBinder createDisplay(String name, boolean secure, boolean optimizeForPower, + String uniqueId, float requestedRefreshRate); /** * Destroy a display in SurfaceFlinger. diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index 97f9a7c4f2b0..8f5b831ca0b4 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -219,7 +219,9 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { && reason != HdmiControlService.INITIATED_BY_BOOT_UP; List<HdmiCecMessage> bufferedActiveSource = mDelayedMessageBuffer .getBufferedMessagesWithOpcode(Constants.MESSAGE_ACTIVE_SOURCE); - if (bufferedActiveSource.isEmpty()) { + List<HdmiCecMessage> bufferedActiveSourceFromService = mService.getCecMessageWithOpcode( + Constants.MESSAGE_ACTIVE_SOURCE); + if (bufferedActiveSource.isEmpty() && bufferedActiveSourceFromService.isEmpty()) { addAndStartAction(new RequestActiveSourceAction(this, new IHdmiControlCallback.Stub() { @Override public void onComplete(int result) { diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 6d973ac8d1b5..fdd0ef2f90e1 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -1593,6 +1593,17 @@ public class HdmiControlService extends SystemService { this.mCecMessageBuffer = cecMessageBuffer; } + List<HdmiCecMessage> getCecMessageWithOpcode(int opcode) { + List<HdmiCecMessage> cecMessagesWithOpcode = new ArrayList<>(); + List<HdmiCecMessage> cecMessages = mCecMessageBuffer.getBuffer(); + for (HdmiCecMessage message: cecMessages) { + if (message.getOpcode() == opcode) { + cecMessagesWithOpcode.add(message); + } + } + return cecMessagesWithOpcode; + } + /** * Returns {@link Looper} of main thread. Use this {@link Looper} instance * for tasks that are running on main service thread. diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index c2fecf283a34..d9db178e0dc2 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -568,6 +568,7 @@ public class InputManagerService extends IInputManager.Stub } mWindowManagerCallbacks = callbacks; registerLidSwitchCallbackInternal(mWindowManagerCallbacks); + mKeyGestureController.setWindowManagerCallbacks(callbacks); } public void setWiredAccessoryCallbacks(WiredAccessoryCallbacks callbacks) { @@ -2756,24 +2757,6 @@ public class InputManagerService extends IInputManager.Stub @Nullable IBinder focussedToken) { return InputManagerService.this.handleKeyGestureEvent(event); } - - @Override - public boolean isKeyGestureSupported(int gestureType) { - switch (gestureType) { - case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP: - case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN: - case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS: - return true; - default: - return false; - - } - } }); } @@ -3371,6 +3354,11 @@ public class InputManagerService extends IInputManager.Stub */ @Nullable SurfaceControl createSurfaceForGestureMonitor(String name, int displayId); + + /** + * Provide information on whether the keyguard is currently locked or not. + */ + boolean isKeyguardLocked(int displayId); } /** diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java index ef5babf19d83..395c77322c04 100644 --- a/services/core/java/com/android/server/input/KeyGestureController.java +++ b/services/core/java/com/android/server/input/KeyGestureController.java @@ -62,8 +62,10 @@ import android.view.Display; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; +import android.view.ViewConfiguration; import com.android.internal.R; +import com.android.internal.accessibility.AccessibilityShortcutController; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.IShortcutService; @@ -104,6 +106,7 @@ final class KeyGestureController { private static final int MSG_NOTIFY_KEY_GESTURE_EVENT = 1; private static final int MSG_PERSIST_CUSTOM_GESTURES = 2; private static final int MSG_LOAD_CUSTOM_GESTURES = 3; + private static final int MSG_ACCESSIBILITY_SHORTCUT = 4; // must match: config_settingsKeyBehavior in config.xml private static final int SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0; @@ -122,12 +125,15 @@ final class KeyGestureController { static final int POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS = 2; private final Context mContext; + private InputManagerService.WindowManagerCallbacks mWindowManagerCallbacks; private final Handler mHandler; private final Handler mIoHandler; private final int mSystemPid; private final KeyCombinationManager mKeyCombinationManager; private final SettingsObserver mSettingsObserver; private final AppLaunchShortcutManager mAppLaunchShortcutManager; + @VisibleForTesting + final AccessibilityShortcutController mAccessibilityShortcutController; private final InputGestureManager mInputGestureManager; private final DisplayManager mDisplayManager; @GuardedBy("mInputDataStore") @@ -175,8 +181,14 @@ final class KeyGestureController { private final boolean mVisibleBackgroundUsersEnabled = isVisibleBackgroundUsersEnabled(); - KeyGestureController(Context context, Looper looper, Looper ioLooper, + public KeyGestureController(Context context, Looper looper, Looper ioLooper, InputDataStore inputDataStore) { + this(context, looper, ioLooper, inputDataStore, new Injector()); + } + + @VisibleForTesting + KeyGestureController(Context context, Looper looper, Looper ioLooper, + InputDataStore inputDataStore, Injector injector) { mContext = context; mHandler = new Handler(looper, this::handleMessage); mIoHandler = new Handler(ioLooper, this::handleIoMessage); @@ -197,6 +209,8 @@ final class KeyGestureController { mSettingsObserver = new SettingsObserver(mHandler); mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext); mInputGestureManager = new InputGestureManager(mContext); + mAccessibilityShortcutController = injector.getAccessibilityShortcutController(mContext, + mHandler); mDisplayManager = Objects.requireNonNull(mContext.getSystemService(DisplayManager.class)); mInputDataStore = inputDataStore; mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); @@ -295,8 +309,8 @@ final class KeyGestureController { KeyEvent.KEYCODE_VOLUME_UP) { @Override public boolean preCondition() { - return isKeyGestureSupported( - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD); + return mAccessibilityShortcutController.isAccessibilityShortcutAvailable( + mWindowManagerCallbacks.isKeyguardLocked(DEFAULT_DISPLAY)); } @Override @@ -376,15 +390,15 @@ final class KeyGestureController { KeyEvent.KEYCODE_DPAD_DOWN) { @Override public boolean preCondition() { - return isKeyGestureSupported( - KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD); + return mAccessibilityShortcutController + .isAccessibilityShortcutAvailable(false); } @Override public void execute() { handleMultiKeyGesture( new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN}, - KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD, + KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD, KeyGestureEvent.ACTION_GESTURE_START, 0); } @@ -392,7 +406,7 @@ final class KeyGestureController { public void cancel() { handleMultiKeyGesture( new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN}, - KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD, + KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD, KeyGestureEvent.ACTION_GESTURE_COMPLETE, KeyGestureEvent.FLAG_CANCELLED); } @@ -438,6 +452,7 @@ final class KeyGestureController { mSettingsObserver.observe(); mAppLaunchShortcutManager.systemRunning(); mInputGestureManager.systemRunning(); + initKeyGestures(); int userId; synchronized (mUserLock) { @@ -447,6 +462,27 @@ final class KeyGestureController { mIoHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget(); } + @SuppressLint("MissingPermission") + private void initKeyGestures() { + InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class)); + im.registerKeyGestureEventHandler((event, focusedToken) -> { + switch (event.getKeyGestureType()) { + case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD: + if (event.getAction() == KeyGestureEvent.ACTION_GESTURE_START) { + mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT), + getAccessibilityShortcutTimeout()); + } else { + mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT); + } + return true; + default: + return false; + } + }); + } + public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) { if (mVisibleBackgroundUsersEnabled && shouldIgnoreKeyEventForVisibleBackgroundUser(event)) { return false; @@ -971,17 +1007,6 @@ final class KeyGestureController { return false; } - private boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) { - synchronized (mKeyGestureHandlerRecords) { - for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) { - if (handler.isKeyGestureSupported(gestureType)) { - return true; - } - } - } - return false; - } - public void notifyKeyGestureCompleted(int deviceId, int[] keycodes, int modifierState, @KeyGestureEvent.KeyGestureType int gestureType) { // TODO(b/358569822): Once we move the gesture detection logic to IMS, we ideally @@ -1019,9 +1044,16 @@ final class KeyGestureController { synchronized (mUserLock) { mCurrentUserId = userId; } + mAccessibilityShortcutController.setCurrentUser(userId); mIoHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget(); } + + public void setWindowManagerCallbacks( + @NonNull InputManagerService.WindowManagerCallbacks callbacks) { + mWindowManagerCallbacks = callbacks; + } + private boolean isDefaultDisplayOn() { Display defaultDisplay = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY); if (defaultDisplay == null) { @@ -1068,6 +1100,9 @@ final class KeyGestureController { AidlKeyGestureEvent event = (AidlKeyGestureEvent) msg.obj; notifyKeyGestureEvent(event); break; + case MSG_ACCESSIBILITY_SHORTCUT: + mAccessibilityShortcutController.performAccessibilityShortcut(); + break; } return true; } @@ -1347,17 +1382,6 @@ final class KeyGestureController { } return false; } - - public boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) { - try { - return mKeyGestureHandler.isKeyGestureSupported(gestureType); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to identify if key gesture type is supported by the " - + "process " + mPid + ", assuming it died.", ex); - binderDied(); - } - return false; - } } private class SettingsObserver extends ContentObserver { @@ -1413,6 +1437,25 @@ final class KeyGestureController { return event; } + private long getAccessibilityShortcutTimeout() { + synchronized (mUserLock) { + final ViewConfiguration config = ViewConfiguration.get(mContext); + final boolean hasDialogShown = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, mCurrentUserId) != 0; + final boolean skipTimeoutRestriction = + Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.SKIP_ACCESSIBILITY_SHORTCUT_DIALOG_TIMEOUT_RESTRICTION, + 0, mCurrentUserId) != 0; + + // If users manually set the volume key shortcut for any accessibility service, the + // system would bypass the timeout restriction of the shortcut dialog. + return hasDialogShown || skipTimeoutRestriction + ? config.getAccessibilityShortcutKeyTimeoutAfterConfirmation() + : config.getAccessibilityShortcutKeyTimeout(); + } + } + public void dump(IndentingPrintWriter ipw) { ipw.println("KeyGestureController:"); ipw.increaseIndent(); @@ -1459,4 +1502,12 @@ final class KeyGestureController { mAppLaunchShortcutManager.dump(ipw); mInputGestureManager.dump(ipw); } + + @VisibleForTesting + static class Injector { + AccessibilityShortcutController getAccessibilityShortcutController(Context context, + Handler handler) { + return new AccessibilityShortcutController(context, handler, UserHandle.USER_SYSTEM); + } + } } diff --git a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java index 600cf7f06981..1a4ead22f658 100644 --- a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java +++ b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java @@ -22,6 +22,7 @@ import static com.android.internal.inputmethod.SoftInputShowHideReason.REMOVE_IM import static com.android.internal.inputmethod.SoftInputShowHideReason.SHOW_IME_SCREENSHOT_FROM_IMMS; import static com.android.server.EventLogTags.IMF_HIDE_IME; import static com.android.server.EventLogTags.IMF_SHOW_IME; +import static com.android.server.inputmethod.ImeProtoLogGroup.IME_VISIBILITY_APPLIER_DEBUG; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_EXPLICIT; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_NOT_ALWAYS; @@ -36,7 +37,6 @@ import android.annotation.UserIdInt; import android.os.IBinder; import android.os.ResultReceiver; import android.util.EventLog; -import android.util.Slog; import android.view.MotionEvent; import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; @@ -46,6 +46,7 @@ import android.view.inputmethod.InputMethodManager; import com.android.internal.annotations.GuardedBy; import com.android.internal.inputmethod.InputMethodDebug; import com.android.internal.inputmethod.SoftInputShowHideReason; +import com.android.internal.protolog.ProtoLog; import com.android.server.LocalServices; import com.android.server.wm.ImeTargetVisibilityPolicy; import com.android.server.wm.WindowManagerInternal; @@ -58,9 +59,7 @@ import java.util.Objects; */ final class DefaultImeVisibilityApplier { - private static final String TAG = "DefaultImeVisibilityApplier"; - - private static final boolean DEBUG = InputMethodManagerService.DEBUG; + static final String TAG = "DefaultImeVisibilityApplier"; private InputMethodManagerService mService; @@ -93,11 +92,10 @@ final class DefaultImeVisibilityApplier { final var bindingController = userData.mBindingController; final IInputMethodInvoker curMethod = bindingController.getCurMethod(); if (curMethod != null) { - if (DEBUG) { - Slog.v(TAG, "Calling " + curMethod + ".showSoftInput(" + showInputToken - + ", " + showFlags + ", " + resultReceiver + ") for reason: " - + InputMethodDebug.softInputDisplayReasonToString(reason)); - } + ProtoLog.v(IME_VISIBILITY_APPLIER_DEBUG, + "Calling %s.showSoftInput(%s, %s, %s) for reason: %s", curMethod, + showInputToken, showFlags, resultReceiver, + InputMethodDebug.softInputDisplayReasonToString(reason)); // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not. if (curMethod.showSoftInput(showInputToken, statsToken, showFlags, resultReceiver)) { if (DEBUG_IME_VISIBILITY) { @@ -136,11 +134,9 @@ final class DefaultImeVisibilityApplier { // delivered to the IME process as an IPC. Hence the inconsistency between // IMMS#mInputShown and IMMS#mImeWindowVis should be resolved spontaneously in // the final state. - if (DEBUG) { - Slog.v(TAG, "Calling " + curMethod + ".hideSoftInput(0, " + hideInputToken - + ", " + resultReceiver + ") for reason: " - + InputMethodDebug.softInputDisplayReasonToString(reason)); - } + ProtoLog.v(IME_VISIBILITY_APPLIER_DEBUG, + "Calling %s.hideSoftInput(0, %s, %s) for reason: %s", curMethod, hideInputToken, + resultReceiver, InputMethodDebug.softInputDisplayReasonToString(reason)); // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not. if (curMethod.hideSoftInput(hideInputToken, statsToken, 0, resultReceiver)) { if (DEBUG_IME_VISIBILITY) { diff --git a/services/core/java/com/android/server/inputmethod/ImeProtoLogGroup.java b/services/core/java/com/android/server/inputmethod/ImeProtoLogGroup.java index f9a56effc800..ea4e29564cc0 100644 --- a/services/core/java/com/android/server/inputmethod/ImeProtoLogGroup.java +++ b/services/core/java/com/android/server/inputmethod/ImeProtoLogGroup.java @@ -23,7 +23,11 @@ import java.util.UUID; public enum ImeProtoLogGroup implements IProtoLogGroup { // TODO(b/393561240): add info/warn/error log level and replace in IMMS IMMS_DEBUG(Consts.ENABLE_DEBUG, false, false, - InputMethodManagerService.TAG); + InputMethodManagerService.TAG), + IME_VISIBILITY_APPLIER_DEBUG(Consts.ENABLE_DEBUG, false, false, + DefaultImeVisibilityApplier.TAG), + IME_VIS_STATE_COMPUTER_DEBUG(Consts.ENABLE_DEBUG, false, false, + ImeVisibilityStateComputer.TAG); private final boolean mEnabled; private volatile boolean mLogToProto; diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java index 5fe8318dbb3f..69353becc692 100644 --- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java +++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java @@ -32,6 +32,7 @@ import static android.view.WindowManager.LayoutParams.SoftInputModeFlags; import static com.android.internal.inputmethod.InputMethodDebug.softInputModeToString; import static com.android.internal.inputmethod.SoftInputShowHideReason.REMOVE_IME_SCREENSHOT_FROM_IMMS; import static com.android.internal.inputmethod.SoftInputShowHideReason.SHOW_IME_SCREENSHOT_FROM_IMMS; +import static com.android.server.inputmethod.ImeProtoLogGroup.IME_VIS_STATE_COMPUTER_DEBUG; import static com.android.server.inputmethod.InputMethodManagerService.computeImeDisplayIdForTarget; import android.accessibilityservice.AccessibilityService; @@ -58,6 +59,7 @@ import android.view.inputmethod.InputMethodManager; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.inputmethod.SoftInputShowHideReason; +import com.android.internal.protolog.ProtoLog; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.wm.WindowManagerInternal; @@ -72,9 +74,7 @@ import java.util.WeakHashMap; */ public final class ImeVisibilityStateComputer { - private static final String TAG = "ImeVisibilityStateComputer"; - - private static final boolean DEBUG = InputMethodManagerService.DEBUG; + static final String TAG = "ImeVisibilityStateComputer"; @UserIdInt private final int mUserId; @@ -292,12 +292,14 @@ public final class ImeVisibilityStateComputer { @InputMethodManager.HideFlags int hideFlags) { if ((hideFlags & InputMethodManager.HIDE_IMPLICIT_ONLY) != 0 && (mRequestedShowExplicitly || mShowForced)) { - if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, + "Not hiding: explicit show not cancelled by non-explicit hide"); ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT); return false; } if (mShowForced && (hideFlags & InputMethodManager.HIDE_NOT_ALWAYS) != 0) { - if (DEBUG) Slog.v(TAG, "Not hiding: forced show not cancelled by not-always hide"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, + "Not hiding: forced show not cancelled by not-always hide"); ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS); return false; } @@ -417,8 +419,8 @@ public final class ImeVisibilityStateComputer { @GuardedBy("ImfLock.class") private void setWindowStateInner(IBinder windowToken, @NonNull ImeTargetWindowState newState) { - if (DEBUG) Slog.d(TAG, "setWindowStateInner, windowToken=" + windowToken - + ", state=" + newState); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "setWindowStateInner, windowToken=%s, state=%s", + windowToken, newState); mRequestWindowStateMap.put(windowToken, newState); } @@ -466,7 +468,7 @@ public final class ImeVisibilityStateComputer { // Because the app might leverage these flags to hide soft-keyboard with showing their own // UI for input. if (state.hasEditorFocused() && shouldRestoreImeVisibility(state)) { - if (DEBUG) Slog.v(TAG, "Will show input to restore visibility"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "Will show input to restore visibility"); // Inherit the last requested IME visible state when the target window is still // focused with an editor. state.setRequestedImeVisible(true); @@ -483,7 +485,8 @@ public final class ImeVisibilityStateComputer { // There is no focus view, and this window will // be behind any soft input window, so hide the // soft input window if it is shown. - if (DEBUG) Slog.v(TAG, "Unspecified window will hide input"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, + "Unspecified window will hide input"); return new ImeVisibilityResult(STATE_HIDE_IME_NOT_ALWAYS, SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW); } @@ -495,7 +498,7 @@ public final class ImeVisibilityStateComputer { // them good context without input information being obscured // by the IME) or if running on a large screen where there // is more room for the target window + IME. - if (DEBUG) Slog.v(TAG, "Unspecified window will show input"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "Unspecified window will show input"); return new ImeVisibilityResult(STATE_SHOW_IME_IMPLICIT, SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV); } @@ -513,7 +516,8 @@ public final class ImeVisibilityStateComputer { // the WindowState, as they're already in the correct state break; } else if (isForwardNavigation) { - if (DEBUG) Slog.v(TAG, "Window asks to hide input going forward"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, + "Window asks to hide input going forward"); return new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT, SoftInputShowHideReason.HIDE_STATE_HIDDEN_FORWARD_NAV); } @@ -524,7 +528,7 @@ public final class ImeVisibilityStateComputer { // the WindowState, as they're already in the correct state break; } else if (state.hasImeFocusChanged()) { - if (DEBUG) Slog.v(TAG, "Window asks to hide input"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "Window asks to hide input"); return new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT, SoftInputShowHideReason.HIDE_ALWAYS_HIDDEN_STATE); } @@ -532,7 +536,8 @@ public final class ImeVisibilityStateComputer { case WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE: if (isForwardNavigation) { if (allowVisible) { - if (DEBUG) Slog.v(TAG, "Window asks to show input going forward"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, + "Window asks to show input going forward"); return new ImeVisibilityResult(STATE_SHOW_IME_IMPLICIT, SoftInputShowHideReason.SHOW_STATE_VISIBLE_FORWARD_NAV); } else { @@ -543,7 +548,7 @@ public final class ImeVisibilityStateComputer { } break; case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE: - if (DEBUG) Slog.v(TAG, "Window asks to always show input"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "Window asks to always show input"); if (allowVisible) { if (state.hasImeFocusChanged()) { return new ImeVisibilityResult(STATE_SHOW_IME_IMPLICIT, @@ -565,7 +570,8 @@ public final class ImeVisibilityStateComputer { // To maintain compatibility, we are now hiding the IME when we don't have // an editor upon refocusing a window. if (state.isStartInputByGainFocus()) { - if (DEBUG) Slog.v(TAG, "Same window without editor will hide input"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, + "Same window without editor will hide input"); return new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT, SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR); } @@ -579,7 +585,7 @@ public final class ImeVisibilityStateComputer { // 1) SOFT_INPUT_STATE_UNCHANGED state without an editor // 2) SOFT_INPUT_STATE_VISIBLE state without an editor // 3) SOFT_INPUT_STATE_ALWAYS_VISIBLE state without an editor - if (DEBUG) Slog.v(TAG, "Window without editor will hide input"); + ProtoLog.v(IME_VIS_STATE_COMPUTER_DEBUG, "Window without editor will hide input"); if (Flags.refactorInsetsController()) { state.setRequestedImeVisible(false); } diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index a0e543300ce7..42d0a5c4757a 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -3618,6 +3618,12 @@ public class LockSettingsService extends ILockSettings.Stub { return; } + UserInfo userInfo = mInjector.getUserManagerInternal().getUserInfo(userId); + if (userInfo != null && userInfo.isForTesting()) { + Slog.i(TAG, "Keeping escrow data for test-only user"); + return; + } + // Disable escrow token permanently on all other device/user types. Slogf.i(TAG, "Permanently disabling support for escrow tokens on user %d", userId); mSpManager.destroyEscrowData(userId); diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index f137de1b3e1d..988924d9f498 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -25,6 +25,7 @@ import static android.media.MediaRouter2.SCANNING_STATE_SCANNING_FULL; import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE; import static android.media.MediaRouter2Utils.getOriginalId; import static android.media.MediaRouter2Utils.getProviderId; + import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION; import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_DESELECT_ROUTE; @@ -63,6 +64,7 @@ import android.media.MediaRouter2Manager; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.media.SuggestedDeviceInfo; import android.os.Binder; import android.os.Bundle; import android.os.Handler; @@ -76,18 +78,21 @@ import android.util.ArrayMap; import android.util.Log; import android.util.Slog; import android.util.SparseArray; + import com.android.internal.annotations.GuardedBy; import com.android.internal.util.function.pooled.PooledLambda; import com.android.media.flags.Flags; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.statusbar.StatusBarManagerInternal; + import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -551,6 +556,36 @@ class MediaRouter2ServiceImpl { } } + public void setDeviceSuggestionsWithRouter2( + @NonNull IMediaRouter2 router, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + Objects.requireNonNull(router, "router must not be null"); + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + setDeviceSuggestionsWithRouter2Locked(router, suggestedDeviceInfo); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2( + @NonNull IMediaRouter2 router) { + Objects.requireNonNull(router, "router must not be null"); + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + return getDeviceSuggestionsWithRouter2Locked(router); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + // End of methods that implement MediaRouter2 operations. // Start of methods that implement MediaRouter2Manager operations. @@ -805,6 +840,36 @@ class MediaRouter2ServiceImpl { } } + public void setDeviceSuggestionsWithManager( + @NonNull IMediaRouter2Manager manager, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + Objects.requireNonNull(manager, "manager must not be null"); + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + setDeviceSuggestionsWithManagerLocked(manager, suggestedDeviceInfo); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManager( + @NonNull IMediaRouter2Manager manager) { + Objects.requireNonNull(manager, "manager must not be null"); + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + return getDeviceSuggestionsWithManagerLocked(manager); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS) public boolean showMediaOutputSwitcherWithProxyRouter( @NonNull IMediaRouter2Manager proxyRouter) { @@ -1582,6 +1647,61 @@ class MediaRouter2ServiceImpl { DUMMY_REQUEST_ID, routerRecord, uniqueSessionId)); } + @GuardedBy("mLock") + private void setDeviceSuggestionsWithRouter2Locked( + @NonNull IMediaRouter2 router, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + final IBinder binder = router.asBinder(); + final RouterRecord routerRecord = mAllRouterRecords.get(binder); + + if (routerRecord == null) { + Slog.w( + TAG, + TextUtils.formatSimple( + "Ignoring set device suggestion for unknown router: %s", router)); + return; + } + + Slog.i( + TAG, + TextUtils.formatSimple( + "setDeviceSuggestions | router: %d suggestion: %d", + routerRecord.mPackageName, suggestedDeviceInfo)); + + routerRecord.mUserRecord.updateDeviceSuggestionsLocked( + routerRecord.mPackageName, routerRecord.mPackageName, suggestedDeviceInfo); + routerRecord.mUserRecord.mHandler.sendMessage( + obtainMessage( + UserHandler::notifyDeviceSuggestionsUpdatedOnHandler, + routerRecord.mUserRecord.mHandler, + routerRecord.mPackageName, + routerRecord.mPackageName, + suggestedDeviceInfo)); + } + + @GuardedBy("mLock") + @Nullable + private Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2Locked( + @NonNull IMediaRouter2 router) { + final IBinder binder = router.asBinder(); + final RouterRecord routerRecord = mAllRouterRecords.get(binder); + + if (routerRecord == null) { + Slog.w( + TAG, + TextUtils.formatSimple( + "Attempted to get device suggestion for unknown router: %s", router)); + return null; + } + + Slog.i( + TAG, + TextUtils.formatSimple( + "getDeviceSuggestions | router: %d", routerRecord.mPackageName)); + + return routerRecord.mUserRecord.getDeviceSuggestionsLocked(routerRecord.mPackageName); + } + // End of locked methods that are used by MediaRouter2. // Start of locked methods that are used by MediaRouter2Manager. @@ -1972,6 +2092,68 @@ class MediaRouter2ServiceImpl { uniqueRequestId, routerRecord, uniqueSessionId)); } + @GuardedBy("mLock") + private void setDeviceSuggestionsWithManagerLocked( + @NonNull IMediaRouter2Manager manager, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + final IBinder binder = manager.asBinder(); + ManagerRecord managerRecord = mAllManagerRecords.get(binder); + + if (managerRecord == null || managerRecord.mTargetPackageName == null) { + Slog.w( + TAG, + TextUtils.formatSimple( + "Ignoring set device suggestion for unknown manager: %s", manager)); + return; + } + + Slog.i( + TAG, + TextUtils.formatSimple( + "setDeviceSuggestions | manager: %d, suggestingPackageName: %d suggestion:" + + " %d", + managerRecord.mManagerId, + managerRecord.mOwnerPackageName, + suggestedDeviceInfo)); + + managerRecord.mUserRecord.updateDeviceSuggestionsLocked( + managerRecord.mTargetPackageName, + managerRecord.mOwnerPackageName, + suggestedDeviceInfo); + managerRecord.mUserRecord.mHandler.sendMessage( + obtainMessage( + UserHandler::notifyDeviceSuggestionsUpdatedOnHandler, + managerRecord.mUserRecord.mHandler, + managerRecord.mTargetPackageName, + managerRecord.mOwnerPackageName, + suggestedDeviceInfo)); + } + + @GuardedBy("mLock") + @Nullable + private Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManagerLocked( + @NonNull IMediaRouter2Manager manager) { + final IBinder binder = manager.asBinder(); + ManagerRecord managerRecord = mAllManagerRecords.get(binder); + + if (managerRecord == null || managerRecord.mTargetPackageName == null) { + Slog.w( + TAG, + TextUtils.formatSimple( + "Attempted to get device suggestion for unknown manager: %s", manager)); + return null; + } + + Slog.i( + TAG, + TextUtils.formatSimple( + "getDeviceSuggestionsWithManagerLocked | manager: %d", + managerRecord.mManagerId)); + + return managerRecord.mUserRecord.getDeviceSuggestionsLocked( + managerRecord.mTargetPackageName); + } + // End of locked methods that are used by MediaRouter2Manager. // Start of locked methods that are used by both MediaRouter2 and MediaRouter2Manager. @@ -2047,6 +2229,11 @@ class MediaRouter2ServiceImpl { //TODO: make records private for thread-safety final ArrayList<RouterRecord> mRouterRecords = new ArrayList<>(); final ArrayList<ManagerRecord> mManagerRecords = new ArrayList<>(); + + // @GuardedBy("mLock") + private final Map<String, Map<String, List<SuggestedDeviceInfo>>> mDeviceSuggestions = + new HashMap<>(); + RouteDiscoveryPreference mCompositeDiscoveryPreference = RouteDiscoveryPreference.EMPTY; Set<String> mActivelyScanningPackages = Set.of(); final UserHandler mHandler; @@ -2076,6 +2263,25 @@ class MediaRouter2ServiceImpl { return null; } + // @GuardedBy("mLock") + public void updateDeviceSuggestionsLocked( + String packageName, + String suggestingPackageName, + List<SuggestedDeviceInfo> deviceSuggestions) { + mDeviceSuggestions.putIfAbsent( + packageName, new HashMap<String, List<SuggestedDeviceInfo>>()); + Map<String, List<SuggestedDeviceInfo>> suggestions = + mDeviceSuggestions.get(packageName); + suggestions.put(suggestingPackageName, deviceSuggestions); + } + + // @GuardedBy("mLock") + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsLocked( + String packageName) { + return mDeviceSuggestions.get(packageName); + } + public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { pw.println(prefix + "UserRecord"); @@ -2314,6 +2520,15 @@ class MediaRouter2ServiceImpl { } } + public void notifyDeviceSuggestionsUpdated( + String suggestingPackageName, List<SuggestedDeviceInfo> suggestedDeviceInfo) { + try { + mRouter.notifyDeviceSuggestionsUpdated(suggestingPackageName, suggestedDeviceInfo); + } catch (RemoteException ex) { + logRemoteException("notifyDeviceSuggestionsUpdated", ex); + } + } + /** * Sends the corresponding router a {@link RoutingSessionInfo session} creation request, * with the given {@link MediaRoute2Info} as the initial member. @@ -3556,6 +3771,41 @@ class MediaRouter2ServiceImpl { // need to update routers other than the one making the update. } + private void notifyDeviceSuggestionsUpdatedOnHandler( + String routerPackageName, + String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + MediaRouter2ServiceImpl service = mServiceRef.get(); + if (service == null) { + return; + } + List<IMediaRouter2Manager> managers = new ArrayList<>(); + synchronized (service.mLock) { + for (ManagerRecord managerRecord : mUserRecord.mManagerRecords) { + if (TextUtils.equals(managerRecord.mTargetPackageName, routerPackageName)) { + managers.add(managerRecord.mManager); + } + } + for (IMediaRouter2Manager manager : managers) { + try { + manager.notifyDeviceSuggestionsUpdated( + routerPackageName, suggestingPackageName, suggestedDeviceInfo); + } catch (RemoteException ex) { + Slog.w( + TAG, + "Failed to notify suggesteion changed. Manager probably died.", + ex); + } + } + for (RouterRecord routerRecord : mUserRecord.mRouterRecords) { + if (TextUtils.equals(routerRecord.mPackageName, routerPackageName)) { + routerRecord.notifyDeviceSuggestionsUpdated( + suggestingPackageName, suggestedDeviceInfo); + } + } + } + } + private void updateDiscoveryPreferenceOnHandler() { MediaRouter2ServiceImpl service = mServiceRef.get(); if (service == null) { diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 35bb19943a24..11f449e790a8 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -49,6 +49,7 @@ import android.media.RemoteDisplayState.RemoteDisplayInfo; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.media.SuggestedDeviceInfo; import android.os.Binder; import android.os.Bundle; import android.os.Handler; @@ -80,6 +81,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -526,6 +528,21 @@ public final class MediaRouterService extends IMediaRouterService.Stub // Binder call @Override + public void setDeviceSuggestionsWithRouter2( + IMediaRouter2 router, @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + mService2.setDeviceSuggestionsWithRouter2(router, suggestedDeviceInfo); + } + + // Binder call + @Override + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2( + IMediaRouter2 router) { + return mService2.getDeviceSuggestionsWithRouter2(router); + } + + // Binder call + @Override public List<RoutingSessionInfo> getRemoteSessions(IMediaRouter2Manager manager) { return mService2.getRemoteSessions(manager); } @@ -666,6 +683,22 @@ public final class MediaRouterService extends IMediaRouterService.Stub return mService2.showMediaOutputSwitcherWithProxyRouter(proxyRouter); } + // Binder call + @Override + public void setDeviceSuggestionsWithManager( + @NonNull IMediaRouter2Manager manager, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + mService2.setDeviceSuggestionsWithManager(manager, suggestedDeviceInfo); + } + + // Binder call + @Override + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManager( + IMediaRouter2Manager manager) { + return mService2.getDeviceSuggestionsWithManager(manager); + } + void restoreBluetoothA2dp() { try { boolean a2dpOn; diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index 91a2843ccaf7..ad108f64ffe3 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -28,6 +28,7 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.hardware.audio.effect.DefaultExtension; import android.hardware.tv.mediaquality.AmbientBacklightColorFormat; import android.hardware.tv.mediaquality.IMediaQuality; import android.hardware.tv.mediaquality.IPictureProfileAdjustmentListener; @@ -48,7 +49,6 @@ import android.media.quality.IMediaQualityManager; import android.media.quality.IPictureProfileCallback; import android.media.quality.ISoundProfileCallback; import android.media.quality.MediaQualityContract.BaseParameters; -import android.media.quality.MediaQualityManager; import android.media.quality.ParameterCapability; import android.media.quality.PictureProfile; import android.media.quality.PictureProfileHandle; @@ -58,6 +58,7 @@ import android.os.Binder; import android.os.Bundle; import android.os.Environment; import android.os.IBinder; +import android.os.Parcel; import android.os.PersistableBundle; import android.os.RemoteCallbackList; import android.os.RemoteException; @@ -187,7 +188,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public PictureProfile createPictureProfile(PictureProfile pp, UserHandle user) { + public PictureProfile createPictureProfile(PictureProfile pp, int userId) { if ((pp.getPackageName() != null && !pp.getPackageName().isEmpty() && !incomingPackageEqualsCallingUidPackage(pp.getPackageName())) && !hasGlobalPictureQualityServicePermission()) { @@ -221,7 +222,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public void updatePictureProfile(String id, PictureProfile pp, UserHandle user) { + public void updatePictureProfile(String id, PictureProfile pp, int userId) { Long dbId = mPictureProfileTempIdMap.getKey(id); if (!hasPermissionToUpdatePictureProfile(dbId, pp)) { mMqManagerNotifier.notifyOnPictureProfileError(id, @@ -249,7 +250,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public void removePictureProfile(String id, UserHandle user) { + public void removePictureProfile(String id, int userId) { synchronized (mPictureProfileLock) { Long dbId = mPictureProfileTempIdMap.getKey(id); @@ -290,10 +291,8 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public PictureProfile getPictureProfile(int type, String name, Bundle options, - UserHandle user) { - boolean includeParams = - options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false); + public PictureProfile getPictureProfile(int type, String name, boolean includeParams, + int userId) { String selection = BaseParameters.PARAMETER_TYPE + " = ? AND " + BaseParameters.PARAMETER_NAME + " = ? AND " + BaseParameters.PARAMETER_PACKAGE + " = ?"; @@ -327,7 +326,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override public List<PictureProfile> getPictureProfilesByPackage( - String packageName, Bundle options, UserHandle user) { + String packageName, boolean includeParams, int userId) { if (!hasGlobalPictureQualityServicePermission()) { mMqManagerNotifier.notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, @@ -335,8 +334,6 @@ public class MediaQualityService extends SystemService { } synchronized (mPictureProfileLock) { - boolean includeParams = - options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false); String selection = BaseParameters.PARAMETER_PACKAGE + " = ?"; String[] selectionArguments = {packageName}; return mMqDatabaseUtils.getPictureProfilesBasedOnConditions(MediaQualityUtils @@ -347,17 +344,17 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public List<PictureProfile> getAvailablePictureProfiles(Bundle options, UserHandle user) { + public List<PictureProfile> getAvailablePictureProfiles(boolean includeParams, int userId) { String packageName = getPackageOfCallingUid(); if (packageName != null) { - return getPictureProfilesByPackage(packageName, options, user); + return getPictureProfilesByPackage(packageName, includeParams, userId); } return new ArrayList<>(); } @GuardedBy("mPictureProfileLock") @Override - public boolean setDefaultPictureProfile(String profileId, UserHandle user) { + public boolean setDefaultPictureProfile(String profileId, int userId) { if (!hasGlobalPictureQualityServicePermission()) { mMqManagerNotifier.notifyOnPictureProfileError(profileId, PictureProfile.ERROR_NO_PERMISSION, @@ -370,13 +367,21 @@ public class MediaQualityService extends SystemService { try { if (mMediaQuality != null) { + PictureParameters pp = new PictureParameters(); PictureParameter[] pictureParameters = MediaQualityUtils .convertPersistableBundleToPictureParameterList(params); - PictureParameters pp = new PictureParameters(); + PersistableBundle vendorPictureParameters = params + .getPersistableBundle(BaseParameters.VENDOR_PARAMETERS); + Parcel parcel = Parcel.obtain(); + if (vendorPictureParameters != null) { + setVendorPictureParameters(pp, parcel, vendorPictureParameters); + } + pp.pictureParameters = pictureParameters; mMediaQuality.sendDefaultPictureParameters(pp); + parcel.recycle(); return true; } } catch (RemoteException e) { @@ -387,7 +392,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public List<String> getPictureProfilePackageNames(UserHandle user) { + public List<String> getPictureProfilePackageNames(int userId) { if (!hasGlobalPictureQualityServicePermission()) { mMqManagerNotifier.notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, @@ -406,7 +411,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public List<PictureProfileHandle> getPictureProfileHandle(String[] ids, UserHandle user) { + public List<PictureProfileHandle> getPictureProfileHandle(String[] ids, int userId) { List<PictureProfileHandle> toReturn = new ArrayList<>(); synchronized (mPictureProfileLock) { for (String id : ids) { @@ -423,13 +428,13 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public List<SoundProfileHandle> getSoundProfileHandle(String[] ids, UserHandle user) { + public List<SoundProfileHandle> getSoundProfileHandle(String[] ids, int userId) { List<SoundProfileHandle> toReturn = new ArrayList<>(); synchronized (mSoundProfileLock) { for (String id : ids) { Long key = mSoundProfileTempIdMap.getKey(id); if (key != null) { - toReturn.add(new SoundProfileHandle(key)); + toReturn.add(MediaQualityUtils.SOUND_PROFILE_HANDLE_NONE); } else { toReturn.add(null); } @@ -440,7 +445,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public SoundProfile createSoundProfile(SoundProfile sp, UserHandle user) { + public SoundProfile createSoundProfile(SoundProfile sp, int userId) { if ((sp.getPackageName() != null && !sp.getPackageName().isEmpty() && !incomingPackageEqualsCallingUidPackage(sp.getPackageName())) && !hasGlobalPictureQualityServicePermission()) { @@ -473,7 +478,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public void updateSoundProfile(String id, SoundProfile sp, UserHandle user) { + public void updateSoundProfile(String id, SoundProfile sp, int userId) { Long dbId = mSoundProfileTempIdMap.getKey(id); if (!hasPermissionToUpdateSoundProfile(dbId, sp)) { mMqManagerNotifier.notifyOnSoundProfileError(id, SoundProfile.ERROR_NO_PERMISSION, @@ -502,7 +507,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public void removeSoundProfile(String id, UserHandle user) { + public void removeSoundProfile(String id, int userId) { synchronized (mSoundProfileLock) { Long dbId = mSoundProfileTempIdMap.getKey(id); SoundProfile toDelete = mMqDatabaseUtils.getSoundProfile(dbId); @@ -542,10 +547,8 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public SoundProfile getSoundProfile(int type, String name, Bundle options, - UserHandle user) { - boolean includeParams = - options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false); + public SoundProfile getSoundProfile(int type, String name, boolean includeParams, + int userId) { String selection = BaseParameters.PARAMETER_TYPE + " = ? AND " + BaseParameters.PARAMETER_NAME + " = ? AND " + BaseParameters.PARAMETER_PACKAGE + " = ?"; @@ -579,15 +582,13 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override public List<SoundProfile> getSoundProfilesByPackage( - String packageName, Bundle options, UserHandle user) { + String packageName, boolean includeParams, int userId) { if (!hasGlobalSoundQualityServicePermission()) { mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } synchronized (mSoundProfileLock) { - boolean includeParams = - options.getBoolean(MediaQualityManager.OPTION_INCLUDE_PARAMETERS, false); String selection = BaseParameters.PARAMETER_PACKAGE + " = ?"; String[] selectionArguments = {packageName}; return mMqDatabaseUtils.getSoundProfilesBasedOnConditions(MediaQualityUtils @@ -598,17 +599,17 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public List<SoundProfile> getAvailableSoundProfiles(Bundle options, UserHandle user) { + public List<SoundProfile> getAvailableSoundProfiles(boolean includeParams, int userId) { String packageName = getPackageOfCallingUid(); if (packageName != null) { - return getSoundProfilesByPackage(packageName, options, user); + return getSoundProfilesByPackage(packageName, includeParams, userId); } return new ArrayList<>(); } @GuardedBy("mSoundProfileLock") @Override - public boolean setDefaultSoundProfile(String profileId, UserHandle user) { + public boolean setDefaultSoundProfile(String profileId, int userId) { if (!hasGlobalSoundQualityServicePermission()) { mMqManagerNotifier.notifyOnSoundProfileError(profileId, SoundProfile.ERROR_NO_PERMISSION, @@ -638,7 +639,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public List<String> getSoundProfilePackageNames(UserHandle user) { + public List<String> getSoundProfilePackageNames(int userId) { if (!hasGlobalSoundQualityServicePermission()) { mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); @@ -737,7 +738,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mAmbientBacklightLock") @Override public void setAmbientBacklightSettings( - AmbientBacklightSettings settings, UserHandle user) { + AmbientBacklightSettings settings, int userId) { if (DEBUG) { Slogf.d(TAG, "setAmbientBacklightSettings " + settings); } @@ -775,7 +776,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mAmbientBacklightLock") @Override - public void setAmbientBacklightEnabled(boolean enabled, UserHandle user) { + public void setAmbientBacklightEnabled(boolean enabled, int userId) { if (DEBUG) { Slogf.d(TAG, "setAmbientBacklightEnabled " + enabled); } @@ -795,7 +796,7 @@ public class MediaQualityService extends SystemService { @Override public List<ParameterCapability> getParameterCapabilities( - List<String> names, UserHandle user) { + List<String> names, int userId) { byte[] byteArray = MediaQualityUtils.convertParameterToByteArray(names); ParamCapability[] caps = new ParamCapability[byteArray.length]; try { @@ -828,7 +829,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public List<String> getPictureProfileAllowList(UserHandle user) { + public List<String> getPictureProfileAllowList(int userId) { if (!hasGlobalPictureQualityServicePermission()) { mMqManagerNotifier.notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, @@ -844,7 +845,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public void setPictureProfileAllowList(List<String> packages, UserHandle user) { + public void setPictureProfileAllowList(List<String> packages, int userId) { if (!hasGlobalPictureQualityServicePermission()) { mMqManagerNotifier.notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, @@ -857,7 +858,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public List<String> getSoundProfileAllowList(UserHandle user) { + public List<String> getSoundProfileAllowList(int userId) { if (!hasGlobalSoundQualityServicePermission()) { mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); @@ -872,7 +873,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public void setSoundProfileAllowList(List<String> packages, UserHandle user) { + public void setSoundProfileAllowList(List<String> packages, int userId) { if (!hasGlobalSoundQualityServicePermission()) { mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); @@ -883,13 +884,13 @@ public class MediaQualityService extends SystemService { } @Override - public boolean isSupported(UserHandle user) { + public boolean isSupported(int userId) { return false; } @GuardedBy("mPictureProfileLock") @Override - public void setAutoPictureQualityEnabled(boolean enabled, UserHandle user) { + public void setAutoPictureQualityEnabled(boolean enabled, int userId) { if (!hasGlobalPictureQualityServicePermission()) { mMqManagerNotifier.notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, @@ -910,7 +911,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public boolean isAutoPictureQualityEnabled(UserHandle user) { + public boolean isAutoPictureQualityEnabled(int userId) { synchronized (mPictureProfileLock) { try { if (mMediaQuality != null) { @@ -927,7 +928,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public void setSuperResolutionEnabled(boolean enabled, UserHandle user) { + public void setSuperResolutionEnabled(boolean enabled, int userId) { if (!hasGlobalPictureQualityServicePermission()) { mMqManagerNotifier.notifyOnPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, @@ -948,7 +949,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mPictureProfileLock") @Override - public boolean isSuperResolutionEnabled(UserHandle user) { + public boolean isSuperResolutionEnabled(int userId) { synchronized (mPictureProfileLock) { try { if (mMediaQuality != null) { @@ -965,7 +966,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public void setAutoSoundQualityEnabled(boolean enabled, UserHandle user) { + public void setAutoSoundQualityEnabled(boolean enabled, int userId) { if (!hasGlobalSoundQualityServicePermission()) { mMqManagerNotifier.notifyOnSoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); @@ -986,7 +987,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mSoundProfileLock") @Override - public boolean isAutoSoundQualityEnabled(UserHandle user) { + public boolean isAutoSoundQualityEnabled(int userId) { synchronized (mSoundProfileLock) { try { if (mMediaQuality != null) { @@ -1003,7 +1004,7 @@ public class MediaQualityService extends SystemService { @GuardedBy("mAmbientBacklightLock") @Override - public boolean isAmbientBacklightEnabled(UserHandle user) { + public boolean isAmbientBacklightEnabled(int userId) { return false; } } @@ -1428,11 +1429,19 @@ public class MediaQualityService extends SystemService { MediaQualityUtils.convertPersistableBundleToPictureParameterList( params); + PersistableBundle vendorPictureParameters = params + .getPersistableBundle(BaseParameters.VENDOR_PARAMETERS); + Parcel parcel = Parcel.obtain(); + if (vendorPictureParameters != null) { + setVendorPictureParameters(pictureParameters, parcel, vendorPictureParameters); + } + android.hardware.tv.mediaquality.PictureProfile toReturn = new android.hardware.tv.mediaquality.PictureProfile(); toReturn.pictureProfileId = id; toReturn.parameters = pictureParameters; + parcel.recycle(); return toReturn; } @@ -1738,4 +1747,16 @@ public class MediaQualityService extends SystemService { return android.hardware.tv.mediaquality.IMediaQualityCallback.Stub.VERSION; } } + + private void setVendorPictureParameters( + PictureParameters pictureParameters, + Parcel parcel, + PersistableBundle vendorPictureParameters) { + vendorPictureParameters.writeToParcel(parcel, 0); + byte[] vendorBundleToByteArray = parcel.marshall(); + DefaultExtension defaultExtension = new DefaultExtension(); + defaultExtension.bytes = Arrays.copyOf( + vendorBundleToByteArray, vendorBundleToByteArray.length); + pictureParameters.vendorPictureParameters.setParcelable(defaultExtension); + } } diff --git a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java index 88d3f1ff7c52..303c96750098 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java @@ -60,6 +60,11 @@ public final class MediaQualityUtils { private static final String TAG = "MediaQualityUtils"; public static final String SETTINGS = "settings"; + public static final SoundProfileHandle SOUND_PROFILE_HANDLE_NONE = new SoundProfileHandle(); + static { + SOUND_PROFILE_HANDLE_NONE.id = -10000; + } + /** * Convert PictureParameter List to PersistableBundle. */ @@ -1022,7 +1027,7 @@ public final class MediaQualityUtils { getInputId(cursor), getPackageName(cursor), jsonToPersistableBundle(getSettingsString(cursor)), - SoundProfileHandle.NONE + SOUND_PROFILE_HANDLE_NONE ); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index bfe0d32f4cb6..7a544cf1c26c 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -1075,6 +1075,7 @@ public class NotificationManagerService extends SystemService { summary.getSbn().getNotification().getGroupAlertBehavior(); if (notificationForceGrouping()) { + summary.getNotification().flags |= Notification.FLAG_SILENT; if (!summary.getChannel().getId().equals(summaryAttr.channelId)) { NotificationChannel newChannel = mPreferencesHelper.getNotificationChannel(pkg, summary.getUid(), summaryAttr.channelId, false); @@ -7450,6 +7451,7 @@ public class NotificationManagerService extends SystemService { // Override group key early for forced grouped notifications r.setOverrideGroupKey(groupName); } + r.getNotification().flags |= Notification.FLAG_SILENT; } addAutoGroupAdjustment(r, groupName); diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java index ee29849465e2..737d943f084d 100644 --- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java +++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java @@ -308,7 +308,9 @@ final class OverlayManagerServiceImpl { Slog.d(TAG, "onPackageRemoved pkgName=" + pkgName + " userId=" + userId); } // Update the state of all overlays that target this package. - final Set<UserPackage> targets = updateOverlaysForTarget(pkgName, userId, 0 /* flags */); + Set<UserPackage> targets = Collections.emptySet(); + targets = CollectionUtils.addAll(targets, + updateOverlaysForTarget(pkgName, userId, 0 /* flags */)); // Remove all the overlays this package declares. return CollectionUtils.addAll(targets, diff --git a/services/core/java/com/android/server/os/instrumentation/OWNERS b/services/core/java/com/android/server/os/instrumentation/OWNERS new file mode 100644 index 000000000000..2522426d93f8 --- /dev/null +++ b/services/core/java/com/android/server/os/instrumentation/OWNERS @@ -0,0 +1 @@ +include platform/packages/modules/UprobeStats:/OWNERS
\ No newline at end of file diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java index 734920435e26..3361dbc2df07 100644 --- a/services/core/java/com/android/server/pm/InstallRequest.java +++ b/services/core/java/com/android/server/pm/InstallRequest.java @@ -644,20 +644,6 @@ final class InstallRequest { return mScanResult.mPkgSetting; } - @Nullable - public PackageSetting getRealPackageSetting() { - // TODO: Fix this to have 1 mutable PackageSetting for scan/install. If the previous - // setting needs to be passed to have a comparison, hide it behind an immutable - // interface. There's no good reason to have 3 different ways to access the real - // PackageSetting object, only one of which is actually correct. - PackageSetting realPkgSetting = isExistingSettingCopied() - ? getScanRequestPackageSetting() : getScannedPackageSetting(); - if (realPkgSetting == null) { - realPkgSetting = getScannedPackageSetting(); - } - return realPkgSetting; - } - public boolean isExistingSettingCopied() { assertScanResultExists(); return mScanResult.mExistingSettingCopied; diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 93837b34f7b3..092ec8ef4a8a 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -7490,6 +7490,7 @@ public class UserManagerService extends IUserManager.Stub { final long now = System.currentTimeMillis(); final long nowRealtime = SystemClock.elapsedRealtime(); final StringBuilder sb = new StringBuilder(); + final Resources resources = Resources.getSystem(); if (args != null && args.length > 0) { switch (args[0]) { @@ -7570,10 +7571,15 @@ public class UserManagerService extends IUserManager.Stub { // Dump some capabilities pw.println(); - pw.print(" Max users: " + UserManager.getMaxSupportedUsers()); + int effectiveMaxSupportedUsers = UserManager.getMaxSupportedUsers(); + pw.print(" Max users: " + effectiveMaxSupportedUsers); + int defaultMaxSupportedUsers = resources.getInteger(R.integer.config_multiuserMaximumUsers); + if (effectiveMaxSupportedUsers != defaultMaxSupportedUsers) { + pw.print(" (built-in value: " + defaultMaxSupportedUsers + ")"); + } pw.println(" (limit reached: " + isUserLimitReached() + ")"); pw.println(" Supports switchable users: " + UserManager.supportsMultipleUsers()); - pw.println(" All guests ephemeral: " + Resources.getSystem().getBoolean( + pw.println(" All guests ephemeral: " + resources.getBoolean( com.android.internal.R.bool.config_guestUserEphemeral)); pw.println(" Force ephemeral users: " + mForceEphemeralUsers); final boolean isHeadlessSystemUserMode = isHeadlessSystemUserMode(); @@ -7588,7 +7594,7 @@ public class UserManagerService extends IUserManager.Stub { } } if (isHeadlessSystemUserMode) { - pw.println(" Can switch to headless system user: " + Resources.getSystem() + pw.println(" Can switch to headless system user: " + resources .getBoolean(com.android.internal.R.bool.config_canSwitchToHeadlessSystemUser)); } pw.println(" User version: " + mUserVersion); diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java index 58c5b1c90a66..5798aa919d96 100644 --- a/services/core/java/com/android/server/pm/UserTypeFactory.java +++ b/services/core/java/com/android/server/pm/UserTypeFactory.java @@ -36,6 +36,7 @@ import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; import static android.os.UserManager.USER_TYPE_PROFILE_COMMUNAL; import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; +import static android.os.UserManager.USER_TYPE_PROFILE_SUPERVISING; import static android.os.UserManager.USER_TYPE_PROFILE_TEST; import static android.os.UserManager.USER_TYPE_SYSTEM_HEADLESS; @@ -111,6 +112,7 @@ public final class UserTypeFactory { builders.put(USER_TYPE_PROFILE_CLONE, getDefaultTypeProfileClone()); builders.put(USER_TYPE_PROFILE_COMMUNAL, getDefaultTypeProfileCommunal()); builders.put(USER_TYPE_PROFILE_PRIVATE, getDefaultTypeProfilePrivate()); + builders.put(USER_TYPE_PROFILE_SUPERVISING, getDefaultTypeProfileSupervising()); if (Build.IS_DEBUGGABLE) { builders.put(USER_TYPE_PROFILE_TEST, getDefaultTypeProfileTest()); } @@ -343,6 +345,29 @@ public final class UserTypeFactory { } /** + * Returns the Builder for the default {@link UserManager#USER_TYPE_PROFILE_SUPERVISING} + * configuration. + */ + private static UserTypeDetails.Builder getDefaultTypeProfileSupervising() { + return new UserTypeDetails.Builder() + .setName(USER_TYPE_PROFILE_SUPERVISING) + .setBaseType(FLAG_PROFILE) + .setMaxAllowed(1) + .setProfileParentRequired(false) + .setEnabled(android.multiuser.Flags.allowSupervisingProfile() ? 1 : 0) + .setLabels(R.string.profile_label_supervising) + .setDefaultRestrictions(getDefaultSupervisingProfileRestrictions()) + .setDefaultSecureSettings(getDefaultNonManagedProfileSecureSettings()) + .setDefaultUserProperties(new UserProperties.Builder() + .setStartWithParent(false) + .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_NO) + .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_NO) + .setShowInQuietMode(UserProperties.SHOW_IN_QUIET_MODE_HIDDEN) + .setCredentialShareableWithParent(false) + .setAlwaysVisible(true)); + } + + /** * Returns the Builder for the default {@link UserManager#USER_TYPE_FULL_SECONDARY} * configuration. */ @@ -449,6 +474,12 @@ public final class UserTypeFactory { return restrictions; } + private static Bundle getDefaultSupervisingProfileRestrictions() { + final Bundle restrictions = getDefaultProfileRestrictions(); + restrictions.putBoolean(UserManager.DISALLOW_INSTALL_APPS, true); + return restrictions; + } + private static Bundle getDefaultManagedProfileSecureSettings() { // Only add String values to the bundle, settings are written as Strings eventually final Bundle settings = new Bundle(); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 46dc75817a36..3230e891db55 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -4240,66 +4240,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (!useKeyGestureEventHandler()) { return; } - mInputManager.registerKeyGestureEventHandler(new InputManager.KeyGestureEventHandler() { - @Override - public boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event, - @Nullable IBinder focusedToken) { - boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event, - focusedToken); - if (handled && !event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch( - (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) { - mPowerKeyHandled = true; - } - return handled; - } - - @Override - public boolean isKeyGestureSupported(int gestureType) { - switch (gestureType) { - case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS: - case KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH: - case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT: - case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT: - case KeyGestureEvent.KEY_GESTURE_TYPE_HOME: - case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS: - case KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL: - case KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT: - case KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT: - case KeyGestureEvent.KEY_GESTURE_TYPE_BACK: - case KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION: - case KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE: - case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT: - case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT: - case KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER: - case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP: - case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN: - case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER: - case KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS: - case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS: - case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH: - case KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH: - case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT: - case KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS: - case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB: - case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD: - case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD: - case KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS: - case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK: - case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS: - return true; - case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD: - return mAccessibilityShortcutController.isAccessibilityShortcutAvailable( - isKeyguardLocked()); - case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD: - return mAccessibilityShortcutController.isAccessibilityShortcutAvailable( - false); - default: - return false; - } + mInputManager.registerKeyGestureEventHandler((event, focusedToken) -> { + boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event, + focusedToken); + if (handled && !event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch( + (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) { + mPowerKeyHandled = true; } + return handled; }); } @@ -4457,13 +4405,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { cancelPendingScreenshotChordAction(); } return true; - case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD: - if (start) { - interceptAccessibilityShortcutChord(); - } else { - cancelPendingAccessibilityShortcutAction(); - } - return true; case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD: if (start) { interceptRingerToggleChord(); @@ -4481,14 +4422,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { cancelGlobalActionsAction(); } return true; - // TODO (b/358569822): Consolidate TV and non-TV gestures into same KeyGestureEvent - case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD: - if (start) { - interceptAccessibilityGestureTv(); - } else { - cancelAccessibilityGestureTv(); - } - return true; case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT: if (start) { interceptBugreportGestureTv(); diff --git a/services/core/java/com/android/server/power/ScreenUndimDetector.java b/services/core/java/com/android/server/power/ScreenUndimDetector.java index c4929c210e2c..b376417061db 100644 --- a/services/core/java/com/android/server/power/ScreenUndimDetector.java +++ b/services/core/java/com/android/server/power/ScreenUndimDetector.java @@ -30,11 +30,11 @@ import android.provider.DeviceConfig; import android.util.Slog; import android.view.Display; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FrameworkStatsLog; import java.util.Set; -import java.util.concurrent.TimeUnit; /** * Detects when user manually undims the screen (x times) and acquires a wakelock to keep the screen @@ -48,7 +48,6 @@ public class ScreenUndimDetector { /** DeviceConfig flag: is keep screen on feature enabled. */ static final String KEY_KEEP_SCREEN_ON_ENABLED = "keep_screen_on_enabled"; - private static final boolean DEFAULT_KEEP_SCREEN_ON_ENABLED = true; private static final int OUTCOME_POWER_BUTTON = FrameworkStatsLog.TIMEOUT_AUTO_EXTENDED_REPORTED__OUTCOME__POWER_BUTTON; private static final int OUTCOME_TIMEOUT = @@ -58,15 +57,11 @@ public class ScreenUndimDetector { /** DeviceConfig flag: how long should we keep the screen on. */ @VisibleForTesting static final String KEY_KEEP_SCREEN_ON_FOR_MILLIS = "keep_screen_on_for_millis"; - @VisibleForTesting - static final long DEFAULT_KEEP_SCREEN_ON_FOR_MILLIS = TimeUnit.MINUTES.toMillis(10); private long mKeepScreenOnForMillis; /** DeviceConfig flag: how many user undims required to trigger keeping the screen on. */ @VisibleForTesting static final String KEY_UNDIMS_REQUIRED = "undims_required"; - @VisibleForTesting - static final int DEFAULT_UNDIMS_REQUIRED = 2; private int mUndimsRequired; /** @@ -76,8 +71,6 @@ public class ScreenUndimDetector { @VisibleForTesting static final String KEY_MAX_DURATION_BETWEEN_UNDIMS_MILLIS = "max_duration_between_undims_millis"; - @VisibleForTesting - static final long DEFAULT_MAX_DURATION_BETWEEN_UNDIMS_MILLIS = TimeUnit.MINUTES.toMillis(5); private long mMaxDurationBetweenUndimsMillis; @VisibleForTesting @@ -92,6 +85,7 @@ public class ScreenUndimDetector { private long mUndimOccurredTime = -1; private long mInteractionAfterUndimTime = -1; private InternalClock mClock; + private Context mContext; public ScreenUndimDetector() { mClock = new InternalClock(); @@ -109,12 +103,13 @@ public class ScreenUndimDetector { /** Should be called in parent's systemReady() */ public void systemReady(Context context) { + mContext = context; readValuesFromDeviceConfig(); DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_ATTENTION_MANAGER_SERVICE, - context.getMainExecutor(), + mContext.getMainExecutor(), (properties) -> onDeviceConfigChange(properties.getKeyset())); - final PowerManager powerManager = context.getSystemService(PowerManager.class); + final PowerManager powerManager = mContext.getSystemService(PowerManager.class); mWakeLock = powerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, UNDIM_DETECTOR_WAKE_LOCK); @@ -203,36 +198,44 @@ public class ScreenUndimDetector { } } - private boolean readKeepScreenOnNotificationEnabled() { + private boolean readKeepScreenOnEnabled() { + boolean defaultKeepScreenOnEnabled = mContext.getResources().getBoolean( + R.bool.config_defaultPreventScreenTimeoutEnabled); return DeviceConfig.getBoolean(NAMESPACE_ATTENTION_MANAGER_SERVICE, KEY_KEEP_SCREEN_ON_ENABLED, - DEFAULT_KEEP_SCREEN_ON_ENABLED); + defaultKeepScreenOnEnabled); } private long readKeepScreenOnForMillis() { + long defaultKeepScreenOnDuration = mContext.getResources().getInteger( + R.integer.config_defaultPreventScreenTimeoutForMillis); return DeviceConfig.getLong(NAMESPACE_ATTENTION_MANAGER_SERVICE, KEY_KEEP_SCREEN_ON_FOR_MILLIS, - DEFAULT_KEEP_SCREEN_ON_FOR_MILLIS); + defaultKeepScreenOnDuration); } private int readUndimsRequired() { + int defaultUndimsRequired = mContext.getResources().getInteger( + R.integer.config_defaultUndimsRequired); int undimsRequired = DeviceConfig.getInt(NAMESPACE_ATTENTION_MANAGER_SERVICE, KEY_UNDIMS_REQUIRED, - DEFAULT_UNDIMS_REQUIRED); + defaultUndimsRequired); if (undimsRequired < 1 || undimsRequired > 5) { Slog.e(TAG, "Provided undimsRequired=" + undimsRequired - + " is not allowed [1, 5]; using the default=" + DEFAULT_UNDIMS_REQUIRED); - return DEFAULT_UNDIMS_REQUIRED; + + " is not allowed [1, 5]; using the default=" + defaultUndimsRequired); + return defaultUndimsRequired; } return undimsRequired; } private long readMaxDurationBetweenUndimsMillis() { + long defaultMaxDurationBetweenUndimsMillis = mContext.getResources().getInteger( + R.integer.config_defaultMaxDurationBetweenUndimsMillis); return DeviceConfig.getLong(NAMESPACE_ATTENTION_MANAGER_SERVICE, KEY_MAX_DURATION_BETWEEN_UNDIMS_MILLIS, - DEFAULT_MAX_DURATION_BETWEEN_UNDIMS_MILLIS); + defaultMaxDurationBetweenUndimsMillis); } private void onDeviceConfigChange(@NonNull Set<String> keys) { @@ -253,15 +256,16 @@ public class ScreenUndimDetector { @VisibleForTesting void readValuesFromDeviceConfig() { - mKeepScreenOnEnabled = readKeepScreenOnNotificationEnabled(); + mKeepScreenOnEnabled = readKeepScreenOnEnabled(); mKeepScreenOnForMillis = readKeepScreenOnForMillis(); mUndimsRequired = readUndimsRequired(); mMaxDurationBetweenUndimsMillis = readMaxDurationBetweenUndimsMillis(); Slog.i(TAG, "readValuesFromDeviceConfig():" + "\nmKeepScreenOnForMillis=" + mKeepScreenOnForMillis - + "\nmKeepScreenOnNotificationEnabled=" + mKeepScreenOnEnabled - + "\nmUndimsRequired=" + mUndimsRequired); + + "\nmKeepScreenOnEnabled=" + mKeepScreenOnEnabled + + "\nmUndimsRequired=" + mUndimsRequired + + "\nmMaxDurationBetweenUndimsMillis=" + mMaxDurationBetweenUndimsMillis); } diff --git a/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java index 5563f98e8842..7cd9bdbc662c 100644 --- a/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java +++ b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java @@ -374,6 +374,10 @@ public class BatteryHistoryDirectory implements BatteryStatsHistory.BatteryHisto @SuppressWarnings("unchecked") @Override public List<BatteryHistoryFragment> getFragments() { + if (!mLock.isHeldByCurrentThread()) { + throw new IllegalStateException("Reading battery history without a lock"); + } + ensureInitialized(); return (List<BatteryHistoryFragment>) (List<? extends BatteryHistoryFragment>) mHistoryFiles; @@ -443,44 +447,6 @@ public class BatteryHistoryDirectory implements BatteryStatsHistory.BatteryHisto } @Override - public BatteryHistoryFragment getNextFragment(BatteryHistoryFragment current, long startTimeMs, - long endTimeMs) { - ensureInitialized(); - - if (!mLock.isHeldByCurrentThread()) { - throw new IllegalStateException("Iterating battery history without a lock"); - } - - int nextFileIndex = 0; - int firstFileIndex = 0; - // skip the last file because its data is in history buffer. - int lastFileIndex = mHistoryFiles.size() - 2; - for (int i = lastFileIndex; i >= 0; i--) { - BatteryHistoryFragment fragment = mHistoryFiles.get(i); - if (current != null && fragment.monotonicTimeMs == current.monotonicTimeMs) { - nextFileIndex = i + 1; - } - if (fragment.monotonicTimeMs > endTimeMs) { - lastFileIndex = i - 1; - } - if (fragment.monotonicTimeMs <= startTimeMs) { - firstFileIndex = i; - break; - } - } - - if (nextFileIndex < firstFileIndex) { - nextFileIndex = firstFileIndex; - } - - if (nextFileIndex <= lastFileIndex) { - return mHistoryFiles.get(nextFileIndex); - } - - return null; - } - - @Override public boolean hasCompletedFragments() { ensureInitialized(); diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java index 54365ff03db0..c5a43a57da82 100644 --- a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java @@ -72,14 +72,20 @@ class SelinuxAuditLogsCollector { } SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) { - this( - () -> - DeviceConfig.getString( - DeviceConfig.NAMESPACE_ADSERVICES, - CONFIG_SELINUX_AUDIT_DOMAIN, - DEFAULT_SELINUX_AUDIT_DOMAIN), - rateLimiter, - quotaLimiter); + this(new DefaultDomainSupplier(), rateLimiter, quotaLimiter); + } + + private static class DefaultDomainSupplier implements Supplier<String> { + @Override + public String get() { + if (SelinuxAuditLogsService.enabledForAllDomains()) { + return "\\w+"; + } + return DeviceConfig.getString( + DeviceConfig.NAMESPACE_ADSERVICES, + CONFIG_SELINUX_AUDIT_DOMAIN, + DEFAULT_SELINUX_AUDIT_DOMAIN); + } } public void setStopRequested(boolean stopRequested) { diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java index d46e8916d9e9..9dc457c5d63b 100644 --- a/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java @@ -16,6 +16,7 @@ package com.android.server.selinux; import static com.android.sdksandbox.flags.Flags.selinuxSdkSandboxAudit; +import static com.android.server.selinux.flags.Flags.selinuxLogsCollect; import android.app.job.JobInfo; import android.app.job.JobParameters; @@ -49,6 +50,9 @@ public class SelinuxAuditLogsService extends JobService { "selinux_audit_job_frequency_hours"; private static final String CONFIG_SELINUX_ENABLE_AUDIT_JOB = "selinux_enable_audit_job"; private static final String CONFIG_SELINUX_AUDIT_CAP = "selinux_audit_cap"; + private static final String DEVICE_CONFIG_SECURITY_NAMESPACE = "security"; + private static final String CONFIG_SECURITY_SELINUX_AUDIT_JOB_ENABLED = + "selinux_audit_job_enabled"; private static final int MAX_PERMITS_CAP_DEFAULT = 50000; private static final int SELINUX_AUDIT_JOB_ID = 25327386; @@ -76,7 +80,7 @@ public class SelinuxAuditLogsService extends JobService { /** Schedule jobs with the {@link JobScheduler}. */ public static void schedule(Context context) { - if (!selinuxSdkSandboxAudit()) { + if (!selinuxSdkSandboxAudit() && !enabledForAllDomains()) { Slog.d(TAG, "SelinuxAuditLogsService not enabled"); return; } @@ -86,13 +90,20 @@ public class SelinuxAuditLogsService extends JobService { return; } - LogsCollectorJobScheduler propertiesListener = + LogsCollectorJobScheduler scheduler = new LogsCollectorJobScheduler( context.getSystemService(JobScheduler.class) .forNamespace(SELINUX_AUDIT_NAMESPACE)); - propertiesListener.schedule(); + scheduler.schedule(); + + AdServicesPropertyMonitor adServicesProperties = new AdServicesPropertyMonitor(scheduler); + DeviceConfig.addOnPropertiesChangedListener( + DeviceConfig.NAMESPACE_ADSERVICES, context.getMainExecutor(), adServicesProperties); + + SecurityPropertyMonitor securityProperties = new SecurityPropertyMonitor(scheduler); DeviceConfig.addOnPropertiesChangedListener( - DeviceConfig.NAMESPACE_ADSERVICES, context.getMainExecutor(), propertiesListener); + DEVICE_CONFIG_SECURITY_NAMESPACE, context.getMainExecutor(), securityProperties); + } @Override @@ -101,7 +112,7 @@ public class SelinuxAuditLogsService extends JobService { Slog.e(TAG, "The job id does not match the expected selinux job id."); return false; } - if (!selinuxSdkSandboxAudit()) { + if (!selinuxSdkSandboxAudit() && !enabledForAllDomains()) { Slog.i(TAG, "Selinux audit job disabled."); return false; } @@ -123,17 +134,33 @@ public class SelinuxAuditLogsService extends JobService { return false; } - /** - * This class is in charge of scheduling the job service, and keeping the scheduling up to date - * when the parameters change. - */ - private static final class LogsCollectorJobScheduler + /** Checks if the service is enabled for all domains */ + public static final boolean enabledForAllDomains() { + if (selinuxLogsCollect()) { + return DeviceConfig.getBoolean( + DEVICE_CONFIG_SECURITY_NAMESPACE, + CONFIG_SECURITY_SELINUX_AUDIT_JOB_ENABLED, + false); + } + return false; + } + + /** Checks if the service is enabled for SDK Sandbox */ + public static final boolean enabledForSdkSandbox() { + if (selinuxSdkSandboxAudit()) { + return DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_ADSERVICES, CONFIG_SELINUX_ENABLE_AUDIT_JOB, false); + } + return false; + } + + private static final class AdServicesPropertyMonitor implements DeviceConfig.OnPropertiesChangedListener { - private final JobScheduler mJobScheduler; + private final LogsCollectorJobScheduler mScheduler; - private LogsCollectorJobScheduler(JobScheduler jobScheduler) { - mJobScheduler = jobScheduler; + private AdServicesPropertyMonitor(LogsCollectorJobScheduler scheduler) { + mScheduler = scheduler; } @Override @@ -149,19 +176,65 @@ public class SelinuxAuditLogsService extends JobService { if (keyset.contains(CONFIG_SELINUX_ENABLE_AUDIT_JOB)) { boolean enabled = changedProperties.getBoolean( - CONFIG_SELINUX_ENABLE_AUDIT_JOB, /* defaultValue= */ false); + CONFIG_SELINUX_ENABLE_AUDIT_JOB, /* defaultValue= */ false) + || enabledForAllDomains(); if (enabled) { - schedule(); + mScheduler.schedule(); } else { - mJobScheduler.cancel(SELINUX_AUDIT_JOB_ID); + mScheduler.cancel(); } } else if (keyset.contains(CONFIG_SELINUX_AUDIT_JOB_FREQUENCY_HOURS)) { // The job frequency changed, reschedule. - schedule(); + mScheduler.schedule(); } } + } + + private static final class SecurityPropertyMonitor + implements DeviceConfig.OnPropertiesChangedListener { + + private final LogsCollectorJobScheduler mScheduler; + + private SecurityPropertyMonitor(LogsCollectorJobScheduler scheduler) { + mScheduler = scheduler; + } + + @Override + public void onPropertiesChanged(Properties changedProperties) { + Set<String> keyset = changedProperties.getKeyset(); + + if (keyset.contains(CONFIG_SECURITY_SELINUX_AUDIT_JOB_ENABLED)) { + boolean enabled = + changedProperties.getBoolean( + CONFIG_SECURITY_SELINUX_AUDIT_JOB_ENABLED, + /* defaultValue= */ false) + || enabledForSdkSandbox(); + if (enabled) { + mScheduler.schedule(); + } else { + mScheduler.cancel(); + } + } + } + } + + /** + * This class is in charge of scheduling the job service, and keeping the scheduling up to date + * when the parameters change. + */ + private static final class LogsCollectorJobScheduler { + + private final JobScheduler mJobScheduler; + + private LogsCollectorJobScheduler(JobScheduler jobScheduler) { + mJobScheduler = jobScheduler; + } + + public void cancel() { + mJobScheduler.cancel(SELINUX_AUDIT_JOB_ID); + } - private void schedule() { + public void schedule() { long frequencyMillis = TimeUnit.HOURS.toMillis( DeviceConfig.getInt( diff --git a/services/core/java/com/android/server/selinux/flags.aconfig b/services/core/java/com/android/server/selinux/flags.aconfig new file mode 100644 index 000000000000..3bb5a6bda1de --- /dev/null +++ b/services/core/java/com/android/server/selinux/flags.aconfig @@ -0,0 +1,9 @@ +package: "com.android.server.selinux.flags" +container: "system" + +flag { + name: "selinux_logs_collect" + namespace: "network_security" + description: "Enable collection of SELinux denials based on selinux_audit_job_enabled" + bug: "372950125" +} diff --git a/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java index 31348cd9156f..17980c02502f 100644 --- a/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java +++ b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java @@ -177,6 +177,7 @@ public final class TextClassificationManagerService extends ITextClassifierServi private final String mDefaultTextClassifierPackage; @Nullable private final String mSystemTextClassifierPackage; + private final MyPackageMonitor mPackageMonitor; private TextClassificationManagerService(Context context) { mContext = Objects.requireNonNull(context); @@ -187,50 +188,50 @@ public final class TextClassificationManagerService extends ITextClassifierServi mDefaultTextClassifierPackage = packageManager.getDefaultTextClassifierPackageName(); mSystemTextClassifierPackage = packageManager.getSystemTextClassifierPackageName(); mSessionCache = new SessionCache(mLock); + mPackageMonitor = new MyPackageMonitor(); } private void startListenSettings() { mSettingsListener.registerObserver(); } - void startTrackingPackageChanges() { - final PackageMonitor monitor = new PackageMonitor() { - - @Override - public void onPackageAdded(String packageName, int uid) { - notifyPackageInstallStatusChange(packageName, /* installed*/ true); - } + private class MyPackageMonitor extends PackageMonitor { + @Override + public void onPackageAdded(String packageName, int uid) { + notifyPackageInstallStatusChange(packageName, /* installed*/ true); + } - @Override - public void onPackageRemoved(String packageName, int uid) { - notifyPackageInstallStatusChange(packageName, /* installed= */ false); - } + @Override + public void onPackageRemoved(String packageName, int uid) { + notifyPackageInstallStatusChange(packageName, /* installed= */ false); + } - @Override - public void onPackageModified(String packageName) { - final int userId = getChangingUserId(); - synchronized (mLock) { - final UserState userState = getUserStateLocked(userId); - final ServiceState serviceState = userState.getServiceStateLocked(packageName); - if (serviceState != null) { - serviceState.onPackageModifiedLocked(); - } + @Override + public void onPackageModified(String packageName) { + final int userId = getChangingUserId(); + synchronized (mLock) { + final UserState userState = getUserStateLocked(userId); + final ServiceState serviceState = userState.getServiceStateLocked(packageName); + if (serviceState != null) { + serviceState.onPackageModifiedLocked(); } } + } - private void notifyPackageInstallStatusChange(String packageName, boolean installed) { - final int userId = getChangingUserId(); - synchronized (mLock) { - final UserState userState = getUserStateLocked(userId); - final ServiceState serviceState = userState.getServiceStateLocked(packageName); - if (serviceState != null) { - serviceState.onPackageInstallStatusChangeLocked(installed); - } + private void notifyPackageInstallStatusChange(String packageName, boolean installed) { + final int userId = getChangingUserId(); + synchronized (mLock) { + final UserState userState = getUserStateLocked(userId); + final ServiceState serviceState = userState.getServiceStateLocked(packageName); + if (serviceState != null) { + serviceState.onPackageInstallStatusChangeLocked(installed); } } - }; + } + } - monitor.register(mContext, null, UserHandle.ALL, true); + void startTrackingPackageChanges() { + mPackageMonitor.register(mContext, null, UserHandle.ALL, true); } @Override diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java index 8bcf1a9be031..47d6879129ee 100644 --- a/services/core/java/com/android/server/tv/TvInputManagerService.java +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -183,7 +183,7 @@ public final class TvInputManagerService extends SystemService { private final Map<String, SessionState> mSessionIdToSessionStateMap = new HashMap<>(); private final MessageHandler mMessageHandler; - + private final MyPackageMonitor mPackageMonitor; private final ActivityManager mActivityManager; private boolean mExternalInputLoggingDisplayNameFilterEnabled = false; @@ -200,6 +200,7 @@ public final class TvInputManagerService extends SystemService { mMessageHandler = new MessageHandler(mContext.getContentResolver(), IoThread.get().getLooper()); mTvInputHardwareManager = new TvInputHardwareManager(context, new HardwareListener()); + mPackageMonitor = new MyPackageMonitor(/* supportsPackageRestartQuery */ true); mActivityManager = (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE); @@ -298,74 +299,79 @@ public final class TvInputManagerService extends SystemService { mExternalInputLoggingDeviceBrandNames.addAll(Arrays.asList(deviceBrandNames)); } - private void registerBroadcastReceivers() { - PackageMonitor monitor = new PackageMonitor(/* supportsPackageRestartQuery */ true) { - private void buildTvInputList(String[] packages) { - int userId = getChangingUserId(); - synchronized (mLock) { - if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) { - buildTvInputListLocked(userId, packages); - buildTvContentRatingSystemListLocked(userId); - } + private class MyPackageMonitor extends PackageMonitor { + MyPackageMonitor(boolean supportsPackageRestartQuery) { + super(supportsPackageRestartQuery); + } + + private void buildTvInputList(String[] packages) { + int userId = getChangingUserId(); + synchronized (mLock) { + if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) { + buildTvInputListLocked(userId, packages); + buildTvContentRatingSystemListLocked(userId); } } + } - @Override - public void onPackageUpdateFinished(String packageName, int uid) { - if (DEBUG) Slog.d(TAG, "onPackageUpdateFinished(packageName=" + packageName + ")"); - // This callback is invoked when the TV input is reinstalled. - // In this case, isReplacing() always returns true. - buildTvInputList(new String[] { packageName }); - } + @Override + public void onPackageUpdateFinished(String packageName, int uid) { + if (DEBUG) Slog.d(TAG, "onPackageUpdateFinished(packageName=" + packageName + ")"); + // This callback is invoked when the TV input is reinstalled. + // In this case, isReplacing() always returns true. + buildTvInputList(new String[] { packageName }); + } - @Override - public void onPackagesAvailable(String[] packages) { - if (DEBUG) { - Slog.d(TAG, "onPackagesAvailable(packages=" + Arrays.toString(packages) + ")"); - } - // This callback is invoked when the media on which some packages exist become - // available. - if (isReplacing()) { - buildTvInputList(packages); - } + @Override + public void onPackagesAvailable(String[] packages) { + if (DEBUG) { + Slog.d(TAG, "onPackagesAvailable(packages=" + Arrays.toString(packages) + ")"); } + // This callback is invoked when the media on which some packages exist become + // available. + if (isReplacing()) { + buildTvInputList(packages); + } + } - @Override - public void onPackagesUnavailable(String[] packages) { - // This callback is invoked when the media on which some packages exist become - // unavailable. - if (DEBUG) { - Slog.d(TAG, "onPackagesUnavailable(packages=" + Arrays.toString(packages) - + ")"); - } - if (isReplacing()) { - buildTvInputList(packages); - } + @Override + public void onPackagesUnavailable(String[] packages) { + // This callback is invoked when the media on which some packages exist become + // unavailable. + if (DEBUG) { + Slog.d(TAG, "onPackagesUnavailable(packages=" + Arrays.toString(packages) + + ")"); } + if (isReplacing()) { + buildTvInputList(packages); + } + } - @Override - public void onSomePackagesChanged() { - // TODO: Use finer-grained methods(e.g. onPackageAdded, onPackageRemoved) to manage - // the TV inputs. - if (DEBUG) Slog.d(TAG, "onSomePackagesChanged()"); - if (isReplacing()) { - if (DEBUG) Slog.d(TAG, "Skipped building TV input list due to replacing"); - // When the package is updated, buildTvInputListLocked is called in other - // methods instead. - return; - } - buildTvInputList(null); + @Override + public void onSomePackagesChanged() { + // TODO: Use finer-grained methods(e.g. onPackageAdded, onPackageRemoved) to manage + // the TV inputs. + if (DEBUG) Slog.d(TAG, "onSomePackagesChanged()"); + if (isReplacing()) { + if (DEBUG) Slog.d(TAG, "Skipped building TV input list due to replacing"); + // When the package is updated, buildTvInputListLocked is called in other + // methods instead. + return; } + buildTvInputList(null); + } - @Override - public boolean onPackageChanged(String packageName, int uid, String[] components) { - // The input list needs to be updated in any cases, regardless of whether - // it happened to the whole package or a specific component. Returning true so that - // the update can be handled in {@link #onSomePackagesChanged}. - return true; - } - }; - monitor.register(mContext, null, UserHandle.ALL, true); + @Override + public boolean onPackageChanged(String packageName, int uid, String[] components) { + // The input list needs to be updated in any cases, regardless of whether + // it happened to the whole package or a specific component. Returning true so that + // the update can be handled in {@link #onSomePackagesChanged}. + return true; + } + } + + private void registerBroadcastReceivers() { + mPackageMonitor.register(mContext, null, UserHandle.ALL, true); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_USER_SWITCHED); diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java index 6a7fc6dcf7cd..42013fab7a14 100644 --- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java @@ -105,6 +105,7 @@ public class TvInteractiveAppManagerService extends SystemService { // A global lock. private final Object mLock = new Object(); private final Context mContext; + private final MyPackageMonitor mPackageMonitor; // ID of the current user. @GuardedBy("mLock") private int mCurrentUserId = UserHandle.USER_SYSTEM; @@ -138,6 +139,7 @@ public class TvInteractiveAppManagerService extends SystemService { super(context); mContext = context; mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE); + mPackageMonitor = new MyPackageMonitor(/* supportsPackageRestartQuery */ true); } @GuardedBy("mLock") @@ -518,86 +520,91 @@ public class TvInteractiveAppManagerService extends SystemService { } } - private void registerBroadcastReceivers() { - PackageMonitor monitor = new PackageMonitor(/* supportsPackageRestartQuery */ true) { - private void buildTvInteractiveAppServiceList(String[] packages) { - int userId = getChangingUserId(); - synchronized (mLock) { - if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) { - buildTvInteractiveAppServiceListLocked(userId, packages); - buildAppLinkInfoLocked(userId); - } + private class MyPackageMonitor extends PackageMonitor { + MyPackageMonitor(boolean supportsPackageRestartQuery) { + super(supportsPackageRestartQuery); + } + + private void buildTvInteractiveAppServiceList(String[] packages) { + int userId = getChangingUserId(); + synchronized (mLock) { + if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) { + buildTvInteractiveAppServiceListLocked(userId, packages); + buildAppLinkInfoLocked(userId); } } - private void buildTvAdServiceList(String[] packages) { - int userId = getChangingUserId(); - synchronized (mLock) { - if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) { - buildTvAdServiceListLocked(userId, packages); - } + } + private void buildTvAdServiceList(String[] packages) { + int userId = getChangingUserId(); + synchronized (mLock) { + if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) { + buildTvAdServiceListLocked(userId, packages); } } + } - @Override - public void onPackageUpdateFinished(String packageName, int uid) { - if (DEBUG) Slogf.d(TAG, "onPackageUpdateFinished(packageName=" + packageName + ")"); - // This callback is invoked when the TV interactive App service is reinstalled. - // In this case, isReplacing() always returns true. - buildTvInteractiveAppServiceList(new String[] { packageName }); - buildTvAdServiceList(new String[] { packageName }); - } + @Override + public void onPackageUpdateFinished(String packageName, int uid) { + if (DEBUG) Slogf.d(TAG, "onPackageUpdateFinished(packageName=" + packageName + ")"); + // This callback is invoked when the TV interactive App service is reinstalled. + // In this case, isReplacing() always returns true. + buildTvInteractiveAppServiceList(new String[] { packageName }); + buildTvAdServiceList(new String[] { packageName }); + } - @Override - public void onPackagesAvailable(String[] packages) { - if (DEBUG) { - Slogf.d(TAG, "onPackagesAvailable(packages=" + Arrays.toString(packages) + ")"); - } - // This callback is invoked when the media on which some packages exist become - // available. - if (isReplacing()) { - buildTvInteractiveAppServiceList(packages); - buildTvAdServiceList(packages); - } + @Override + public void onPackagesAvailable(String[] packages) { + if (DEBUG) { + Slogf.d(TAG, "onPackagesAvailable(packages=" + Arrays.toString(packages) + ")"); } + // This callback is invoked when the media on which some packages exist become + // available. + if (isReplacing()) { + buildTvInteractiveAppServiceList(packages); + buildTvAdServiceList(packages); + } + } - @Override - public void onPackagesUnavailable(String[] packages) { - // This callback is invoked when the media on which some packages exist become - // unavailable. - if (DEBUG) { - Slogf.d(TAG, "onPackagesUnavailable(packages=" + Arrays.toString(packages) - + ")"); - } - if (isReplacing()) { - buildTvInteractiveAppServiceList(packages); - buildTvAdServiceList(packages); - } + @Override + public void onPackagesUnavailable(String[] packages) { + // This callback is invoked when the media on which some packages exist become + // unavailable. + if (DEBUG) { + Slogf.d(TAG, "onPackagesUnavailable(packages=" + Arrays.toString(packages) + + ")"); } + if (isReplacing()) { + buildTvInteractiveAppServiceList(packages); + buildTvAdServiceList(packages); + } + } - @Override - public void onSomePackagesChanged() { - if (DEBUG) Slogf.d(TAG, "onSomePackagesChanged()"); - if (isReplacing()) { - if (DEBUG) { - Slogf.d(TAG, "Skipped building TV interactive App list due to replacing"); - } - // When the package is updated, buildTvInteractiveAppServiceListLocked is called - // in other methods instead. - return; + @Override + public void onSomePackagesChanged() { + if (DEBUG) Slogf.d(TAG, "onSomePackagesChanged()"); + if (isReplacing()) { + if (DEBUG) { + Slogf.d(TAG, "Skipped building TV interactive App list due to replacing"); } - buildTvInteractiveAppServiceList(null); - buildTvAdServiceList(null); + // When the package is updated, buildTvInteractiveAppServiceListLocked is called + // in other methods instead. + return; } + buildTvInteractiveAppServiceList(null); + buildTvAdServiceList(null); + } - @Override - public boolean onPackageChanged(String packageName, int uid, String[] components) { - // The interactive App list needs to be updated in any cases, regardless of whether - // it happened to the whole package or a specific component. Returning true so that - // the update can be handled in {@link #onSomePackagesChanged}. - return true; - } - }; - monitor.register(mContext, null, UserHandle.ALL, true); + @Override + public boolean onPackageChanged(String packageName, int uid, String[] components) { + // The interactive App list needs to be updated in any cases, regardless of whether + // it happened to the whole package or a specific component. Returning true so that + // the update can be handled in {@link #onSomePackagesChanged}. + return true; + } + } + + private void registerBroadcastReceivers() { + mPackageMonitor.register(mContext, null, UserHandle.ALL, true); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_USER_SWITCHED); diff --git a/services/core/java/com/android/server/updates/CertPinInstallReceiver.java b/services/core/java/com/android/server/updates/CertPinInstallReceiver.java index c8e7a8dea5c3..250e99b47b1a 100644 --- a/services/core/java/com/android/server/updates/CertPinInstallReceiver.java +++ b/services/core/java/com/android/server/updates/CertPinInstallReceiver.java @@ -19,10 +19,7 @@ package com.android.server.updates; import android.content.Context; import android.content.Intent; -import java.io.File; - public class CertPinInstallReceiver extends ConfigUpdateInstallReceiver { - private static final String KEYCHAIN_DIR = "/data/misc/keychain/"; public CertPinInstallReceiver() { super("/data/misc/keychain/", "pins", "metadata/", "version"); @@ -30,22 +27,7 @@ public class CertPinInstallReceiver extends ConfigUpdateInstallReceiver { @Override public void onReceive(final Context context, final Intent intent) { - if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { - if (com.android.server.flags.Flags.certpininstallerRemoval()) { - File pins = new File(KEYCHAIN_DIR + "pins"); - if (pins.exists()) { - pins.delete(); - } - File version = new File(KEYCHAIN_DIR + "metadata/version"); - if (version.exists()) { - version.delete(); - } - File metadata = new File(KEYCHAIN_DIR + "metadata"); - if (metadata.exists()) { - metadata.delete(); - } - } - } else if (!com.android.server.flags.Flags.certpininstallerRemoval()) { + if (!com.android.server.flags.Flags.certpininstallerRemoval()) { super.onReceive(context, intent); } } diff --git a/services/core/java/com/android/server/vr/EnabledComponentsObserver.java b/services/core/java/com/android/server/vr/EnabledComponentsObserver.java index 7c2ce6467122..458cb023cc4e 100644 --- a/services/core/java/com/android/server/vr/EnabledComponentsObserver.java +++ b/services/core/java/com/android/server/vr/EnabledComponentsObserver.java @@ -58,6 +58,7 @@ public class EnabledComponentsObserver implements SettingChangeListener { private final Object mLock; private final Context mContext; + private final PackageMonitor mPackageMonitor; private final String mSettingName; private final String mServiceName; private final String mServicePermission; @@ -78,13 +79,39 @@ public class EnabledComponentsObserver implements SettingChangeListener { private EnabledComponentsObserver(@NonNull Context context, @NonNull String settingName, @NonNull String servicePermission, @NonNull String serviceName, @NonNull Object lock, - @NonNull Collection<EnabledComponentChangeListener> listeners) { + @NonNull Collection<EnabledComponentChangeListener> listeners, + @NonNull Looper looper) { mLock = lock; mContext = context; mSettingName = settingName; mServiceName = serviceName; mServicePermission = servicePermission; mEnabledComponentListeners.addAll(listeners); + mPackageMonitor = new PackageMonitor(true) { + @Override + public void onSomePackagesChanged() { + onPackagesChanged(); + } + + @Override + public void onPackageDisappeared(String packageName, int reason) { + onPackagesChanged(); + } + + @Override + public void onPackageModified(String packageName) { + onPackagesChanged(); + } + + @Override + public boolean onHandleForceStop(Intent intent, String[] packages, int uid, + boolean doit) { + onPackagesChanged(); + return super.onHandleForceStop(intent, packages, uid, doit); + } + }; + + mPackageMonitor.register(context, looper, UserHandle.ALL, true);; } /** @@ -108,38 +135,7 @@ public class EnabledComponentsObserver implements SettingChangeListener { SettingsObserver s = SettingsObserver.build(context, handler, settingName); final EnabledComponentsObserver o = new EnabledComponentsObserver(context, settingName, - servicePermission, serviceName, lock, listeners); - - PackageMonitor packageMonitor = new PackageMonitor(true) { - @Override - public void onSomePackagesChanged() { - o.onPackagesChanged(); - - } - - @Override - public void onPackageDisappeared(String packageName, int reason) { - o.onPackagesChanged(); - - } - - @Override - public void onPackageModified(String packageName) { - o.onPackagesChanged(); - - } - - @Override - public boolean onHandleForceStop(Intent intent, String[] packages, int uid, - boolean doit) { - o.onPackagesChanged(); - - return super.onHandleForceStop(intent, packages, uid, doit); - } - }; - - packageMonitor.register(context, looper, UserHandle.ALL, true); - + servicePermission, serviceName, lock, listeners, looper); s.addListener(o); return o; diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java index 8e8455ad5288..6e640d890fb8 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java @@ -17,7 +17,6 @@ package com.android.server.wallpaper; import static android.app.WallpaperManager.ORIENTATION_LANDSCAPE; -import static android.app.WallpaperManager.ORIENTATION_SQUARE_LANDSCAPE; import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; import static android.app.WallpaperManager.getOrientation; import static android.app.WallpaperManager.getRotatedOrientation; @@ -85,20 +84,11 @@ public class WallpaperCropper { private final WallpaperDisplayHelper mWallpaperDisplayHelper; - /** - * Helpers exposed to the window manager part (WallpaperController) - */ - public interface WallpaperCropUtils { - - /** - * Equivalent to {@link WallpaperCropper#getCrop(Point, Point, SparseArray, boolean)} - */ - Rect getCrop(Point displaySize, Point bitmapSize, - SparseArray<Rect> suggestedCrops, boolean rtl); - } + private final WallpaperDefaultDisplayInfo mDefaultDisplayInfo; WallpaperCropper(WallpaperDisplayHelper wallpaperDisplayHelper) { mWallpaperDisplayHelper = wallpaperDisplayHelper; + mDefaultDisplayInfo = mWallpaperDisplayHelper.getDefaultDisplayInfo(); } /** @@ -116,16 +106,16 @@ public class WallpaperCropper { * {@link #getAdjustedCrop}. * </ul> * - * @param displaySize The dimensions of the surface where we want to render the wallpaper - * @param bitmapSize The dimensions of the wallpaper bitmap - * @param rtl Whether the device is right-to-left - * @param suggestedCrops An optional list of user-defined crops for some orientations. - * If there is a suggested crop for + * @param displaySize The dimensions of the surface where we want to render the wallpaper + * @param defaultDisplayInfo The default display info + * @param bitmapSize The dimensions of the wallpaper bitmap + * @param rtl Whether the device is right-to-left + * @param suggestedCrops An optional list of user-defined crops for some orientations. * * @return A Rect indicating how to crop the bitmap for the current display. */ - public Rect getCrop(Point displaySize, Point bitmapSize, - SparseArray<Rect> suggestedCrops, boolean rtl) { + public static Rect getCrop(Point displaySize, WallpaperDefaultDisplayInfo defaultDisplayInfo, + Point bitmapSize, SparseArray<Rect> suggestedCrops, boolean rtl) { int orientation = getOrientation(displaySize); @@ -135,23 +125,24 @@ public class WallpaperCropper { // The first exception is if the device is a foldable and we're on the folded screen. // In that case, show the center of what's on the unfolded screen. - int unfoldedOrientation = mWallpaperDisplayHelper.getUnfoldedOrientation(orientation); + int unfoldedOrientation = defaultDisplayInfo.getUnfoldedOrientation(orientation); if (unfoldedOrientation != ORIENTATION_UNKNOWN) { // Let the system know that we're showing the full image on the unfolded screen SparseArray<Rect> newSuggestedCrops = new SparseArray<>(); newSuggestedCrops.put(unfoldedOrientation, crop); // This will fall into "Case 4" of this function and center the folded screen - return getCrop(displaySize, bitmapSize, newSuggestedCrops, rtl); + return getCrop(displaySize, defaultDisplayInfo, bitmapSize, newSuggestedCrops, + rtl); } // The second exception is if we're on tablet and we're on portrait mode. // In that case, center the wallpaper relatively to landscape and put some parallax. - boolean isTablet = mWallpaperDisplayHelper.isLargeScreen() - && !mWallpaperDisplayHelper.isFoldable(); + boolean isTablet = defaultDisplayInfo.isLargeScreen && !defaultDisplayInfo.isFoldable; if (isTablet && displaySize.x < displaySize.y) { Point rotatedDisplaySize = new Point(displaySize.y, displaySize.x); // compute the crop on landscape (without parallax) - Rect landscapeCrop = getCrop(rotatedDisplaySize, bitmapSize, suggestedCrops, rtl); + Rect landscapeCrop = getCrop(rotatedDisplaySize, defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl); landscapeCrop = noParallax(landscapeCrop, rotatedDisplaySize, bitmapSize, rtl); // compute the crop on portrait at the center of the landscape crop crop = getAdjustedCrop(landscapeCrop, bitmapSize, displaySize, false, rtl, ADD); @@ -173,7 +164,8 @@ public class WallpaperCropper { if (testCrop == null || testCrop.left < 0 || testCrop.top < 0 || testCrop.right > bitmapSize.x || testCrop.bottom > bitmapSize.y) { Slog.w(TAG, "invalid crop: " + testCrop + " for bitmap size: " + bitmapSize); - return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl); + return getCrop(displaySize, defaultDisplayInfo, bitmapSize, new SparseArray<>(), + rtl); } } @@ -185,10 +177,9 @@ public class WallpaperCropper { // Case 3: if we have the 90° rotated orientation in the suggested crops, reuse it and // trying to preserve the zoom level and the center of the image - SparseArray<Point> defaultDisplaySizes = mWallpaperDisplayHelper.getDefaultDisplaySizes(); int rotatedOrientation = getRotatedOrientation(orientation); suggestedCrop = suggestedCrops.get(rotatedOrientation); - Point suggestedDisplaySize = defaultDisplaySizes.get(rotatedOrientation); + Point suggestedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(rotatedOrientation); if (suggestedCrop != null) { // only keep the visible part (without parallax) Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl); @@ -197,9 +188,9 @@ public class WallpaperCropper { // Case 4: if the device is a foldable, if we're looking for a folded orientation and have // the suggested crop of the relative unfolded orientation, reuse it by removing content. - int unfoldedOrientation = mWallpaperDisplayHelper.getUnfoldedOrientation(orientation); + int unfoldedOrientation = defaultDisplayInfo.getUnfoldedOrientation(orientation); suggestedCrop = suggestedCrops.get(unfoldedOrientation); - suggestedDisplaySize = defaultDisplaySizes.get(unfoldedOrientation); + suggestedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(unfoldedOrientation); if (suggestedCrop != null) { // compute the visible part (without parallax) of the unfolded screen Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl); @@ -207,8 +198,11 @@ public class WallpaperCropper { Rect res = getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, REMOVE); // if we removed some width, add it back to add a parallax effect if (res.width() < adjustedCrop.width()) { - if (rtl) res.left = Math.min(res.left, adjustedCrop.left); - else res.right = Math.max(res.right, adjustedCrop.right); + if (rtl) { + res.left = Math.min(res.left, adjustedCrop.left); + } else { + res.right = Math.max(res.right, adjustedCrop.right); + } // use getAdjustedCrop(parallax=true) to make sure we don't exceed MAX_PARALLAX res = getAdjustedCrop(res, bitmapSize, displaySize, true, rtl, ADD); } @@ -218,9 +212,9 @@ public class WallpaperCropper { // Case 5: if the device is a foldable, if we're looking for an unfolded orientation and // have the suggested crop of the relative folded orientation, reuse it by adding content. - int foldedOrientation = mWallpaperDisplayHelper.getFoldedOrientation(orientation); + int foldedOrientation = defaultDisplayInfo.getFoldedOrientation(orientation); suggestedCrop = suggestedCrops.get(foldedOrientation); - suggestedDisplaySize = defaultDisplaySizes.get(foldedOrientation); + suggestedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(foldedOrientation); if (suggestedCrop != null) { // only keep the visible part (without parallax) Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl); @@ -229,17 +223,19 @@ public class WallpaperCropper { // Case 6: for a foldable device, try to combine case 3 + case 4 or 5: // rotate, then fold or unfold - Point rotatedDisplaySize = defaultDisplaySizes.get(rotatedOrientation); + Point rotatedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(rotatedOrientation); if (rotatedDisplaySize != null) { - int rotatedFolded = mWallpaperDisplayHelper.getFoldedOrientation(rotatedOrientation); - int rotateUnfolded = mWallpaperDisplayHelper.getUnfoldedOrientation(rotatedOrientation); + int rotatedFolded = defaultDisplayInfo.getFoldedOrientation(rotatedOrientation); + int rotateUnfolded = defaultDisplayInfo.getUnfoldedOrientation(rotatedOrientation); for (int suggestedOrientation : new int[]{rotatedFolded, rotateUnfolded}) { suggestedCrop = suggestedCrops.get(suggestedOrientation); if (suggestedCrop != null) { - Rect rotatedCrop = getCrop(rotatedDisplaySize, bitmapSize, suggestedCrops, rtl); + Rect rotatedCrop = getCrop(rotatedDisplaySize, defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl); SparseArray<Rect> rotatedCropMap = new SparseArray<>(); rotatedCropMap.put(rotatedOrientation, rotatedCrop); - return getCrop(displaySize, bitmapSize, rotatedCropMap, rtl); + return getCrop(displaySize, defaultDisplayInfo, bitmapSize, rotatedCropMap, + rtl); } } } @@ -248,8 +244,8 @@ public class WallpaperCropper { Slog.w(TAG, "Could not find a proper default crop for display: " + displaySize + ", bitmap size: " + bitmapSize + ", suggested crops: " + suggestedCrops + ", orientation: " + orientation + ", rtl: " + rtl - + ", defaultDisplaySizes: " + defaultDisplaySizes); - return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl); + + ", defaultDisplaySizes: " + defaultDisplayInfo.defaultDisplaySizes); + return getCrop(displaySize, defaultDisplayInfo, bitmapSize, new SparseArray<>(), rtl); } /** @@ -445,7 +441,7 @@ public class WallpaperCropper { Rect suggestedCrop = suggestedCrops.get(orientation); if (suggestedCrop != null) { adjustedSuggestedCrops.put(orientation, - getCrop(displaySize, bitmapSize, suggestedCrops, rtl)); + getCrop(displaySize, mDefaultDisplayInfo, bitmapSize, suggestedCrops, rtl)); } } @@ -455,7 +451,8 @@ public class WallpaperCropper { int orientation = defaultDisplaySizes.keyAt(i); if (result.contains(orientation)) continue; Point displaySize = defaultDisplaySizes.valueAt(i); - Rect newCrop = getCrop(displaySize, bitmapSize, adjustedSuggestedCrops, rtl); + Rect newCrop = getCrop(displaySize, mDefaultDisplayInfo, bitmapSize, + adjustedSuggestedCrops, rtl); result.put(orientation, newCrop); } return result; diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java index ba0262a8bd19..69f0ef7c430e 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java @@ -542,9 +542,11 @@ public class WallpaperDataParser { // to support back compatibility in B&R, save the crops for one orientation in the // legacy "cropLeft", "cropTop", "cropRight", "cropBottom" entries int orientationToPutInLegacyCrop = wallpaper.mOrientationWhenSet; - if (mWallpaperDisplayHelper.isFoldable()) { - int unfoldedOrientation = mWallpaperDisplayHelper - .getUnfoldedOrientation(orientationToPutInLegacyCrop); + WallpaperDefaultDisplayInfo defaultDisplayInfo = + mWallpaperDisplayHelper.getDefaultDisplayInfo(); + if (defaultDisplayInfo.isFoldable) { + int unfoldedOrientation = defaultDisplayInfo.getUnfoldedOrientation( + orientationToPutInLegacyCrop); if (unfoldedOrientation != ORIENTATION_UNKNOWN) { orientationToPutInLegacyCrop = unfoldedOrientation; } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java b/services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java new file mode 100644 index 000000000000..dabe91968338 --- /dev/null +++ b/services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.wallpaper; + +import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; +import static android.app.WallpaperManager.getRotatedOrientation; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP; + +import android.app.WallpaperManager; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.SparseArray; +import android.view.WindowManager; +import android.view.WindowMetrics; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; + + +/** A data class for the default display attributes used in wallpaper related operations. */ +public final class WallpaperDefaultDisplayInfo { + /** + * A data class representing the screen orientations for a foldable device in the folded and + * unfolded states. + */ + @VisibleForTesting + static final class FoldableOrientations { + @WallpaperManager.ScreenOrientation + public final int foldedOrientation; + @WallpaperManager.ScreenOrientation + public final int unfoldedOrientation; + + FoldableOrientations(int foldedOrientation, int unfoldedOrientation) { + this.foldedOrientation = foldedOrientation; + this.unfoldedOrientation = unfoldedOrientation; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof FoldableOrientations that)) return false; + return foldedOrientation == that.foldedOrientation + && unfoldedOrientation == that.unfoldedOrientation; + } + + @Override + public int hashCode() { + return Objects.hash(foldedOrientation, unfoldedOrientation); + } + } + + public final SparseArray<Point> defaultDisplaySizes; + public final boolean isLargeScreen; + public final boolean isFoldable; + @VisibleForTesting + final List<FoldableOrientations> foldableOrientations; + + public WallpaperDefaultDisplayInfo() { + this.defaultDisplaySizes = new SparseArray<>(); + this.isLargeScreen = false; + this.isFoldable = false; + this.foldableOrientations = Collections.emptyList(); + } + + public WallpaperDefaultDisplayInfo(WindowManager windowManager, Resources resources) { + Set<WindowMetrics> metrics = windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY); + boolean isFoldable = resources.getIntArray(R.array.config_foldedDeviceStates).length > 0; + if (isFoldable) { + this.foldableOrientations = getFoldableOrientations(metrics); + } else { + this.foldableOrientations = Collections.emptyList(); + } + this.defaultDisplaySizes = getDisplaySizes(metrics); + this.isLargeScreen = isLargeScreen(metrics); + this.isFoldable = isFoldable; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof WallpaperDefaultDisplayInfo that)) return false; + return isLargeScreen == that.isLargeScreen && isFoldable == that.isFoldable + && defaultDisplaySizes.contentEquals(that.defaultDisplaySizes) + && Objects.equals(foldableOrientations, that.foldableOrientations); + } + + @Override + public int hashCode() { + return 31 * Objects.hash(isLargeScreen, isFoldable, foldableOrientations) + + defaultDisplaySizes.contentHashCode(); + } + + /** + * Returns the folded orientation corresponds to the {@code unfoldedOrientation} found in + * {@link #foldableOrientations}. If not found, returns + * {@link WallpaperManager.ORIENTATION_UNKNOWN}. + */ + public int getFoldedOrientation(int unfoldedOrientation) { + for (FoldableOrientations orientations : foldableOrientations) { + if (orientations.unfoldedOrientation == unfoldedOrientation) { + return orientations.foldedOrientation; + } + } + return ORIENTATION_UNKNOWN; + } + + /** + * Returns the unfolded orientation corresponds to the {@code foldedOrientation} found in + * {@link #foldableOrientations}. If not found, returns + * {@link WallpaperManager.ORIENTATION_UNKNOWN}. + */ + public int getUnfoldedOrientation(int foldedOrientation) { + for (FoldableOrientations orientations : foldableOrientations) { + if (orientations.foldedOrientation == foldedOrientation) { + return orientations.unfoldedOrientation; + } + } + return ORIENTATION_UNKNOWN; + } + + private static SparseArray<Point> getDisplaySizes(Set<WindowMetrics> displayMetrics) { + SparseArray<Point> displaySizes = new SparseArray<>(); + for (WindowMetrics metric : displayMetrics) { + Rect bounds = metric.getBounds(); + Point displaySize = new Point(bounds.width(), bounds.height()); + Point reversedDisplaySize = new Point(displaySize.y, displaySize.x); + for (Point point : List.of(displaySize, reversedDisplaySize)) { + int orientation = WallpaperManager.getOrientation(point); + // don't add an entry if there is already a larger display of the same orientation + Point display = displaySizes.get(orientation); + if (display == null || display.x * display.y < point.x * point.y) { + displaySizes.put(orientation, point); + } + } + } + return displaySizes; + } + + private static boolean isLargeScreen(Set<WindowMetrics> displayMetrics) { + float smallestWidth = Float.MAX_VALUE; + for (WindowMetrics metric : displayMetrics) { + Rect bounds = metric.getBounds(); + smallestWidth = Math.min(smallestWidth, bounds.width() / metric.getDensity()); + } + return smallestWidth >= LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP; + } + + /** + * Determines all potential foldable orientations, populating {@code + * outFoldableOrientationPairs} with pairs of (folded orientation, unfolded orientation). If + * {@code defaultDisplayMetrics} isn't for foldable, {@code outFoldableOrientationPairs} will + * not be populated. + */ + private static List<FoldableOrientations> getFoldableOrientations( + Set<WindowMetrics> defaultDisplayMetrics) { + if (defaultDisplayMetrics.size() != 2) { + return Collections.emptyList(); + } + List<FoldableOrientations> foldableOrientations = new ArrayList<>(); + float surface = 0; + int firstOrientation = -1; + for (WindowMetrics metric : defaultDisplayMetrics) { + Rect bounds = metric.getBounds(); + Point displaySize = new Point(bounds.width(), bounds.height()); + + int orientation = WallpaperManager.getOrientation(displaySize); + float newSurface = displaySize.x * displaySize.y + / (metric.getDensity() * metric.getDensity()); + if (surface <= 0) { + surface = newSurface; + firstOrientation = orientation; + } else { + FoldableOrientations orientations = (newSurface > surface) + ? new FoldableOrientations(firstOrientation, orientation) + : new FoldableOrientations(orientation, firstOrientation); + FoldableOrientations rotatedOrientations = new FoldableOrientations( + getRotatedOrientation(orientations.foldedOrientation), + getRotatedOrientation(orientations.unfoldedOrientation)); + foldableOrientations.add(orientations); + foldableOrientations.add(rotatedOrientations); + } + } + return Collections.unmodifiableList(foldableOrientations); + } +} diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java index 3636f5aa8f27..bff5fc9c49f3 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java @@ -16,31 +16,25 @@ package com.android.server.wallpaper; -import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; -import static android.app.WallpaperManager.getRotatedOrientation; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.window.flags.Flags.multiCrop; import android.app.WallpaperManager; +import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.os.Binder; import android.os.Debug; -import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.view.Display; import android.view.DisplayInfo; import android.view.WindowManager; -import android.view.WindowMetrics; import com.android.server.wm.WindowManagerInternal; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; import java.util.function.Consumer; /** @@ -59,65 +53,25 @@ class WallpaperDisplayHelper { } private static final String TAG = WallpaperDisplayHelper.class.getSimpleName(); - private static final float LARGE_SCREEN_MIN_DP = 600f; private final SparseArray<DisplayData> mDisplayDatas = new SparseArray<>(); private final DisplayManager mDisplayManager; private final WindowManagerInternal mWindowManagerInternal; - private final SparseArray<Point> mDefaultDisplaySizes = new SparseArray<>(); - // related orientations pairs for foldable (folded orientation, unfolded orientation) - private final List<Pair<Integer, Integer>> mFoldableOrientationPairs = new ArrayList<>(); - - private final boolean mIsFoldable; - private boolean mIsLargeScreen = false; + private final WallpaperDefaultDisplayInfo mDefaultDisplayInfo; WallpaperDisplayHelper( DisplayManager displayManager, WindowManager windowManager, WindowManagerInternal windowManagerInternal, - boolean isFoldable) { + Resources resources) { mDisplayManager = displayManager; mWindowManagerInternal = windowManagerInternal; - mIsFoldable = isFoldable; - if (!multiCrop()) return; - Set<WindowMetrics> metrics = windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY); - boolean populateOrientationPairs = isFoldable && metrics.size() == 2; - float surface = 0; - int firstOrientation = -1; - for (WindowMetrics metric: metrics) { - Rect bounds = metric.getBounds(); - Point displaySize = new Point(bounds.width(), bounds.height()); - Point reversedDisplaySize = new Point(displaySize.y, displaySize.x); - for (Point point : List.of(displaySize, reversedDisplaySize)) { - int orientation = WallpaperManager.getOrientation(point); - // don't add an entry if there is already a larger display of the same orientation - Point display = mDefaultDisplaySizes.get(orientation); - if (display == null || display.x * display.y < point.x * point.y) { - mDefaultDisplaySizes.put(orientation, point); - } - } - - mIsLargeScreen |= (displaySize.x / metric.getDensity() >= LARGE_SCREEN_MIN_DP); - - if (populateOrientationPairs) { - int orientation = WallpaperManager.getOrientation(displaySize); - float newSurface = displaySize.x * displaySize.y - / (metric.getDensity() * metric.getDensity()); - if (surface <= 0) { - surface = newSurface; - firstOrientation = orientation; - } else { - Pair<Integer, Integer> pair = (newSurface > surface) - ? new Pair<>(firstOrientation, orientation) - : new Pair<>(orientation, firstOrientation); - Pair<Integer, Integer> rotatedPair = new Pair<>( - getRotatedOrientation(pair.first), getRotatedOrientation(pair.second)); - mFoldableOrientationPairs.add(pair); - mFoldableOrientationPairs.add(rotatedPair); - } - } + if (!multiCrop()) { + mDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(); + return; } + mDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(windowManager, resources); } DisplayData getDisplayDataOrCreate(int displayId) { @@ -203,51 +157,21 @@ class WallpaperDisplayHelper { } SparseArray<Point> getDefaultDisplaySizes() { - return mDefaultDisplaySizes; + return mDefaultDisplayInfo.defaultDisplaySizes; } /** Return the number of pixel of the largest dimension of the default display */ int getDefaultDisplayLargestDimension() { + SparseArray<Point> defaultDisplaySizes = mDefaultDisplayInfo.defaultDisplaySizes; int result = -1; - for (int i = 0; i < mDefaultDisplaySizes.size(); i++) { - Point size = mDefaultDisplaySizes.valueAt(i); + for (int i = 0; i < defaultDisplaySizes.size(); i++) { + Point size = defaultDisplaySizes.valueAt(i); result = Math.max(result, Math.max(size.x, size.y)); } return result; } - boolean isFoldable() { - return mIsFoldable; - } - - /** - * Return true if any of the screens of the default display is considered large (DP >= 600) - */ - boolean isLargeScreen() { - return mIsLargeScreen; - } - - /** - * If a given orientation corresponds to an unfolded orientation on foldable, return the - * corresponding folded orientation. Otherwise, return UNKNOWN. Always return UNKNOWN if the - * device is not a foldable. - */ - int getFoldedOrientation(int orientation) { - for (Pair<Integer, Integer> pair : mFoldableOrientationPairs) { - if (pair.second.equals(orientation)) return pair.first; - } - return ORIENTATION_UNKNOWN; - } - - /** - * If a given orientation corresponds to a folded orientation on foldable, return the - * corresponding unfolded orientation. Otherwise, return UNKNOWN. Always return UNKNOWN if the - * device is not a foldable. - */ - int getUnfoldedOrientation(int orientation) { - for (Pair<Integer, Integer> pair : mFoldableOrientationPairs) { - if (pair.first.equals(orientation)) return pair.second; - } - return ORIENTATION_UNKNOWN; + public WallpaperDefaultDisplayInfo getDefaultDisplayInfo() { + return mDefaultDisplayInfo; } } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index bac732637d8d..e7da33d50b27 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -1666,12 +1666,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); displayManager.registerDisplayListener(mDisplayListener, null /* handler */); WindowManager windowManager = mContext.getSystemService(WindowManager.class); - boolean isFoldable = mContext.getResources() - .getIntArray(R.array.config_foldedDeviceStates).length > 0; mWallpaperDisplayHelper = new WallpaperDisplayHelper( - displayManager, windowManager, mWindowManagerInternal, isFoldable); + displayManager, windowManager, mWindowManagerInternal, mContext.getResources()); mWallpaperCropper = new WallpaperCropper(mWallpaperDisplayHelper); - mWindowManagerInternal.setWallpaperCropUtils(mWallpaperCropper::getCrop); mActivityManager = mContext.getSystemService(ActivityManager.class); if (mContext.getResources().getBoolean( @@ -2510,9 +2507,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub List<Rect> result = new ArrayList<>(); boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL; + WallpaperDefaultDisplayInfo defaultDisplayInfo = + mWallpaperDisplayHelper.getDefaultDisplayInfo(); for (Point displaySize : displaySizes) { - result.add(mWallpaperCropper.getCrop( - displaySize, croppedBitmapSize, adjustedRelativeSuggestedCrops, rtl)); + result.add(WallpaperCropper.getCrop(displaySize, defaultDisplayInfo, + croppedBitmapSize, adjustedRelativeSuggestedCrops, rtl)); } if (originalBitmap) result = WallpaperCropper.getOriginalCropHints(wallpaper, result); return result; @@ -2548,8 +2547,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub List<Rect> result = new ArrayList<>(); boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL; + WallpaperDefaultDisplayInfo defaultDisplayInfo = + mWallpaperDisplayHelper.getDefaultDisplayInfo(); for (Point displaySize : displaySizes) { - result.add(mWallpaperCropper.getCrop(displaySize, bitmapSize, defaultCrops, rtl)); + result.add(WallpaperCropper.getCrop(displaySize, defaultDisplayInfo, bitmapSize, + defaultCrops, rtl)); } return result; } diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index a94183849bc5..e2b47b92f232 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -112,7 +112,6 @@ import com.android.server.FgThread; import com.android.server.LocalServices; import com.android.server.apphibernation.AppHibernationManagerInternal; import com.android.server.apphibernation.AppHibernationService; -import com.android.window.flags.Flags; import java.util.ArrayList; import java.util.concurrent.TimeUnit; @@ -807,14 +806,8 @@ class ActivityMetricsLogger { } final Task otherTask = otherInfo.mLastLaunchedActivity.getTask(); // The adjacent task is the split root in which activities are started - final boolean isDescendantOfAdjacent; - if (Flags.allowMultipleAdjacentTaskFragments()) { - isDescendantOfAdjacent = launchedSplitRootTask.forOtherAdjacentTasks( - otherTask::isDescendantOf); - } else { - isDescendantOfAdjacent = otherTask.isDescendantOf( - launchedSplitRootTask.getAdjacentTask()); - } + final boolean isDescendantOfAdjacent = launchedSplitRootTask.forOtherAdjacentTasks( + otherTask::isDescendantOf); if (isDescendantOfAdjacent) { if (DEBUG_METRICS) { Slog.i(TAG, "Found adjacent tasks t1=" + launchedActivityTask.mTaskId diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 3cd4db7d8dfc..e91d88901751 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -1716,6 +1716,7 @@ final class ActivityRecord extends WindowToken { } mAppCompatController.getLetterboxPolicy().onMovedToDisplay(mDisplayContent.getDisplayId()); + mAppCompatController.getDisplayCompatModePolicy().onMovedToDisplay(); } void layoutLetterboxIfNeeded(WindowState winHint) { @@ -3801,19 +3802,10 @@ final class ActivityRecord extends WindowToken { final TaskFragment taskFragment = getTaskFragment(); if (next != null && taskFragment != null && taskFragment.isEmbedded()) { final TaskFragment organized = taskFragment.getOrganizedTaskFragment(); - if (Flags.allowMultipleAdjacentTaskFragments()) { - delayRemoval = organized != null - && organized.topRunningActivity() == null - && organized.isDelayLastActivityRemoval() - && organized.forOtherAdjacentTaskFragments(next::isDescendantOf); - } else { - final TaskFragment adjacent = - organized != null ? organized.getAdjacentTaskFragment() : null; - if (adjacent != null && next.isDescendantOf(adjacent) - && organized.topRunningActivity() == null) { - delayRemoval = organized.isDelayLastActivityRemoval(); - } - } + delayRemoval = organized != null + && organized.topRunningActivity() == null + && organized.isDelayLastActivityRemoval() + && organized.forOtherAdjacentTaskFragments(next::isDescendantOf); } // isNextNotYetVisible is to check if the next activity is invisible, or it has been @@ -4787,11 +4779,6 @@ final class ActivityRecord extends WindowToken { } // Make sure the embedded adjacent can also be shown. - if (!Flags.allowMultipleAdjacentTaskFragments()) { - final ActivityRecord adjacentActivity = taskFragment.getAdjacentTaskFragment() - .getTopNonFinishingActivity(); - return canShowWhenLocked(adjacentActivity); - } final boolean hasAdjacentNotAllowToShow = taskFragment.forOtherAdjacentTaskFragments( adjacentTF -> !canShowWhenLocked(adjacentTF.getTopNonFinishingActivity())); return !hasAdjacentNotAllowToShow; @@ -8980,6 +8967,7 @@ final class ActivityRecord extends WindowToken { // Reset the existing override configuration so it can be updated according to the latest // configuration. mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); + mAppCompatController.getDisplayCompatModePolicy().onProcessRestarted(); if (!attachedToProcess()) { return; diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java index 21628341ea62..0f1939bfbb49 100644 --- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java +++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java @@ -34,7 +34,6 @@ import android.window.TaskSnapshot; import com.android.internal.annotations.VisibleForTesting; import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider; -import com.android.window.flags.Flags; import java.io.File; import java.io.PrintWriter; @@ -107,8 +106,7 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord && !ActivityManager.isLowRamDeviceStatic(); // Don't support Android Go setSnapshotEnabled(snapshotEnabled); mSnapshotPersistQueue = persistQueue; - mPersistInfoProvider = createPersistInfoProvider(service, - Environment::getDataSystemCeDirectory); + mPersistInfoProvider = createPersistInfoProvider(service); mPersister = new TaskSnapshotPersister( persistQueue, mPersistInfoProvider, @@ -117,6 +115,11 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord initialize(new ActivitySnapshotCache()); } + @VisibleForTesting + PersistInfoProvider createPersistInfoProvider(WindowManagerService service) { + return createPersistInfoProvider(service, Environment::getDataSystemCeDirectory); + } + @Override protected float initSnapshotScale() { final float config = mService.mContext.getResources().getFloat( @@ -528,26 +531,6 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord final int currentIndex = currTF.asTask() != null ? currentTask.mChildren.indexOf(currentActivity) : currentTask.mChildren.indexOf(currTF); - if (!Flags.allowMultipleAdjacentTaskFragments()) { - final int prevAdjacentIndex = currentTask.mChildren.indexOf( - prevTF.getAdjacentTaskFragment()); - if (prevAdjacentIndex > currentIndex) { - // PrevAdjacentTF already above currentActivity - return; - } - // Add both the one below, and its adjacent. - if (!inTransition || isInParticipant(initPrev, mTmpTransitionParticipants)) { - result.add(initPrev); - } - final ActivityRecord prevAdjacentActivity = prevTF.getAdjacentTaskFragment() - .getTopMostActivity(); - if (prevAdjacentActivity != null && (!inTransition - || isInParticipant(prevAdjacentActivity, mTmpTransitionParticipants))) { - result.add(prevAdjacentActivity); - } - return; - } - final boolean hasAdjacentAboveCurrent = prevTF.forOtherAdjacentTaskFragments( prevAdjacentTF -> { final int prevAdjacentIndex = currentTask.mChildren.indexOf(prevAdjacentTF); diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index a4c061e9d010..a84a008f66eb 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1111,8 +1111,11 @@ class ActivityStarter { if (sourceRecord != null) { if (requestCode >= 0 && !sourceRecord.finishing) { resultRecord = sourceRecord; + request.logMessage.append(" (rr="); + } else { + request.logMessage.append(" (sr="); } - request.logMessage.append(" (sr=" + System.identityHashCode(sourceRecord) + ")"); + request.logMessage.append(System.identityHashCode(sourceRecord) + ")"); } } diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index a7f2153993bb..b0563128870a 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -164,7 +164,6 @@ import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import com.android.server.pm.SaferIntentUtils; import com.android.server.utils.Slogf; import com.android.server.wm.ActivityMetricsLogger.LaunchingState; -import com.android.window.flags.Flags; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -2991,17 +2990,9 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { if (child.asTaskFragment() != null && child.asTaskFragment().hasAdjacentTaskFragment()) { - final boolean isAnyTranslucent; - if (Flags.allowMultipleAdjacentTaskFragments()) { - final TaskFragment.AdjacentSet set = - child.asTaskFragment().getAdjacentTaskFragments(); - isAnyTranslucent = set.forAllTaskFragments( - tf -> !isOpaque(tf), null); - } else { - final TaskFragment adjacent = child.asTaskFragment() - .getAdjacentTaskFragment(); - isAnyTranslucent = !isOpaque(child) || !isOpaque(adjacent); - } + final boolean isAnyTranslucent = !isOpaque(child) + || child.asTaskFragment().forOtherAdjacentTaskFragments( + tf -> !isOpaque(tf)); if (!isAnyTranslucent) { // This task fragment and all its adjacent task fragments are opaque, // consider it opaque even if it doesn't fill its parent. diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java index 48f08e945a59..c479591a5e0d 100644 --- a/services/core/java/com/android/server/wm/AppCompatController.java +++ b/services/core/java/com/android/server/wm/AppCompatController.java @@ -46,6 +46,8 @@ class AppCompatController { private final AppCompatSizeCompatModePolicy mSizeCompatModePolicy; @NonNull private final AppCompatSandboxingPolicy mSandboxingPolicy; + @NonNull + private final AppCompatDisplayCompatModePolicy mDisplayCompatModePolicy; AppCompatController(@NonNull WindowManagerService wmService, @NonNull ActivityRecord activityRecord) { @@ -69,6 +71,7 @@ class AppCompatController { mSizeCompatModePolicy = new AppCompatSizeCompatModePolicy(activityRecord, mAppCompatOverrides); mSandboxingPolicy = new AppCompatSandboxingPolicy(activityRecord); + mDisplayCompatModePolicy = new AppCompatDisplayCompatModePolicy(); } @NonNull @@ -151,6 +154,11 @@ class AppCompatController { return mSandboxingPolicy; } + @NonNull + AppCompatDisplayCompatModePolicy getDisplayCompatModePolicy() { + return mDisplayCompatModePolicy; + } + void dump(@NonNull PrintWriter pw, @NonNull String prefix) { getTransparentPolicy().dump(pw, prefix); getLetterboxPolicy().dump(pw, prefix); diff --git a/services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java new file mode 100644 index 000000000000..acf51707c894 --- /dev/null +++ b/services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.wm; + +import com.android.window.flags.Flags; + +/** + * Encapsulate app-compat logic for multi-display environments. + */ +class AppCompatDisplayCompatModePolicy { + + private boolean mIsRestartMenuEnabledForDisplayMove; + + boolean isRestartMenuEnabledForDisplayMove() { + return Flags.enableRestartMenuForConnectedDisplays() && mIsRestartMenuEnabledForDisplayMove; + } + + void onMovedToDisplay() { + mIsRestartMenuEnabledForDisplayMove = true; + } + + void onProcessRestarted() { + mIsRestartMenuEnabledForDisplayMove = false; + } +} diff --git a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java index b03aa5263927..0f1e36d70db2 100644 --- a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java @@ -361,7 +361,10 @@ class AppCompatSizeCompatModePolicy { if (enableSizeCompatModeImprovementsForConnectedDisplays()) { overrideConfig.touchscreen = fullConfig.touchscreen; overrideConfig.navigation = fullConfig.navigation; - overrideConfig.fontScale = fullConfig.fontScale; + overrideConfig.keyboard = fullConfig.keyboard; + overrideConfig.keyboardHidden = fullConfig.keyboardHidden; + overrideConfig.hardKeyboardHidden = fullConfig.hardKeyboardHidden; + overrideConfig.navigationHidden = fullConfig.navigationHidden; } // The smallest screen width is the short side of screen bounds. Because the bounds // and density won't be changed, smallestScreenWidthDp is also fixed. diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java index 146044008b3f..b91a12598e01 100644 --- a/services/core/java/com/android/server/wm/AppCompatUtils.java +++ b/services/core/java/com/android/server/wm/AppCompatUtils.java @@ -161,6 +161,9 @@ final class AppCompatUtils { top.mAppCompatController.getLetterboxOverrides() .isLetterboxEducationEnabled()); + appCompatTaskInfo.setRestartMenuEnabledForDisplayMove(top.mAppCompatController + .getDisplayCompatModePolicy().isRestartMenuEnabledForDisplayMove()); + final AppCompatAspectRatioOverrides aspectRatioOverrides = top.mAppCompatController.getAspectRatioOverrides(); appCompatTaskInfo.setUserFullscreenOverrideEnabled( diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index e9b7649e8cbd..dfe323c43abb 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -479,21 +479,16 @@ class BackNavigationController { } } else { // If adjacent TF has companion to current TF, those two TF will be closed together. - final TaskFragment adjacentTF; - if (Flags.allowMultipleAdjacentTaskFragments()) { - if (currTF.getAdjacentTaskFragments().size() > 2) { - throw new IllegalStateException( - "Not yet support 3+ adjacent for non-Task TFs"); - } - final TaskFragment[] tmpAdjacent = new TaskFragment[1]; - currTF.forOtherAdjacentTaskFragments(tf -> { - tmpAdjacent[0] = tf; - return true; - }); - adjacentTF = tmpAdjacent[0]; - } else { - adjacentTF = currTF.getAdjacentTaskFragment(); + if (currTF.getAdjacentTaskFragments().size() > 2) { + throw new IllegalStateException( + "Not yet support 3+ adjacent for non-Task TFs"); } + final TaskFragment[] tmpAdjacent = new TaskFragment[1]; + currTF.forOtherAdjacentTaskFragments(tf -> { + tmpAdjacent[0] = tf; + return true; + }); + final TaskFragment adjacentTF = tmpAdjacent[0]; if (isSecondCompanionToFirst(currTF, adjacentTF)) { // The two TFs are adjacent (visually displayed side-by-side), search if any // activity below the lowest one. @@ -553,15 +548,6 @@ class BackNavigationController { if (!prevTF.hasAdjacentTaskFragment()) { return; } - if (!Flags.allowMultipleAdjacentTaskFragments()) { - final TaskFragment prevTFAdjacent = prevTF.getAdjacentTaskFragment(); - final ActivityRecord prevActivityAdjacent = - prevTFAdjacent.getTopNonFinishingActivity(); - if (prevActivityAdjacent != null) { - outPrevActivities.add(prevActivityAdjacent); - } - return; - } prevTF.forOtherAdjacentTaskFragments(prevTFAdjacent -> { final ActivityRecord prevActivityAdjacent = prevTFAdjacent.getTopNonFinishingActivity(); @@ -577,14 +563,6 @@ class BackNavigationController { if (mainTF == null || !mainTF.hasAdjacentTaskFragment()) { return; } - if (!Flags.allowMultipleAdjacentTaskFragments()) { - final TaskFragment adjacentTF = mainTF.getAdjacentTaskFragment(); - final ActivityRecord topActivity = adjacentTF.getTopNonFinishingActivity(); - if (topActivity != null) { - outList.add(topActivity); - } - return; - } mainTF.forOtherAdjacentTaskFragments(adjacentTF -> { final ActivityRecord topActivity = adjacentTF.getTopNonFinishingActivity(); if (topActivity != null) { diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 2287a687700c..f50a68cc5389 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -49,8 +49,8 @@ import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskS import static com.android.window.flags.Flags.balImprovedMetrics; import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator; import static com.android.window.flags.Flags.balShowToastsBlocked; -import static com.android.window.flags.Flags.balStrictModeRo; import static com.android.window.flags.Flags.balStrictModeGracePeriod; +import static com.android.window.flags.Flags.balStrictModeRo; import static java.lang.annotation.RetentionPolicy.SOURCE; import static java.util.Objects.requireNonNull; @@ -91,7 +91,6 @@ import com.android.internal.util.Preconditions; import com.android.server.UiThread; import com.android.server.am.PendingIntentRecord; import com.android.server.wm.BackgroundLaunchProcessController.BalCheckConfiguration; -import com.android.window.flags.Flags; import java.lang.annotation.Retention; import java.util.ArrayList; @@ -1687,14 +1686,6 @@ public class BackgroundActivityStartController { } // Check the adjacent fragment. - if (!Flags.allowMultipleAdjacentTaskFragments()) { - TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment(); - topActivity = adjacentTaskFragment.getActivity(topOfStackPredicate); - if (topActivity == null) { - return bas; - } - return checkCrossUidActivitySwitchFromBelow(topActivity, uid, bas); - } final BlockActivityStart[] out = { bas }; taskFragment.forOtherAdjacentTaskFragments(adjacentTaskFragment -> { final ActivityRecord top = adjacentTaskFragment.getActivity(topOfStackPredicate); diff --git a/services/core/java/com/android/server/wm/BaseAppSnapshotPersister.java b/services/core/java/com/android/server/wm/BaseAppSnapshotPersister.java index 5db02dff8351..aaa5a0074506 100644 --- a/services/core/java/com/android/server/wm/BaseAppSnapshotPersister.java +++ b/services/core/java/com/android/server/wm/BaseAppSnapshotPersister.java @@ -16,10 +16,20 @@ package com.android.server.wm; +import static com.android.server.wm.AbsAppSnapshotController.TAG; + import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.window.TaskSnapshot; +import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; + import java.io.File; +import java.util.UUID; class BaseAppSnapshotPersister { static final String LOW_RES_FILE_POSTFIX = "_reduced"; @@ -29,6 +39,7 @@ class BaseAppSnapshotPersister { // Shared with SnapshotPersistQueue protected final Object mLock; protected final SnapshotPersistQueue mSnapshotPersistQueue; + @VisibleForTesting protected final PersistInfoProvider mPersistInfoProvider; BaseAppSnapshotPersister(SnapshotPersistQueue persistQueue, @@ -79,6 +90,8 @@ class BaseAppSnapshotPersister { private final boolean mEnableLowResSnapshots; private final float mLowResScaleFactor; private final boolean mUse16BitFormat; + private final SparseBooleanArray mInitializedUsers = new SparseBooleanArray(); + private final SparseArray<File> mScrambleDirectories = new SparseArray<>(); PersistInfoProvider(DirectoryResolver directoryResolver, String dirName, boolean enableLowResSnapshots, float lowResScaleFactor, boolean use16BitFormat) { @@ -91,9 +104,80 @@ class BaseAppSnapshotPersister { @NonNull File getDirectory(int userId) { + if (Flags.scrambleSnapshotFileName()) { + final File directory = getOrInitScrambleDirectory(userId); + if (directory != null) { + return directory; + } + } + return getBaseDirectory(userId); + } + + @NonNull + private File getBaseDirectory(int userId) { return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), mDirName); } + @Nullable + private File getOrInitScrambleDirectory(int userId) { + synchronized (mScrambleDirectories) { + if (mInitializedUsers.get(userId)) { + return mScrambleDirectories.get(userId); + } + mInitializedUsers.put(userId, true); + final File scrambledDirectory = getScrambleDirectory(userId); + final File baseDir = getBaseDirectory(userId); + String newName = null; + // If directory exists, rename + if (scrambledDirectory.exists()) { + newName = UUID.randomUUID().toString(); + final File scrambleTo = new File(baseDir, newName); + if (!scrambledDirectory.renameTo(scrambleTo)) { + Slog.w(TAG, "SnapshotPersister rename scramble folder fail."); + return null; + } + } else { + // If directory not exists, mkDir. + if (!baseDir.exists() && !baseDir.mkdir()) { + Slog.w(TAG, "SnapshotPersister make base folder fail."); + return null; + } + if (!scrambledDirectory.mkdir()) { + Slog.e(TAG, "SnapshotPersister make scramble folder fail"); + return null; + } + // Move any existing files to this folder. + final String[] files = baseDir.list(); + if (files != null) { + for (String file : files) { + final File original = new File(baseDir, file); + if (original.isDirectory()) { + newName = file; + } else { + File to = new File(scrambledDirectory, file); + original.renameTo(to); + } + } + } + } + final File newFolder = new File(baseDir, newName); + mScrambleDirectories.put(userId, newFolder); + return newFolder; + } + } + + @NonNull + private File getScrambleDirectory(int userId) { + final File dir = getBaseDirectory(userId); + final String[] directories = dir.list( + (current, name) -> new File(current, name).isDirectory()); + if (directories != null && directories.length > 0) { + return new File(dir, directories[0]); + } else { + return new File(dir, UUID.randomUUID().toString()); + } + } + /** * Return if task snapshots are stored in 16 bit pixel format. * diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java index f473b7b7e4fb..fcc697242ff6 100644 --- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java +++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java @@ -18,7 +18,7 @@ package com.android.server.wm; import static android.view.WindowManager.TRANSIT_CHANGE; -import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS; +import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS_MIN; import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY; import static com.android.server.wm.utils.DisplayInfoOverrides.WM_OVERRIDE_FIELDS; import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields; @@ -140,8 +140,9 @@ class DeferredDisplayUpdater { if (displayInfoDiff == DIFF_EVERYTHING || !mDisplayContent.getLastHasContent() || !mDisplayContent.mTransitionController.isShellTransitionsEnabled()) { - ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS, - "DeferredDisplayUpdater: applying DisplayInfo immediately"); + ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN, + "DeferredDisplayUpdater: applying DisplayInfo(%d x %d) immediately", + displayInfo.logicalWidth, displayInfo.logicalHeight); mLastWmDisplayInfo = displayInfo; applyLatestDisplayInfo(); @@ -151,17 +152,23 @@ class DeferredDisplayUpdater { // If there are non WM-specific display info changes, apply only these fields immediately if ((displayInfoDiff & DIFF_NOT_WM_DEFERRABLE) > 0) { - ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS, - "DeferredDisplayUpdater: partially applying DisplayInfo immediately"); + ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN, + "DeferredDisplayUpdater: partially applying DisplayInfo(%d x %d) immediately", + displayInfo.logicalWidth, displayInfo.logicalHeight); applyLatestDisplayInfo(); } // If there are WM-specific display info changes, apply them through a Shell transition if ((displayInfoDiff & DIFF_WM_DEFERRABLE) > 0) { - ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS, - "DeferredDisplayUpdater: deferring DisplayInfo update"); + ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN, + "DeferredDisplayUpdater: deferring DisplayInfo(%d x %d) update", + displayInfo.logicalWidth, displayInfo.logicalHeight); requestDisplayChangeTransition(physicalDisplayUpdated, () -> { + ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN, + "DeferredDisplayUpdater: applying DisplayInfo(%d x %d) after deferring", + displayInfo.logicalWidth, displayInfo.logicalHeight); + // Apply deferrable fields to DisplayContent only when the transition // starts collecting, non-deferrable fields are ignored in mLastWmDisplayInfo mLastWmDisplayInfo = displayInfo; @@ -199,7 +206,7 @@ class DeferredDisplayUpdater { mDisplayContent.getDisplayPolicy().getNotificationShade(); if (notificationShade != null && notificationShade.isVisible() && mDisplayContent.mAtmService.mKeyguardController.isKeyguardOrAodShowing( - mDisplayContent.mDisplayId)) { + mDisplayContent.mDisplayId)) { Slog.i(TAG, notificationShade + " uses blast for display switch"); notificationShade.mSyncMethodOverride = BLASTSyncEngine.METHOD_BLAST; } @@ -209,9 +216,6 @@ class DeferredDisplayUpdater { try { onStartCollect.run(); - ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS, - "DeferredDisplayUpdater: applied DisplayInfo after deferring"); - if (physicalDisplayUpdated) { onDisplayUpdated(transition, fromRotation, startBounds); } else { diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java index d466a646c7dd..ddcb5eccb1d8 100644 --- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java +++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java @@ -19,6 +19,12 @@ package com.android.server.wm; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; +import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE; +import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK; +import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; @@ -131,6 +137,18 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { return RESULT_SKIP; } + if (DesktopModeFlags.INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES.isTrue()) { + ActivityRecord topVisibleFreeformActivity = + task.getDisplayContent().getTopMostVisibleFreeformActivity(); + if (shouldInheritExistingTaskBounds(topVisibleFreeformActivity, activity, task)) { + appendLog("inheriting bounds from existing closing instance"); + outParams.mBounds.set(topVisibleFreeformActivity.getBounds()); + appendLog("final desktop mode task bounds set to %s", outParams.mBounds); + // Return result done to prevent other modifiers from changing or cascading bounds. + return RESULT_DONE; + } + } + DesktopModeBoundsCalculator.updateInitialBounds(task, layout, activity, options, outParams.mBounds, this::appendLog); appendLog("final desktop mode task bounds set to %s", outParams.mBounds); @@ -159,7 +177,7 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { // activity will also enter desktop mode. On this same relationship, we can also assume // if there are not visible freeform tasks but a freeform activity is now launching, it // will force the device into desktop mode. - return (task.getDisplayContent().getTopMostVisibleFreeformActivity() != null + return (task.getDisplayContent().getTopMostFreeformActivity() != null && checkSourceWindowModesCompatible(task, options, currentParams)) || isRequestingFreeformWindowMode(task, options, currentParams); } @@ -201,6 +219,40 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { }; } + /** + * Whether the launching task should inherit the task bounds of an existing closing instance. + */ + private boolean shouldInheritExistingTaskBounds( + @Nullable ActivityRecord existingTaskActivity, + @Nullable ActivityRecord launchingActivity, + @NonNull Task launchingTask) { + if (existingTaskActivity == null || launchingActivity == null) return false; + return (existingTaskActivity.packageName == launchingActivity.packageName) + && isLaunchingNewTask(launchingActivity.launchMode, + launchingTask.getBaseIntent().getFlags()) + && isClosingExitingInstance(launchingTask.getBaseIntent().getFlags()); + } + + /** + * Returns true if the launch mode or intent will result in a new task being created for the + * activity. + */ + private boolean isLaunchingNewTask(int launchMode, int intentFlags) { + return launchMode == LAUNCH_SINGLE_TASK + || launchMode == LAUNCH_SINGLE_INSTANCE + || launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK + || (intentFlags & FLAG_ACTIVITY_NEW_TASK) != 0; + } + + /** + * Returns true if the intent will result in an existing task instance being closed if a new + * one appears. + */ + private boolean isClosingExitingInstance(int intentFlags) { + return (intentFlags & FLAG_ACTIVITY_CLEAR_TASK) != 0 + || (intentFlags & FLAG_ACTIVITY_MULTIPLE_TASK) == 0; + } + private void initLogBuilder(Task task, ActivityRecord activity) { if (DEBUG) { mLogBuilder = new StringBuilder( diff --git a/services/core/java/com/android/server/wm/DisplayWindowListenerController.java b/services/core/java/com/android/server/wm/DisplayWindowListenerController.java index fa7a99d55896..d90fff229cd9 100644 --- a/services/core/java/com/android/server/wm/DisplayWindowListenerController.java +++ b/services/core/java/com/android/server/wm/DisplayWindowListenerController.java @@ -40,14 +40,14 @@ class DisplayWindowListenerController { } int[] registerListener(IDisplayWindowListener listener) { + mDisplayListeners.register(listener); + final IntArray displayIds = new IntArray(); synchronized (mService.mGlobalLock) { - mDisplayListeners.register(listener); - final IntArray displayIds = new IntArray(); mService.mAtmService.mRootWindowContainer.forAllDisplays((displayContent) -> { displayIds.add(displayContent.mDisplayId); }); - return displayIds.toArray(); } + return displayIds.toArray(); } void unregisterListener(IDisplayWindowListener listener) { diff --git a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java index a017a1173d97..e508a6d23178 100644 --- a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java +++ b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java @@ -23,8 +23,6 @@ import static com.android.server.wm.Task.TAG_VISIBILITY; import android.annotation.Nullable; import android.util.Slog; -import com.android.window.flags.Flags; - import java.util.ArrayList; /** Helper class to ensure activities are in the right visible state for a container. */ @@ -112,18 +110,11 @@ class EnsureActivitiesVisibleHelper { if (adjacentTaskFragments != null && adjacentTaskFragments.contains( childTaskFragment)) { - final boolean isTranslucent; - if (Flags.allowMultipleAdjacentTaskFragments()) { - isTranslucent = childTaskFragment.isTranslucent(starting) - || childTaskFragment.forOtherAdjacentTaskFragments( - adjacentTaskFragment -> { - return adjacentTaskFragment.isTranslucent(starting); - }); - } else { - isTranslucent = childTaskFragment.isTranslucent(starting) - || childTaskFragment.getAdjacentTaskFragment() - .isTranslucent(starting); - } + final boolean isTranslucent = childTaskFragment.isTranslucent(starting) + || childTaskFragment.forOtherAdjacentTaskFragments( + adjacentTaskFragment -> { + return adjacentTaskFragment.isTranslucent(starting); + }); if (!isTranslucent) { // Everything behind two adjacent TaskFragments are occluded. mBehindFullyOccludedContainer = true; @@ -135,14 +126,10 @@ class EnsureActivitiesVisibleHelper { if (adjacentTaskFragments == null) { adjacentTaskFragments = new ArrayList<>(); } - if (Flags.allowMultipleAdjacentTaskFragments()) { - final ArrayList<TaskFragment> adjacentTfs = adjacentTaskFragments; - childTaskFragment.forOtherAdjacentTaskFragments(adjacentTf -> { - adjacentTfs.add(adjacentTf); - }); - } else { - adjacentTaskFragments.add(childTaskFragment.getAdjacentTaskFragment()); - } + final ArrayList<TaskFragment> adjacentTfs = adjacentTaskFragments; + childTaskFragment.forOtherAdjacentTaskFragments(adjacentTf -> { + adjacentTfs.add(adjacentTf); + }); } } else if (child.asActivityRecord() != null) { setActivityVisibilityState(child.asActivityRecord(), starting, resumeTopActivity); diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java index 2cac63c1e5e9..a937691e7998 100644 --- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java @@ -263,8 +263,8 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { boolean oldVisibility = mSource.isVisible(); super.updateVisibility(); if (Flags.refactorInsetsController()) { - if (mSource.isVisible() && !oldVisibility && mImeRequester != null) { - reportImeDrawnForOrganizerIfNeeded(mImeRequester); + if (mSource.isVisible() && !oldVisibility && mControlTarget != null) { + reportImeDrawnForOrganizerIfNeeded(mControlTarget); } } onSourceChanged(); diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java index 7751ac3f9fc6..a4bc5cbcb5d3 100644 --- a/services/core/java/com/android/server/wm/InputManagerCallback.java +++ b/services/core/java/com/android/server/wm/InputManagerCallback.java @@ -343,6 +343,13 @@ final class InputManagerCallback implements InputManagerService.WindowManagerCal } } + @Override + public boolean isKeyguardLocked(int displayId) { + synchronized (mService.mGlobalLock) { + return mService.mAtmService.mKeyguardController.isKeyguardLocked(displayId); + } + } + /** Waits until the built-in input devices have been configured. */ public boolean waitForInputDevicesReady(long timeoutMillis) { synchronized (mInputDevicesReadyMonitor) { diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index cf201c9f34f0..609302ce3f56 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -1732,26 +1732,14 @@ class RootWindowContainer extends WindowContainer<DisplayContent> activityAssistInfos.clear(); activityAssistInfos.add(new ActivityAssistInfo(top)); // Check if the activity on the split screen. - if (Flags.allowMultipleAdjacentTaskFragments()) { - top.getTask().forOtherAdjacentTasks(task -> { - final ActivityRecord adjacentActivityRecord = - task.getTopNonFinishingActivity(); - if (adjacentActivityRecord != null) { - activityAssistInfos.add( - new ActivityAssistInfo(adjacentActivityRecord)); - } - }); - } else { - final Task adjacentTask = top.getTask().getAdjacentTask(); - if (adjacentTask != null) { - final ActivityRecord adjacentActivityRecord = - adjacentTask.getTopNonFinishingActivity(); - if (adjacentActivityRecord != null) { - activityAssistInfos.add( - new ActivityAssistInfo(adjacentActivityRecord)); - } + top.getTask().forOtherAdjacentTasks(task -> { + final ActivityRecord adjacentActivityRecord = + task.getTopNonFinishingActivity(); + if (adjacentActivityRecord != null) { + activityAssistInfos.add( + new ActivityAssistInfo(adjacentActivityRecord)); } - } + }); if (rootTask == topFocusedRootTask) { topVisibleActivities.addAll(0, activityAssistInfos); } else { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index d16c301cec40..e98b2b749af8 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -2461,21 +2461,6 @@ class Task extends TaskFragment { return parentTask == null ? null : parentTask.getCreatedByOrganizerTask(); } - /** @deprecated b/373709676 replace with {@link #forOtherAdjacentTasks(Consumer)} ()}. */ - @Deprecated - @Nullable - Task getAdjacentTask() { - if (Flags.allowMultipleAdjacentTaskFragments()) { - throw new IllegalStateException("allowMultipleAdjacentTaskFragments is enabled. " - + "Use #forOtherAdjacentTasks instead"); - } - final Task taskWithAdjacent = getTaskWithAdjacent(); - if (taskWithAdjacent == null) { - return null; - } - return taskWithAdjacent.getAdjacentTaskFragment().asTask(); - } - /** Finds the first Task parent (or itself) that has adjacent. */ @Nullable Task getTaskWithAdjacent() { @@ -2499,11 +2484,6 @@ class Task extends TaskFragment { * Tasks. The invoke order is not guaranteed. */ void forOtherAdjacentTasks(@NonNull Consumer<Task> callback) { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - throw new IllegalStateException("allowMultipleAdjacentTaskFragments is not enabled. " - + "Use #getAdjacentTask instead"); - } - final Task taskWithAdjacent = getTaskWithAdjacent(); if (taskWithAdjacent == null) { return; @@ -2521,10 +2501,6 @@ class Task extends TaskFragment { * guaranteed. */ boolean forOtherAdjacentTasks(@NonNull Predicate<Task> callback) { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - throw new IllegalStateException("allowMultipleAdjacentTaskFragments is not enabled. " - + "Use getAdjacentTask instead"); - } final Task taskWithAdjacent = getTaskWithAdjacent(); if (taskWithAdjacent == null) { return false; @@ -3651,20 +3627,13 @@ class Task extends TaskFragment { final TaskFragment taskFragment = wc.asTaskFragment(); if (taskFragment != null && taskFragment.isEmbedded() && taskFragment.hasAdjacentTaskFragment()) { - if (Flags.allowMultipleAdjacentTaskFragments()) { - final int[] nextLayer = { layer }; - taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> { - if (adjacentTf.shouldBoostDimmer()) { - adjacentTf.assignLayer(t, nextLayer[0]++); - } - }); - layer = nextLayer[0]; - } else { - final TaskFragment adjacentTf = taskFragment.getAdjacentTaskFragment(); + final int[] nextLayer = { layer }; + taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> { if (adjacentTf.shouldBoostDimmer()) { - adjacentTf.assignLayer(t, layer++); + adjacentTf.assignLayer(t, nextLayer[0]++); } - } + }); + layer = nextLayer[0]; } // Place the decor surface just above the owner TaskFragment. diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index 1966ecf57c73..fb7bab4b3e26 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -60,7 +60,6 @@ import com.android.internal.util.function.pooled.PooledLambda; import com.android.internal.util.function.pooled.PooledPredicate; import com.android.server.pm.UserManagerInternal; import com.android.server.wm.LaunchParamsController.LaunchParams; -import com.android.window.flags.Flags; import java.io.PrintWriter; import java.util.ArrayList; @@ -1089,19 +1088,14 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { // Use launch-adjacent-flag-root if launching with launch-adjacent flag. if ((launchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0 && mLaunchAdjacentFlagRootTask != null) { - final Task launchAdjacentRootAdjacentTask; - if (Flags.allowMultipleAdjacentTaskFragments()) { - final Task[] tmpTask = new Task[1]; - mLaunchAdjacentFlagRootTask.forOtherAdjacentTasks(task -> { - // TODO(b/382208145): enable FLAG_ACTIVITY_LAUNCH_ADJACENT for 3+. - // Find the first adjacent for now. - tmpTask[0] = task; - return true; - }); - launchAdjacentRootAdjacentTask = tmpTask[0]; - } else { - launchAdjacentRootAdjacentTask = mLaunchAdjacentFlagRootTask.getAdjacentTask(); - } + final Task[] tmpTask = new Task[1]; + mLaunchAdjacentFlagRootTask.forOtherAdjacentTasks(task -> { + // TODO(b/382208145): enable FLAG_ACTIVITY_LAUNCH_ADJACENT for 3+. + // Find the first adjacent for now. + tmpTask[0] = task; + return true; + }); + final Task launchAdjacentRootAdjacentTask = tmpTask[0]; if (sourceTask != null && (sourceTask == candidateTask || sourceTask.topRunningActivity() == null)) { // Do nothing when task that is getting opened is same as the source or when @@ -1129,14 +1123,6 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { if (launchRootTask == null || sourceTask == null) { return launchRootTask; } - if (!Flags.allowMultipleAdjacentTaskFragments()) { - final Task adjacentRootTask = launchRootTask.getAdjacentTask(); - if (adjacentRootTask != null && (sourceTask == adjacentRootTask - || sourceTask.isDescendantOf(adjacentRootTask))) { - return adjacentRootTask; - } - return launchRootTask; - } final Task[] adjacentRootTask = new Task[1]; launchRootTask.forOtherAdjacentTasks(task -> { if (sourceTask == task || sourceTask.isDescendantOf(task)) { @@ -1163,24 +1149,16 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { return sourceTask.getCreatedByOrganizerTask(); } // Check if the candidate is already positioned in the adjacent Task. - if (Flags.allowMultipleAdjacentTaskFragments()) { - final Task[] adjacentRootTask = new Task[1]; - sourceTask.forOtherAdjacentTasks(task -> { - if (candidateTask == task || candidateTask.isDescendantOf(task)) { - adjacentRootTask[0] = task; - return true; - } - return false; - }); - if (adjacentRootTask[0] != null) { - return adjacentRootTask[0]; - } - } else { - final Task adjacentTarget = taskWithAdjacent.getAdjacentTask(); - if (candidateTask == adjacentTarget - || candidateTask.isDescendantOf(adjacentTarget)) { - return adjacentTarget; + final Task[] adjacentRootTask = new Task[1]; + sourceTask.forOtherAdjacentTasks(task -> { + if (candidateTask == task || candidateTask.isDescendantOf(task)) { + adjacentRootTask[0] = task; + return true; } + return false; + }); + if (adjacentRootTask[0] != null) { + return adjacentRootTask[0]; } return sourceTask.getCreatedByOrganizerTask(); } diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 5183c6b57f15..2dabb253744a 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -235,11 +235,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { /** This task fragment will be removed when the cleanup of its children are done. */ private boolean mIsRemovalRequested; - /** @deprecated b/373709676 replace with {@link #mAdjacentTaskFragments} */ - @Deprecated - @Nullable - private TaskFragment mAdjacentTaskFragment; - /** * The TaskFragments that are adjacent to each other, including this TaskFragment. * All TaskFragments in this set share the same set instance. @@ -455,22 +450,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { return service.mWindowOrganizerController.getTaskFragment(token); } - /** @deprecated b/373709676 replace with {@link #setAdjacentTaskFragments}. */ - @Deprecated - void setAdjacentTaskFragment(@NonNull TaskFragment taskFragment) { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - if (mAdjacentTaskFragment == taskFragment) { - return; - } - resetAdjacentTaskFragment(); - mAdjacentTaskFragment = taskFragment; - taskFragment.setAdjacentTaskFragment(this); - return; - } - - setAdjacentTaskFragments(new AdjacentSet(this, taskFragment)); - } - void setAdjacentTaskFragments(@NonNull AdjacentSet adjacentTaskFragments) { adjacentTaskFragments.setAsAdjacent(); } @@ -483,56 +462,18 @@ class TaskFragment extends WindowContainer<WindowContainer> { return mCompanionTaskFragment; } - /** @deprecated b/373709676 replace with {@link #clearAdjacentTaskFragments()}. */ - @Deprecated - private void resetAdjacentTaskFragment() { - if (Flags.allowMultipleAdjacentTaskFragments()) { - throw new IllegalStateException("resetAdjacentTaskFragment shouldn't be called when" - + " allowMultipleAdjacentTaskFragments is enabled. Use either" - + " #clearAdjacentTaskFragments or #removeFromAdjacentTaskFragments"); - } - // Reset the adjacent TaskFragment if its adjacent TaskFragment is also this TaskFragment. - if (mAdjacentTaskFragment != null && mAdjacentTaskFragment.mAdjacentTaskFragment == this) { - mAdjacentTaskFragment.mAdjacentTaskFragment = null; - mAdjacentTaskFragment.mDelayLastActivityRemoval = false; - } - mAdjacentTaskFragment = null; - mDelayLastActivityRemoval = false; - } - void clearAdjacentTaskFragments() { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - resetAdjacentTaskFragment(); - return; - } - if (mAdjacentTaskFragments != null) { mAdjacentTaskFragments.clear(); } } void removeFromAdjacentTaskFragments() { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - resetAdjacentTaskFragment(); - return; - } - if (mAdjacentTaskFragments != null) { mAdjacentTaskFragments.remove(this); } } - /** @deprecated b/373709676 replace with {@link #getAdjacentTaskFragments()}. */ - @Deprecated - @Nullable - TaskFragment getAdjacentTaskFragment() { - if (Flags.allowMultipleAdjacentTaskFragments()) { - throw new IllegalStateException("allowMultipleAdjacentTaskFragments is enabled. " - + "Use #getAdjacentTaskFragments instead"); - } - return mAdjacentTaskFragment; - } - @Nullable AdjacentSet getAdjacentTaskFragments() { return mAdjacentTaskFragments; @@ -561,16 +502,10 @@ class TaskFragment extends WindowContainer<WindowContainer> { } boolean hasAdjacentTaskFragment() { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - return mAdjacentTaskFragment != null; - } return mAdjacentTaskFragments != null; } boolean isAdjacentTo(@NonNull TaskFragment other) { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - return mAdjacentTaskFragment == other; - } return other != this && mAdjacentTaskFragments != null && mAdjacentTaskFragments.contains(other); @@ -1377,21 +1312,12 @@ class TaskFragment extends WindowContainer<WindowContainer> { if (taskFragment.isAdjacentTo(this)) { continue; } - if (Flags.allowMultipleAdjacentTaskFragments()) { - final boolean isOccluding = mTmpRect.intersect(taskFragment.getBounds()) - || taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> { - return mTmpRect.intersect(adjacentTf.getBounds()); - }); - if (isOccluding) { - return TASK_FRAGMENT_VISIBILITY_INVISIBLE; - } - } else { - final TaskFragment adjacentTaskFragment = - taskFragment.mAdjacentTaskFragment; - if (mTmpRect.intersect(taskFragment.getBounds()) - || mTmpRect.intersect(adjacentTaskFragment.getBounds())) { - return TASK_FRAGMENT_VISIBILITY_INVISIBLE; - } + final boolean isOccluding = mTmpRect.intersect(taskFragment.getBounds()) + || taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> { + return mTmpRect.intersect(adjacentTf.getBounds()); + }); + if (isOccluding) { + return TASK_FRAGMENT_VISIBILITY_INVISIBLE; } } } @@ -1427,37 +1353,22 @@ class TaskFragment extends WindowContainer<WindowContainer> { // 2. Adjacent TaskFragments do not overlap, so that if this TaskFragment is behind // any translucent TaskFragment in the adjacent set, then this TaskFragment is // visible behind translucent. - if (Flags.allowMultipleAdjacentTaskFragments()) { - final boolean hasTraversedAdj = otherTaskFrag.forOtherAdjacentTaskFragments( - adjacentTaskFragments::contains); - if (hasTraversedAdj) { - final boolean isTranslucent = - isBehindTransparentTaskFragment(otherTaskFrag, starting) - || otherTaskFrag.forOtherAdjacentTaskFragments( - (Predicate<TaskFragment>) tf -> - isBehindTransparentTaskFragment(tf, starting)); - if (isTranslucent) { - // Can be visible behind a translucent adjacent TaskFragments. - gotTranslucentFullscreen = true; - gotTranslucentAdjacent = true; - continue; - } - // Can not be visible behind adjacent TaskFragments. - return TASK_FRAGMENT_VISIBILITY_INVISIBLE; - } - } else { - if (adjacentTaskFragments.contains(otherTaskFrag.mAdjacentTaskFragment)) { - if (isBehindTransparentTaskFragment(otherTaskFrag, starting) - || isBehindTransparentTaskFragment( - otherTaskFrag.mAdjacentTaskFragment, starting)) { - // Can be visible behind a translucent adjacent TaskFragments. - gotTranslucentFullscreen = true; - gotTranslucentAdjacent = true; - continue; - } - // Can not be visible behind adjacent TaskFragments. - return TASK_FRAGMENT_VISIBILITY_INVISIBLE; + final boolean hasTraversedAdj = otherTaskFrag.forOtherAdjacentTaskFragments( + adjacentTaskFragments::contains); + if (hasTraversedAdj) { + final boolean isTranslucent = + isBehindTransparentTaskFragment(otherTaskFrag, starting) + || otherTaskFrag.forOtherAdjacentTaskFragments( + (Predicate<TaskFragment>) tf -> + isBehindTransparentTaskFragment(tf, starting)); + if (isTranslucent) { + // Can be visible behind a translucent adjacent TaskFragments. + gotTranslucentFullscreen = true; + gotTranslucentAdjacent = true; + continue; } + // Can not be visible behind adjacent TaskFragments. + return TASK_FRAGMENT_VISIBILITY_INVISIBLE; } adjacentTaskFragments.add(otherTaskFrag); } @@ -3299,40 +3210,23 @@ class TaskFragment extends WindowContainer<WindowContainer> { final ArrayList<WindowContainer> siblings = getParent().mChildren; final int zOrder = siblings.indexOf(this); - - if (!Flags.allowMultipleAdjacentTaskFragments()) { - if (siblings.indexOf(getAdjacentTaskFragment()) < zOrder) { - // early return if this TF already has higher z-ordering. - return false; - } - } else { - final boolean hasAdjacentOnTop = forOtherAdjacentTaskFragments( - tf -> siblings.indexOf(tf) > zOrder); - if (!hasAdjacentOnTop) { - // early return if this TF already has higher z-ordering. - return false; - } + final boolean hasAdjacentOnTop = forOtherAdjacentTaskFragments( + tf -> siblings.indexOf(tf) > zOrder); + if (!hasAdjacentOnTop) { + // early return if this TF already has higher z-ordering. + return false; } final ToBooleanFunction<WindowState> getDimBehindWindow = (w) -> (w.mAttrs.flags & FLAG_DIM_BEHIND) != 0 && w.mActivityRecord != null && w.mActivityRecord.isEmbedded() && (w.mActivityRecord.isVisibleRequested() || w.mActivityRecord.isVisible()); - - if (!Flags.allowMultipleAdjacentTaskFragments()) { - final TaskFragment adjacentTf = getAdjacentTaskFragment(); - if (adjacentTf.forAllWindows(getDimBehindWindow, true)) { - // early return if the adjacent Tf has a dimming window. - return false; - } - } else { - final boolean adjacentHasDimmingWindow = forOtherAdjacentTaskFragments(tf -> { - return tf.forAllWindows(getDimBehindWindow, true); - }); - if (adjacentHasDimmingWindow) { - // early return if the adjacent Tf has a dimming window. - return false; - } + final boolean adjacentHasDimmingWindow = forOtherAdjacentTaskFragments(tf -> { + return tf.forAllWindows(getDimBehindWindow, true); + }); + if (adjacentHasDimmingWindow) { + // early return if the adjacent Tf has a dimming window. + return false; } // boost if there's an Activity window that has FLAG_DIM_BEHIND flag. @@ -3456,16 +3350,9 @@ class TaskFragment extends WindowContainer<WindowContainer> { sb.append(" organizerProc="); sb.append(mTaskFragmentOrganizerProcessName); } - if (Flags.allowMultipleAdjacentTaskFragments()) { - if (mAdjacentTaskFragments != null) { - sb.append(" adjacent="); - sb.append(mAdjacentTaskFragments); - } - } else { - if (mAdjacentTaskFragment != null) { - sb.append(" adjacent="); - sb.append(mAdjacentTaskFragment); - } + if (mAdjacentTaskFragments != null) { + sb.append(" adjacent="); + sb.append(mAdjacentTaskFragments); } sb.append('}'); return sb.toString(); @@ -3591,10 +3478,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { } AdjacentSet(@NonNull ArraySet<TaskFragment> taskFragments) { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - throw new IllegalStateException("allowMultipleAdjacentTaskFragments must be" - + " enabled to set more than two TaskFragments adjacent to each other."); - } final int size = taskFragments.size(); if (size < 2) { throw new IllegalArgumentException("Adjacent TaskFragments must contain at least" diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index c78cdaa10df2..803c21ccab6e 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -2589,9 +2589,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } // When the TaskFragment has an adjacent TaskFragment, sibling behind them should be // hidden unless any of them are translucent. - if (!Flags.allowMultipleAdjacentTaskFragments()) { - return taskFragment.getAdjacentTaskFragment().isTranslucentForTransition(); - } return taskFragment.forOtherAdjacentTaskFragments(TaskFragment::isTranslucentForTransition); } diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 3a4d9d27f65a..e1553cd37d03 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -57,7 +57,8 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; import com.android.internal.util.ToBooleanFunction; -import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils; +import com.android.server.wallpaper.WallpaperCropper; +import com.android.server.wallpaper.WallpaperDefaultDisplayInfo; import java.io.PrintWriter; import java.util.ArrayList; @@ -71,7 +72,6 @@ import java.util.function.Consumer; class WallpaperController { private static final String TAG = TAG_WITH_CLASS_NAME ? "WallpaperController" : TAG_WM; private WindowManagerService mService; - private WallpaperCropUtils mWallpaperCropUtils = null; private DisplayContent mDisplayContent; // Larger index has higher z-order. @@ -116,6 +116,10 @@ class WallpaperController { private boolean mShouldOffsetWallpaperCenter; + // This is for WallpaperCropper, which has cropping logic for the default display only. + // TODO(b/400685784) make the WallpaperCropper operate on every display independently + private final WallpaperDefaultDisplayInfo mDefaultDisplayInfo; + private final ToBooleanFunction<WindowState> mFindWallpaperTargetFunction = w -> { final ActivityRecord ar = w.mActivityRecord; // The animating window can still be visible on screen if it is in transition, so we @@ -198,12 +202,14 @@ class WallpaperController { WallpaperController(WindowManagerService service, DisplayContent displayContent) { mService = service; mDisplayContent = displayContent; + WindowManager windowManager = service.mContext.getSystemService(WindowManager.class); Resources resources = service.mContext.getResources(); mMinWallpaperScale = resources.getFloat(com.android.internal.R.dimen.config_wallpaperMinScale); mMaxWallpaperScale = resources.getFloat(R.dimen.config_wallpaperMaxScale); mShouldOffsetWallpaperCenter = resources.getBoolean( com.android.internal.R.bool.config_offsetWallpaperToCenterOfLargestDisplay); + mDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(windowManager, resources); } void resetLargestDisplay(Display display) { @@ -246,10 +252,6 @@ class WallpaperController { return largestDisplaySize; } - void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils) { - mWallpaperCropUtils = wallpaperCropUtils; - } - WindowState getWallpaperTarget() { return mWallpaperTarget; } @@ -352,16 +354,12 @@ class WallpaperController { int offsetY; if (multiCrop()) { - if (mWallpaperCropUtils == null) { - Slog.e(TAG, "Update wallpaper offsets before the system is ready. Aborting"); - return false; - } Point bitmapSize = new Point( wallpaperWin.mRequestedWidth, wallpaperWin.mRequestedHeight); SparseArray<Rect> cropHints = token.getCropHints(); wallpaperFrame = bitmapSize.x <= 0 || bitmapSize.y <= 0 ? wallpaperWin.getFrame() - : mWallpaperCropUtils.getCrop(screenSize, bitmapSize, cropHints, - wallpaperWin.isRtl()); + : WallpaperCropper.getCrop(screenSize, mDefaultDisplayInfo, bitmapSize, + cropHints, wallpaperWin.isRtl()); int frameWidth = wallpaperFrame.width(); int frameHeight = wallpaperFrame.height(); float frameRatio = (float) frameWidth / frameHeight; diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 466ed7863c84..772a7fdfc684 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -2006,11 +2006,16 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return getActivity(r -> !r.finishing, true /* traverseTopToBottom */); } - ActivityRecord getTopMostVisibleFreeformActivity() { + ActivityRecord getTopMostFreeformActivity() { return getActivity(r -> r.isVisibleRequested() && r.inFreeformWindowingMode(), true /* traverseTopToBottom */); } + ActivityRecord getTopMostVisibleFreeformActivity() { + return getActivity(r -> r.isVisible() && r.inFreeformWindowingMode(), + true /* traverseTopToBottom */); + } + ActivityRecord getTopActivity(boolean includeFinishing, boolean includeOverlays) { // Break down into 4 calls to avoid object creation due to capturing input params. if (includeFinishing) { diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 4b5a3a031931..5f2a2ad7f0eb 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -54,7 +54,6 @@ import android.window.ScreenCapture.ScreenshotHardwareBuffer; import com.android.internal.policy.KeyInterceptionInfo; import com.android.server.input.InputManagerService; import com.android.server.policy.WindowManagerPolicy; -import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils; import com.android.server.wm.SensitiveContentPackages.PackageInfo; import java.lang.annotation.Retention; @@ -772,12 +771,6 @@ public abstract class WindowManagerInternal { public abstract void setWallpaperCropHints(IBinder windowToken, SparseArray<Rect> cropHints); /** - * Transmits the {@link WallpaperCropUtils} instance to {@link WallpaperController}. - * {@link WallpaperCropUtils} contains the helpers to properly position the wallpaper. - */ - public abstract void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils); - - /** * Returns {@code true} if a Window owned by {@code uid} has focus. */ public abstract boolean isUidFocused(int uid); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 9fc0339c52a2..c078d67b6cc6 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -356,7 +356,6 @@ import com.android.server.policy.WindowManagerPolicy; import com.android.server.policy.WindowManagerPolicy.ScreenOffListener; import com.android.server.power.ShutdownThread; import com.android.server.utils.PriorityDump; -import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils; import com.android.window.flags.Flags; import dalvik.annotation.optimization.NeverCompile; @@ -8100,12 +8099,6 @@ public class WindowManagerService extends IWindowManager.Stub } @Override - public void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils) { - mRoot.getDisplayContent(DEFAULT_DISPLAY).mWallpaperController - .setWallpaperCropUtils(wallpaperCropUtils); - } - - @Override public boolean isUidFocused(int uid) { synchronized (mGlobalLock) { for (int i = mRoot.getChildCount() - 1; i >= 0; i--) { @@ -9374,23 +9367,6 @@ public class WindowManagerService extends IWindowManager.Stub return focusedActivity; } - if (!Flags.allowMultipleAdjacentTaskFragments()) { - final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment(); - final ActivityRecord adjacentTopActivity = adjacentTaskFragment.topRunningActivity(); - if (adjacentTopActivity == null) { - // Return if no adjacent activity. - return focusedActivity; - } - - if (adjacentTopActivity.getLastWindowCreateTime() - < focusedActivity.getLastWindowCreateTime()) { - // Return if the current focus activity has more recently active window. - return focusedActivity; - } - - return adjacentTopActivity; - } - // Find the adjacent activity with more recently active window. final ActivityRecord[] mostRecentActiveActivity = { focusedActivity }; final long[] mostRecentActiveTime = { focusedActivity.getLastWindowCreateTime() }; @@ -9461,20 +9437,15 @@ public class WindowManagerService extends IWindowManager.Stub // No adjacent window. return false; } - final TaskFragment adjacentFragment; - if (Flags.allowMultipleAdjacentTaskFragments()) { - if (fromFragment.getAdjacentTaskFragments().size() > 2) { - throw new IllegalStateException("Not yet support 3+ adjacent for non-Task TFs"); - } - final TaskFragment[] tmpAdjacent = new TaskFragment[1]; - fromFragment.forOtherAdjacentTaskFragments(adjacentTF -> { - tmpAdjacent[0] = adjacentTF; - return true; - }); - adjacentFragment = tmpAdjacent[0]; - } else { - adjacentFragment = fromFragment.getAdjacentTaskFragment(); + if (fromFragment.getAdjacentTaskFragments().size() > 2) { + throw new IllegalStateException("Not yet support 3+ adjacent for non-Task TFs"); } + final TaskFragment[] tmpAdjacent = new TaskFragment[1]; + fromFragment.forOtherAdjacentTaskFragments(adjacentTF -> { + tmpAdjacent[0] = adjacentTF; + return true; + }); + final TaskFragment adjacentFragment = tmpAdjacent[0]; if (adjacentFragment.isIsolatedNav()) { // Don't move the focus if the adjacent TF is isolated navigation. 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 ea1f35a130b0..a012ec137892 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -1671,13 +1671,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } if (!taskFragment.isAdjacentTo(secondaryTaskFragment)) { // Only have lifecycle effect if the adjacent changed. - if (Flags.allowMultipleAdjacentTaskFragments()) { - // Activity Embedding only set two TFs adjacent. - taskFragment.setAdjacentTaskFragments( - new TaskFragment.AdjacentSet(taskFragment, secondaryTaskFragment)); - } else { - taskFragment.setAdjacentTaskFragment(secondaryTaskFragment); - } + // Activity Embedding only set two TFs adjacent. + taskFragment.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(taskFragment, secondaryTaskFragment)); effects |= TRANSACT_EFFECTS_LIFECYCLE; } @@ -2220,30 +2216,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } private int setAdjacentRootsHierarchyOp(WindowContainerTransaction.HierarchyOp hop) { - if (!Flags.allowMultipleAdjacentTaskFragments()) { - final WindowContainer wc1 = WindowContainer.fromBinder(hop.getContainer()); - if (wc1 == null || !wc1.isAttached()) { - Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc1); - return TRANSACT_EFFECTS_NONE; - } - final TaskFragment root1 = wc1.asTaskFragment(); - final WindowContainer wc2 = WindowContainer.fromBinder(hop.getAdjacentRoot()); - if (wc2 == null || !wc2.isAttached()) { - Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc2); - return TRANSACT_EFFECTS_NONE; - } - final TaskFragment root2 = wc2.asTaskFragment(); - if (!root1.mCreatedByOrganizer || !root2.mCreatedByOrganizer) { - throw new IllegalArgumentException("setAdjacentRootsHierarchyOp: Not created by" - + " organizer root1=" + root1 + " root2=" + root2); - } - if (root1.isAdjacentTo(root2)) { - return TRANSACT_EFFECTS_NONE; - } - root1.setAdjacentTaskFragment(root2); - return TRANSACT_EFFECTS_LIFECYCLE; - } - final IBinder[] containers = hop.getContainers(); final ArraySet<TaskFragment> adjacentRoots = new ArraySet<>(); for (IBinder container : containers) { diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index 95776088aad8..66d04df8095b 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -193,10 +193,6 @@ cc_defaults { "android.hardware.tv.input@1.0", "android.hardware.tv.input-V2-ndk", "android.hardware.vibrator-V3-ndk", - "android.hardware.vibrator@1.0", - "android.hardware.vibrator@1.1", - "android.hardware.vibrator@1.2", - "android.hardware.vibrator@1.3", "android.hardware.vr@1.0", "android.hidl.token@1.0-utils", "android.frameworks.schedulerservice@1.0", diff --git a/services/core/jni/com_android_server_display_DisplayControl.cpp b/services/core/jni/com_android_server_display_DisplayControl.cpp index aeae13d83809..c674f9037aa4 100644 --- a/services/core/jni/com_android_server_display_DisplayControl.cpp +++ b/services/core/jni/com_android_server_display_DisplayControl.cpp @@ -24,12 +24,13 @@ namespace android { static jobject nativeCreateVirtualDisplay(JNIEnv* env, jclass clazz, jstring nameObj, - jboolean secure, jstring uniqueIdStr, - jfloat requestedRefreshRate) { + jboolean secure, jboolean optimizeForPower, + jstring uniqueIdStr, jfloat requestedRefreshRate) { const ScopedUtfChars name(env, nameObj); const ScopedUtfChars uniqueId(env, uniqueIdStr); sp<IBinder> token(SurfaceComposerClient::createVirtualDisplay(std::string(name.c_str()), bool(secure), + bool(optimizeForPower), std::string(uniqueId.c_str()), requestedRefreshRate)); return javaObjectForIBinder(env, token); @@ -182,7 +183,7 @@ static jobject nativeGetPhysicalDisplayToken(JNIEnv* env, jclass clazz, jlong ph static const JNINativeMethod sDisplayMethods[] = { // clang-format off - {"nativeCreateVirtualDisplay", "(Ljava/lang/String;ZLjava/lang/String;F)Landroid/os/IBinder;", + {"nativeCreateVirtualDisplay", "(Ljava/lang/String;ZZLjava/lang/String;F)Landroid/os/IBinder;", (void*)nativeCreateVirtualDisplay }, {"nativeDestroyVirtualDisplay", "(Landroid/os/IBinder;)V", (void*)nativeDestroyVirtualDisplay }, diff --git a/services/core/jni/com_android_server_vibrator_VibratorController.cpp b/services/core/jni/com_android_server_vibrator_VibratorController.cpp index 534dbb1f6cf1..11dbbdfb850e 100644 --- a/services/core/jni/com_android_server_vibrator_VibratorController.cpp +++ b/services/core/jni/com_android_server_vibrator_VibratorController.cpp @@ -19,7 +19,6 @@ #include <aidl/android/hardware/vibrator/IVibrator.h> #include <android/binder_parcel.h> #include <android/binder_parcel_jni.h> -#include <android/hardware/vibrator/1.3/IVibrator.h> #include <android/persistable_bundle_aidl.h> #include <android_os_vibrator.h> #include <nativehelper/JNIHelp.h> @@ -32,8 +31,6 @@ #include "core_jni_helpers.h" #include "jni.h" -namespace V1_0 = android::hardware::vibrator::V1_0; -namespace V1_3 = android::hardware::vibrator::V1_3; namespace Aidl = aidl::android::hardware::vibrator; using aidl::android::os::PersistableBundle; @@ -80,31 +77,6 @@ static struct { jfieldID timeMillis; } sPwlePointClassInfo; -static_assert(static_cast<uint8_t>(V1_0::EffectStrength::LIGHT) == - static_cast<uint8_t>(Aidl::EffectStrength::LIGHT)); -static_assert(static_cast<uint8_t>(V1_0::EffectStrength::MEDIUM) == - static_cast<uint8_t>(Aidl::EffectStrength::MEDIUM)); -static_assert(static_cast<uint8_t>(V1_0::EffectStrength::STRONG) == - static_cast<uint8_t>(Aidl::EffectStrength::STRONG)); - -static_assert(static_cast<uint8_t>(V1_3::Effect::CLICK) == - static_cast<uint8_t>(Aidl::Effect::CLICK)); -static_assert(static_cast<uint8_t>(V1_3::Effect::DOUBLE_CLICK) == - static_cast<uint8_t>(Aidl::Effect::DOUBLE_CLICK)); -static_assert(static_cast<uint8_t>(V1_3::Effect::TICK) == static_cast<uint8_t>(Aidl::Effect::TICK)); -static_assert(static_cast<uint8_t>(V1_3::Effect::THUD) == static_cast<uint8_t>(Aidl::Effect::THUD)); -static_assert(static_cast<uint8_t>(V1_3::Effect::POP) == static_cast<uint8_t>(Aidl::Effect::POP)); -static_assert(static_cast<uint8_t>(V1_3::Effect::HEAVY_CLICK) == - static_cast<uint8_t>(Aidl::Effect::HEAVY_CLICK)); -static_assert(static_cast<uint8_t>(V1_3::Effect::RINGTONE_1) == - static_cast<uint8_t>(Aidl::Effect::RINGTONE_1)); -static_assert(static_cast<uint8_t>(V1_3::Effect::RINGTONE_2) == - static_cast<uint8_t>(Aidl::Effect::RINGTONE_2)); -static_assert(static_cast<uint8_t>(V1_3::Effect::RINGTONE_15) == - static_cast<uint8_t>(Aidl::Effect::RINGTONE_15)); -static_assert(static_cast<uint8_t>(V1_3::Effect::TEXTURE_TICK) == - static_cast<uint8_t>(Aidl::Effect::TEXTURE_TICK)); - static std::shared_ptr<vibrator::HalController> findVibrator(int32_t vibratorId) { vibrator::ManagerHalController* manager = android_server_vibrator_VibratorManagerService_getManager(); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 215d6ca964eb..51ed6bb2aa40 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -245,6 +245,7 @@ import static android.app.admin.ProvisioningException.ERROR_REMOVE_NON_REQUIRED_ import static android.app.admin.ProvisioningException.ERROR_SETTING_PROFILE_OWNER_FAILED; import static android.app.admin.ProvisioningException.ERROR_SET_DEVICE_OWNER_FAILED; import static android.app.admin.ProvisioningException.ERROR_STARTING_PROFILE_FAILED; +import static android.content.Context.RECEIVER_NOT_EXPORTED; import static android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE; import static android.content.Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; @@ -486,6 +487,7 @@ import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.telephony.data.ApnSetting; +import android.telephony.euicc.EuiccManager; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.ArrayMap; @@ -643,6 +645,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { static final String ACTION_PROFILE_OFF_DEADLINE = "com.android.server.ACTION_PROFILE_OFF_DEADLINE"; + /** Broadcast action invoked when a managed eSIM is removed while deleting work profile. */ + private static final String ACTION_ESIM_REMOVED_WITH_MANAGED_PROFILE = + "com.android.server.ACTION_ESIM_REMOVED_WITH_MANAGED_PROFILE"; + + /** Extra for the subscription ID of the managed eSIM removed while deleting work profile. */ + private static final String EXTRA_REMOVED_ESIM_SUBSCRIPTION_ID = + "com.android.server.EXTRA_ESIM_REMOVED_WITH_MANAGED_PROFILE_SUBSCRIPTION_ID"; + private static final String CALLED_FROM_PARENT = "calledFromParent"; private static final String NOT_CALLED_FROM_PARENT = "notCalledFromParent"; @@ -1266,6 +1276,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { removeCredentialManagementApp(intent.getData().getSchemeSpecificPart()); } else if (Intent.ACTION_MANAGED_PROFILE_ADDED.equals(action)) { clearWipeProfileNotification(); + } else if (Intent.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) { + removeManagedEmbeddedSubscriptionsForUser(userHandle); } else if (Intent.ACTION_DATE_CHANGED.equals(action) || Intent.ACTION_TIME_CHANGED.equals(action)) { // Update freeze period record when clock naturally progresses to the next day @@ -1298,6 +1310,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { triggerPolicyComplianceCheckIfNeeded(userHandle, suspended); } else if (LOGIN_ACCOUNTS_CHANGED_ACTION.equals(action)) { calculateHasIncompatibleAccounts(); + } else if (ACTION_ESIM_REMOVED_WITH_MANAGED_PROFILE.equals(action)) { + int removedSubscriptionId = intent.getIntExtra(EXTRA_REMOVED_ESIM_SUBSCRIPTION_ID, + -1); + Slogf.i(LOG_TAG, + "Deleted subscription with ID %d because owning managed profile was " + + "removed", + removedSubscriptionId); } } @@ -2219,9 +2238,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler); filter = new IntentFilter(); filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED); + filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED); filter.addAction(Intent.ACTION_TIME_CHANGED); filter.addAction(Intent.ACTION_DATE_CHANGED); mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler); + filter = new IntentFilter(); + filter.addAction(ACTION_ESIM_REMOVED_WITH_MANAGED_PROFILE); + mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, + mHandler, RECEIVER_NOT_EXPORTED); LocalServices.addService(DevicePolicyManagerInternal.class, mLocalService); @@ -3782,9 +3806,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { // Update user switcher message to activity manager. ActivityManagerInternal activityManagerInternal = mInjector.getActivityManagerInternal(); - activityManagerInternal.setSwitchingFromSystemUserMessage( + int deviceOwnerUserId = UserHandle.getUserId(deviceOwner.getUid()); + activityManagerInternal.setSwitchingFromUserMessage(deviceOwnerUserId, deviceOwner.startUserSessionMessage); - activityManagerInternal.setSwitchingToSystemUserMessage( + activityManagerInternal.setSwitchingToUserMessage(deviceOwnerUserId, deviceOwner.endUserSessionMessage); } @@ -3970,6 +3995,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { deletedUsers.remove(userInfo.id); } for (Integer userId : deletedUsers) { + removeManagedEmbeddedSubscriptionsForUser(userId); removeUserData(userId); mDevicePolicyEngine.handleUserRemoved(userId); } @@ -8099,6 +8125,45 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { mInjector.getNotificationManager().cancel(SystemMessage.NOTE_PROFILE_WIPED); } + /** + * Remove eSIM subscriptions that are managed by any of the admin packages of the given + * userHandle. + */ + private void removeManagedEmbeddedSubscriptionsForUser(int userHandle) { + if (!Flags.removeManagedEsimOnWorkProfileDeletion()) { + return; + } + + Slogf.i(LOG_TAG, + "Managed profile with ID=%d deleted: going to remove managed embedded " + + "subscriptions", userHandle); + String profileOwnerPackage = mOwners.getProfileOwnerPackage(userHandle); + if (profileOwnerPackage == null) { + Slogf.wtf(LOG_TAG, "Profile owner package for managed profile is null"); + return; + } + IntArray managedSubscriptionIds = getSubscriptionIdsInternal(profileOwnerPackage); + deleteEmbeddedSubscriptions(managedSubscriptionIds); + } + + private void deleteEmbeddedSubscriptions(IntArray subscriptionIds) { + EuiccManager euiccManager = mContext.getSystemService(EuiccManager.class); + for (int subscriptionId : subscriptionIds.toArray()) { + Slogf.i(LOG_TAG, "Deleting embedded subscription with ID %d", subscriptionId); + euiccManager.deleteSubscription(subscriptionId, + createCallbackPendingIntentForRemovingManagedSubscription( + subscriptionId)); + } + } + + private PendingIntent createCallbackPendingIntentForRemovingManagedSubscription( + Integer subscriptionId) { + Intent intent = new Intent(ACTION_ESIM_REMOVED_WITH_MANAGED_PROFILE); + intent.putExtra(EXTRA_REMOVED_ESIM_SUBSCRIPTION_ID, subscriptionId); + return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE); + } + + @Override public void setFactoryResetProtectionPolicy(ComponentName who, String callerPackageName, @Nullable FactoryResetProtectionPolicy policy) { @@ -19652,7 +19717,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } mInjector.getActivityManagerInternal() - .setSwitchingFromSystemUserMessage(startUserSessionMessageString); + .setSwitchingFromUserMessage(caller.getUserId(), startUserSessionMessageString); } @Override @@ -19677,7 +19742,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } mInjector.getActivityManagerInternal() - .setSwitchingToSystemUserMessage(endUserSessionMessageString); + .setSwitchingToUserMessage(caller.getUserId(), endUserSessionMessageString); } @Override diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java index 8e749529978e..de78271acddc 100644 --- a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java +++ b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java @@ -20,6 +20,7 @@ import static android.hardware.SensorManager.SENSOR_DELAY_FASTEST; import static android.hardware.devicestate.DeviceState.PROPERTY_POLICY_UNSUPPORTED_WHEN_POWER_SAVE_MODE; import static android.hardware.devicestate.DeviceState.PROPERTY_POLICY_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL; import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER; +import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.TYPE_EXTERNAL; @@ -324,6 +325,11 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, } if (newState != INVALID_DEVICE_STATE_IDENTIFIER && newState != mLastReportedState) { + if (Trace.isTagEnabled(TRACE_TAG_SYSTEM_SERVER)) { + Trace.instant(TRACE_TAG_SYSTEM_SERVER, + "[Device state changed] Last hinge sensor event timestamp: " + + mLastHingeAngleSensorEvent.timestamp); + } mLastReportedState = newState; stateToReport = newState; } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 860b6fb1dcd1..788b3b883160 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -366,6 +366,8 @@ public final class SystemServer implements Dumpable { "com.android.clockwork.time.WearTimeService"; private static final String WEAR_SETTINGS_SERVICE_CLASS = "com.android.clockwork.settings.WearSettingsService"; + private static final String WEAR_GESTURE_SERVICE_CLASS = + "com.android.clockwork.gesture.WearGestureService"; private static final String WRIST_ORIENTATION_SERVICE_CLASS = "com.android.clockwork.wristorientation.WristOrientationService"; private static final String IOT_SERVICE_CLASS = @@ -2844,6 +2846,13 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(WRIST_ORIENTATION_SERVICE_CLASS); t.traceEnd(); } + + if (android.server.Flags.wearGestureApi() + && SystemProperties.getBoolean("config.enable_gesture_api", false)) { + t.traceBegin("StartWearGestureService"); + mSystemServiceManager.startService(WEAR_GESTURE_SERVICE_CLASS); + t.traceEnd(); + } } if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_SLICES_DISABLED)) { diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig index 86ccd878de7c..7a6bd75e5893 100644 --- a/services/java/com/android/server/flags.aconfig +++ b/services/java/com/android/server/flags.aconfig @@ -65,4 +65,12 @@ flag { namespace: "package_manager_service" description: "Remove AppIntegrityManagerService" bug: "364200023" +} + +flag { + name: "wear_gesture_api" + namespace: "wear_frameworks" + description: "Whether the Wear Gesture API is available." + bug: "396154116" + is_exported: true }
\ No newline at end of file diff --git a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java index 2dd16f68dc56..80a3a8788d80 100644 --- a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java +++ b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java @@ -109,6 +109,7 @@ import com.android.server.EventLogTags; import com.android.server.LocalServices; import com.android.server.backup.BackupAgentConnectionManager; import com.android.server.backup.BackupRestoreTask; +import com.android.server.backup.BackupRestoreTask.CancellationReason; import com.android.server.backup.BackupWakeLock; import com.android.server.backup.DataChangedJournal; import com.android.server.backup.KeyValueBackupJob; @@ -2412,7 +2413,7 @@ public class KeyValueBackupTaskTest { KeyValueBackupTask task = spy(createKeyValueBackupTask(transportMock, PACKAGE_1)); doNothing().when(task).waitCancel(); - task.handleCancel(true); + task.handleCancel(CancellationReason.EXTERNAL); InOrder inOrder = inOrder(task); inOrder.verify(task).markCancel(); @@ -2420,12 +2421,14 @@ public class KeyValueBackupTaskTest { } @Test - public void testHandleCancel_whenCancelAllFalse_throws() throws Exception { + public void testHandleCancel_timeout_throws() throws Exception { TransportMock transportMock = setUpInitializedTransport(mTransport); setUpAgentWithData(PACKAGE_1); KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); - expectThrows(IllegalArgumentException.class, () -> task.handleCancel(false)); + expectThrows( + IllegalArgumentException.class, + () -> task.handleCancel(CancellationReason.TIMEOUT)); } /** Do not update backup token if no data was moved. */ diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/OWNERS b/services/tests/DynamicInstrumentationManagerServiceTests/OWNERS new file mode 100644 index 000000000000..2522426d93f8 --- /dev/null +++ b/services/tests/DynamicInstrumentationManagerServiceTests/OWNERS @@ -0,0 +1 @@ +include platform/packages/modules/UprobeStats:/OWNERS
\ No newline at end of file diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp index ae9a34efc222..c1d8382fcd0e 100644 --- a/services/tests/InputMethodSystemServerTests/Android.bp +++ b/services/tests/InputMethodSystemServerTests/Android.bp @@ -73,10 +73,7 @@ android_ravenwood_test { static_libs: [ "androidx.annotation_annotation", "androidx.test.rules", - "framework", - "ravenwood-runtime", - "ravenwood-utils", - "services", + "services.core", ], libs: [ "android.test.base.stubs.system", diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/util/IgnoreableExpect.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/util/IgnoreableExpect.kt index afb18f5be669..5c9ba401bf88 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/util/IgnoreableExpect.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/util/IgnoreableExpect.kt @@ -32,7 +32,7 @@ internal class IgnoreableExpect : TestRule { private var ignore = false - override fun apply(base: Statement?, description: Description?): Statement { + override fun apply(base: Statement, description: Description): Statement { return object : Statement() { override fun evaluate() { ignore = false diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index 1f8ccde98d35..2770caa8aaa4 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -324,7 +324,8 @@ public class DisplayManagerServiceTest { return new VirtualDisplayAdapter(syncRoot, context, handler, displayAdapterListener, new VirtualDisplayAdapter.SurfaceControlDisplayFactory() { @Override - public IBinder createDisplay(String name, boolean secure, String uniqueId, + public IBinder createDisplay(String name, boolean secure, + boolean optimizeForPower, String uniqueId, float requestedRefreshRate) { return mMockDisplayToken; } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt index 206c90d0481a..6dc7361e5366 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt @@ -47,7 +47,7 @@ class DisplayTopologyCoordinatorTest { private val mockTopology = mock<DisplayTopology>() private val mockTopologyCopy = mock<DisplayTopology>() private val mockTopologyGraph = mock<DisplayTopologyGraph>() - private val mockIsExtendedDisplayEnabled = mock<() -> Boolean>() + private val mockIsExtendedDisplayAllowed = mock<() -> Boolean>() private val mockTopologySavedCallback = mock<() -> Unit>() private val mockTopologyChangedCallback = mock<(android.util.Pair<DisplayTopology, DisplayTopologyGraph>) -> Unit>() @@ -73,10 +73,10 @@ class DisplayTopologyCoordinatorTest { ) = mockTopologyStore } - whenever(mockIsExtendedDisplayEnabled()).thenReturn(true) + whenever(mockIsExtendedDisplayAllowed()).thenReturn(true) whenever(mockTopology.copy()).thenReturn(mockTopologyCopy) whenever(mockTopologyCopy.getGraph(any())).thenReturn(mockTopologyGraph) - coordinator = DisplayTopologyCoordinator(injector, mockIsExtendedDisplayEnabled, + coordinator = DisplayTopologyCoordinator(injector, mockIsExtendedDisplayAllowed, mockTopologyChangedCallback, topologyChangeExecutor, DisplayManagerService.SyncRoot(), mockTopologySavedCallback) } @@ -195,7 +195,7 @@ class DisplayTopologyCoordinatorTest { @Test fun addDisplay_external_extendedDisplaysDisabled() { - whenever(mockIsExtendedDisplayEnabled()).thenReturn(false) + whenever(mockIsExtendedDisplayAllowed()).thenReturn(false) for (displayInfo in displayInfos) { coordinator.onDisplayAdded(displayInfo) @@ -208,7 +208,7 @@ class DisplayTopologyCoordinatorTest { @Test fun addDisplay_overlay_extendedDisplaysDisabled() { displayInfos[0].type = Display.TYPE_OVERLAY - whenever(mockIsExtendedDisplayEnabled()).thenReturn(false) + whenever(mockIsExtendedDisplayAllowed()).thenReturn(false) for (displayInfo in displayInfos) { coordinator.onDisplayAdded(displayInfo) @@ -314,7 +314,7 @@ class DisplayTopologyCoordinatorTest { @Test fun updateDisplay_external_extendedDisplaysDisabled() { - whenever(mockIsExtendedDisplayEnabled()).thenReturn(false) + whenever(mockIsExtendedDisplayAllowed()).thenReturn(false) for (displayInfo in displayInfos) { coordinator.onDisplayChanged(displayInfo) @@ -328,7 +328,7 @@ class DisplayTopologyCoordinatorTest { @Test fun updateDisplay_overlay_extendedDisplaysDisabled() { displayInfos[0].type = Display.TYPE_OVERLAY - whenever(mockIsExtendedDisplayEnabled()).thenReturn(false) + whenever(mockIsExtendedDisplayAllowed()).thenReturn(false) coordinator.onDisplayChanged(displayInfos[0]) diff --git a/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java index 0bef3b89547f..10bea7d331cd 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java @@ -416,7 +416,7 @@ public class VirtualDisplayAdapterTest { final String uniqueId = "uniqueId"; final IBinder displayToken = new Binder(); when(mMockSufaceControlDisplayFactory.createDisplay( - any(), anyBoolean(), eq(uniqueId), anyFloat())) + any(), anyBoolean(), anyBoolean(), eq(uniqueId), anyFloat())) .thenReturn(displayToken); // The display needs to be public, otherwise it will be considered never blank. @@ -456,6 +456,49 @@ public class VirtualDisplayAdapterTest { verify(mMockCallback).onPaused(); } + @EnableFlags( + android.companion.virtualdevice.flags.Flags.FLAG_CORRECT_VIRTUAL_DISPLAY_POWER_STATE) + @Test + public void createVirtualDisplayLocked_neverBlank_optimizesForPower() { + final String uniqueId = "uniqueId"; + final IBinder displayToken = new Binder(); + final String name = "name"; + when(mVirtualDisplayConfigMock.getName()).thenReturn(name); + when(mMockSufaceControlDisplayFactory.createDisplay( + any(), anyBoolean(), anyBoolean(), eq(uniqueId), anyFloat())) + .thenReturn(displayToken); + + // Use a private display to cause the display to be never blank. + mAdapter.createVirtualDisplayLocked(mMockCallback, + /* projection= */ null, /* ownerUid= */ 10, /* packageName= */ "testpackage", + uniqueId, /* surface= */ mSurfaceMock, 0, mVirtualDisplayConfigMock); + + verify(mMockSufaceControlDisplayFactory).createDisplay(eq(name), eq(false), eq(true), + eq(uniqueId), anyFloat()); + } + + @EnableFlags( + android.companion.virtualdevice.flags.Flags.FLAG_CORRECT_VIRTUAL_DISPLAY_POWER_STATE) + @Test + public void createVirtualDisplayLocked_blankable_optimizesForPerformance() { + final String uniqueId = "uniqueId"; + final IBinder displayToken = new Binder(); + final String name = "name"; + when(mVirtualDisplayConfigMock.getName()).thenReturn(name); + when(mMockSufaceControlDisplayFactory.createDisplay( + any(), anyBoolean(), anyBoolean(), eq(uniqueId), anyFloat())) + .thenReturn(displayToken); + + // Use a public display to cause the display to be blankable + mAdapter.createVirtualDisplayLocked(mMockCallback, + /* projection= */ null, /* ownerUid= */ 10, /* packageName= */ "testpackage", + uniqueId, /* surface= */ mSurfaceMock, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, + mVirtualDisplayConfigMock); + + verify(mMockSufaceControlDisplayFactory).createDisplay(eq(name), eq(false), eq(false), + eq(uniqueId), anyFloat()); + } + private IVirtualDisplayCallback createCallback() { return new IVirtualDisplayCallback.Stub() { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index 5d8f57866f7d..e094111c327a 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -746,6 +746,36 @@ public class MockingOomAdjusterTests { @SuppressWarnings("GuardedBy") @Test @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) + public void testUpdateOomAdjFreezeState_bindingWithAllowFreeze() { + ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, + MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); + WindowProcessController wpc = app.getWindowProcessController(); + doReturn(true).when(wpc).hasVisibleActivities(); + + final ProcessRecord app2 = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID, + MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false)); + + // App with a visible activity binds to app2 without any special flag. + bindService(app2, app, null, null, 0, mock(IBinder.class)); + + final ProcessRecord app3 = spy(makeDefaultProcessRecord(MOCKAPP3_PID, MOCKAPP3_UID, + MOCKAPP3_PROCESSNAME, MOCKAPP3_PACKAGENAME, false)); + + // App with a visible activity binds to app3 with ALLOW_FREEZE. + bindService(app3, app, null, null, Context.BIND_ALLOW_FREEZE, mock(IBinder.class)); + + setProcessesToLru(app, app2, app3); + + updateOomAdj(app); + + assertCpuTime(app); + assertCpuTime(app2); + assertNoCpuTime(app3); + } + + @SuppressWarnings("GuardedBy") + @Test + @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) @DisableFlags(Flags.FLAG_PROTOTYPE_AGGRESSIVE_FREEZING) public void testUpdateOomAdjFreezeState_bindingFromFgs() { final ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/BackupAgentConnectionManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/BackupAgentConnectionManagerTest.java index 8aaa72339c5b..33bd95ec9f5b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/BackupAgentConnectionManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/BackupAgentConnectionManagerTest.java @@ -51,6 +51,7 @@ import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.runner.AndroidJUnit4; import com.android.server.LocalServices; +import com.android.server.backup.BackupRestoreTask.CancellationReason; import com.android.server.backup.internal.LifecycleOperationStorage; import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; @@ -368,9 +369,12 @@ public class BackupAgentConnectionManagerTest { mConnectionManager.agentDisconnected(TEST_PACKAGE); mTestThread.join(); - verify(mUserBackupManagerService).handleCancel(eq(123), eq(true)); - verify(mUserBackupManagerService).handleCancel(eq(456), eq(true)); - verify(mUserBackupManagerService).handleCancel(eq(789), eq(true)); + verify(mUserBackupManagerService) + .handleCancel(eq(123), eq(CancellationReason.AGENT_DISCONNECTED)); + verify(mUserBackupManagerService) + .handleCancel(eq(456), eq(CancellationReason.AGENT_DISCONNECTED)); + verify(mUserBackupManagerService) + .handleCancel(eq(789), eq(CancellationReason.AGENT_DISCONNECTED)); } @Test diff --git a/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java b/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java index 8ce05e2fa115..c9f86b04be22 100644 --- a/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/power/ScreenUndimDetectorTest.java @@ -23,7 +23,6 @@ import static android.hardware.display.DisplayManagerInternal.DisplayPowerReques import static android.provider.DeviceConfig.NAMESPACE_ATTENTION_MANAGER_SERVICE; import static android.view.Display.DEFAULT_DISPLAY_GROUP; -import static com.android.server.power.ScreenUndimDetector.DEFAULT_MAX_DURATION_BETWEEN_UNDIMS_MILLIS; import static com.android.server.power.ScreenUndimDetector.KEY_KEEP_SCREEN_ON_ENABLED; import static com.android.server.power.ScreenUndimDetector.KEY_MAX_DURATION_BETWEEN_UNDIMS_MILLIS; import static com.android.server.power.ScreenUndimDetector.KEY_UNDIMS_REQUIRED; @@ -49,6 +48,7 @@ import org.junit.runners.JUnit4; import java.util.Arrays; import java.util.List; +import java.util.concurrent.TimeUnit; /** * Tests for {@link com.android.server.power.ScreenUndimDetector} @@ -61,7 +61,8 @@ public class ScreenUndimDetectorTest { POLICY_DIM, POLICY_BRIGHT); private static final int OTHER_DISPLAY_GROUP = DEFAULT_DISPLAY_GROUP + 1; - + private static final long DEFAULT_MAX_DURATION_BETWEEN_UNDIMS_MILLIS = + TimeUnit.MINUTES.toMillis(5); @ClassRule public static final TestableContext sContext = new TestableContext( InstrumentationRegistry.getInstrumentation().getTargetContext(), null); @@ -88,7 +89,8 @@ public class ScreenUndimDetectorTest { @Before public void setup() { InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - + DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE, + KEY_KEEP_SCREEN_ON_ENABLED, Boolean.TRUE.toString(), false /*makeDefault*/); DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE, KEY_UNDIMS_REQUIRED, Integer.toString(1), false /*makeDefault*/); @@ -108,10 +110,10 @@ public class ScreenUndimDetectorTest { @Test public void recordScreenPolicy_disabledByFlag_noop() { + setup(); DeviceConfig.setProperty(NAMESPACE_ATTENTION_MANAGER_SERVICE, KEY_KEEP_SCREEN_ON_ENABLED, Boolean.FALSE.toString(), false /*makeDefault*/); mScreenUndimDetector.readValuesFromDeviceConfig(); - mScreenUndimDetector.recordScreenPolicy(DEFAULT_DISPLAY_GROUP, POLICY_DIM); mScreenUndimDetector.recordScreenPolicy(DEFAULT_DISPLAY_GROUP, POLICY_BRIGHT); diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java index 49c37f163ff2..241ffdc19ce4 100644 --- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java @@ -36,24 +36,29 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; +import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.RequiresFlagsEnabled; +import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; import android.view.Display; import android.view.DisplayInfo; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; import androidx.test.runner.AndroidJUnit4; import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.internal.R; import org.junit.AfterClass; import org.junit.Before; @@ -70,10 +75,11 @@ import java.io.File; import java.io.IOException; import java.util.Comparator; import java.util.List; +import java.util.Set; /** * Unit tests for the most important helpers of {@link WallpaperCropper}, in particular - * {@link WallpaperCropper#getCrop(Point, Point, SparseArray, boolean)}. + * {@link WallpaperCropper#getCrop(Point, WallpaperDefaultDisplayInfo, Point, SparseArray, boolean)}. */ @Presubmit @RunWith(AndroidJUnit4.class) @@ -83,6 +89,12 @@ public class WallpaperCropperTest { @Mock private WallpaperDisplayHelper mWallpaperDisplayHelper; + + @Mock + private WindowManager mWindowManager; + + @Mock + private Resources mResources; private WallpaperCropper mWallpaperCropper; private static final Point PORTRAIT_ONE = new Point(500, 800); @@ -175,14 +187,21 @@ public class WallpaperCropperTest { return tempDir; } - private void setUpWithDisplays(List<Point> displaySizes) { + private WallpaperDefaultDisplayInfo setUpWithDisplays(List<Point> displaySizes) { mDisplaySizes = new SparseArray<>(); displaySizes.forEach(size -> { mDisplaySizes.put(getOrientation(size), size); Point rotated = new Point(size.y, size.x); mDisplaySizes.put(getOrientation(rotated), rotated); }); + Set<WindowMetrics> windowMetrics = new ArraySet<>(); + for (Point displaySize : displaySizes) { + windowMetrics.add( + new WindowMetrics(new Rect(0, 0, displaySize.x, displaySize.y), + new WindowInsets.Builder().build())); + } when(mWallpaperDisplayHelper.getDefaultDisplaySizes()).thenReturn(mDisplaySizes); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())).thenReturn(windowMetrics); if (displaySizes.size() == 2) { Point largestDisplay = displaySizes.stream().max( Comparator.comparingInt(p -> p.x * p.y)).get(); @@ -192,11 +211,16 @@ public class WallpaperCropperTest { mFolded = getOrientation(smallestDisplay); mUnfoldedRotated = getRotatedOrientation(mUnfolded); mFoldedRotated = getRotatedOrientation(mFolded); + // foldable + doReturn(new int[]{0}).when(mResources).getIntArray(R.array.config_foldedDeviceStates); + } else { + // no foldable + doReturn(new int[]{}).when(mResources).getIntArray(R.array.config_foldedDeviceStates); } - doAnswer(invocation -> getFoldedOrientation(invocation.getArgument(0))) - .when(mWallpaperDisplayHelper).getFoldedOrientation(anyInt()); - doAnswer(invocation -> getUnfoldedOrientation(invocation.getArgument(0))) - .when(mWallpaperDisplayHelper).getUnfoldedOrientation(anyInt()); + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + when(mWallpaperDisplayHelper.getDefaultDisplayInfo()).thenReturn(defaultDisplayInfo); + return defaultDisplayInfo; } private int getFoldedOrientation(int orientation) { @@ -435,7 +459,7 @@ public class WallpaperCropperTest { */ @Test public void testGetCrop_noSuggestedCrops() { - setUpWithDisplays(STANDARD_DISPLAY); + WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY); Point bitmapSize = new Point(800, 1000); Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y); SparseArray<Rect> suggestedCrops = new SparseArray<>(); @@ -455,8 +479,9 @@ public class WallpaperCropperTest { for (boolean rtl : List.of(false, true)) { Rect expectedCrop = rtl ? rightOf(bitmapRect, expectedCropSize) : leftOf(bitmapRect, expectedCropSize); - assertThat(mWallpaperCropper.getCrop( - displaySize, bitmapSize, suggestedCrops, rtl)) + assertThat( + WallpaperCropper.getCrop( + displaySize, defaultDisplayInfo, bitmapSize, suggestedCrops, rtl)) .isEqualTo(expectedCrop); } } @@ -469,7 +494,7 @@ public class WallpaperCropperTest { */ @Test public void testGetCrop_hasSuggestedCrop() { - setUpWithDisplays(STANDARD_DISPLAY); + WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY); Point bitmapSize = new Point(800, 1000); SparseArray<Rect> suggestedCrops = new SparseArray<>(); suggestedCrops.put(ORIENTATION_PORTRAIT, new Rect(0, 0, 400, 800)); @@ -479,11 +504,13 @@ public class WallpaperCropperTest { } for (boolean rtl : List.of(false, true)) { - assertThat(mWallpaperCropper.getCrop( - new Point(300, 800), bitmapSize, suggestedCrops, rtl)) + assertThat( + WallpaperCropper.getCrop(new Point(300, 800), defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl)) .isEqualTo(suggestedCrops.get(ORIENTATION_PORTRAIT)); - assertThat(mWallpaperCropper.getCrop( - new Point(500, 800), bitmapSize, suggestedCrops, rtl)) + assertThat( + WallpaperCropper.getCrop(new Point(500, 800), defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl)) .isEqualTo(new Rect(0, 0, 500, 800)); } } @@ -499,7 +526,7 @@ public class WallpaperCropperTest { */ @Test public void testGetCrop_hasRotatedSuggestedCrop() { - setUpWithDisplays(STANDARD_DISPLAY); + WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY); Point bitmapSize = new Point(2000, 1800); Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y); SparseArray<Rect> suggestedCrops = new SparseArray<>(); @@ -510,12 +537,14 @@ public class WallpaperCropperTest { suggestedCrops.put(ORIENTATION_PORTRAIT, centerOf(bitmapRect, portrait)); suggestedCrops.put(ORIENTATION_SQUARE_LANDSCAPE, centerOf(bitmapRect, squareLandscape)); for (boolean rtl : List.of(false, true)) { - assertThat(mWallpaperCropper.getCrop( - landscape, bitmapSize, suggestedCrops, rtl)) + assertThat( + WallpaperCropper.getCrop(landscape, defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl)) .isEqualTo(centerOf(bitmapRect, landscape)); - assertThat(mWallpaperCropper.getCrop( - squarePortrait, bitmapSize, suggestedCrops, rtl)) + assertThat( + WallpaperCropper.getCrop(squarePortrait, defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl)) .isEqualTo(centerOf(bitmapRect, squarePortrait)); } } @@ -532,7 +561,7 @@ public class WallpaperCropperTest { @Test public void testGetCrop_hasUnfoldedSuggestedCrop() { for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) { - setUpWithDisplays(displaySizes); + WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes); Point bitmapSize = new Point(2000, 2400); Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y); @@ -569,8 +598,9 @@ public class WallpaperCropperTest { expectedCrop.right = Math.min( unfoldedCrop.right, unfoldedCrop.right + maxParallax); } - assertThat(mWallpaperCropper.getCrop( - foldedDisplay, bitmapSize, suggestedCrops, rtl)) + assertThat( + WallpaperCropper.getCrop(foldedDisplay, defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl)) .isEqualTo(expectedCrop); } } @@ -588,7 +618,7 @@ public class WallpaperCropperTest { @Test public void testGetCrop_hasFoldedSuggestedCrop() { for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) { - setUpWithDisplays(displaySizes); + WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes); Point bitmapSize = new Point(2000, 2000); Rect bitmapRect = new Rect(0, 0, 2000, 2000); @@ -610,12 +640,14 @@ public class WallpaperCropperTest { Point unfoldedDisplayTwo = mDisplaySizes.get(unfoldedTwo); for (boolean rtl : List.of(false, true)) { - assertThat(centerOf(mWallpaperCropper.getCrop( - unfoldedDisplayOne, bitmapSize, suggestedCrops, rtl), foldedDisplayOne)) + assertThat(centerOf( + WallpaperCropper.getCrop(unfoldedDisplayOne, defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl), foldedDisplayOne)) .isEqualTo(foldedCropOne); - assertThat(centerOf(mWallpaperCropper.getCrop( - unfoldedDisplayTwo, bitmapSize, suggestedCrops, rtl), foldedDisplayTwo)) + assertThat(centerOf( + WallpaperCropper.getCrop(unfoldedDisplayTwo, defaultDisplayInfo, bitmapSize, + suggestedCrops, rtl), foldedDisplayTwo)) .isEqualTo(foldedCropTwo); } } @@ -633,7 +665,7 @@ public class WallpaperCropperTest { @Test public void testGetCrop_hasRotatedUnfoldedSuggestedCrop() { for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) { - setUpWithDisplays(displaySizes); + WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes); Point bitmapSize = new Point(2000, 2000); Rect bitmapRect = new Rect(0, 0, 2000, 2000); Point largestDisplay = displaySizes.stream().max( @@ -650,8 +682,9 @@ public class WallpaperCropperTest { Point rotatedFoldedDisplay = mDisplaySizes.get(rotatedFolded); for (boolean rtl : List.of(false, true)) { - assertThat(mWallpaperCropper.getCrop( - rotatedFoldedDisplay, bitmapSize, suggestedCrops, rtl)) + assertThat( + WallpaperCropper.getCrop(rotatedFoldedDisplay, defaultDisplayInfo, + bitmapSize, suggestedCrops, rtl)) .isEqualTo(centerOf(rotatedUnfoldedCrop, rotatedFoldedDisplay)); } } @@ -670,7 +703,7 @@ public class WallpaperCropperTest { @Test public void testGetCrop_hasRotatedFoldedSuggestedCrop() { for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) { - setUpWithDisplays(displaySizes); + WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes); Point bitmapSize = new Point(2000, 2000); Rect bitmapRect = new Rect(0, 0, 2000, 2000); @@ -689,8 +722,8 @@ public class WallpaperCropperTest { Point rotatedUnfoldedDisplay = mDisplaySizes.get(rotatedUnfolded); for (boolean rtl : List.of(false, true)) { - Rect rotatedUnfoldedCrop = mWallpaperCropper.getCrop( - rotatedUnfoldedDisplay, bitmapSize, suggestedCrops, rtl); + Rect rotatedUnfoldedCrop = WallpaperCropper.getCrop(rotatedUnfoldedDisplay, + defaultDisplayInfo, bitmapSize, suggestedCrops, rtl); assertThat(centerOf(rotatedUnfoldedCrop, rotatedFoldedDisplay)) .isEqualTo(rotatedFoldedCrop); } diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java new file mode 100644 index 000000000000..312db91afb12 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.wallpaper; + +import static android.app.WallpaperManager.ORIENTATION_LANDSCAPE; +import static android.app.WallpaperManager.ORIENTATION_PORTRAIT; +import static android.app.WallpaperManager.ORIENTATION_SQUARE_LANDSCAPE; +import static android.app.WallpaperManager.ORIENTATION_SQUARE_PORTRAIT; +import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.platform.test.annotations.Presubmit; +import android.util.SparseArray; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.R; +import com.android.server.wallpaper.WallpaperDefaultDisplayInfo.FoldableOrientations; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +import java.util.Set; + +/** Unit tests for {@link WallpaperDefaultDisplayInfo}. */ +@Presubmit +@RunWith(AndroidJUnit4.class) +public class WallpaperDefaultDisplayInfoTest { + @Mock + private WindowManager mWindowManager; + + @Mock + private Resources mResources; + + @Before + public void setUp() { + initMocks(this); + } + + @Test + public void defaultDisplayInfo_foldable_shouldHaveExpectedContent() { + doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152); + Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424); + WindowMetrics innerDisplayMetrics = + new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + WindowMetrics outerDisplayMetrics = + new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + SparseArray<Point> displaySizes = new SparseArray<>(); + displaySizes.put(ORIENTATION_PORTRAIT, new Point(1080, 2424)); + displaySizes.put(ORIENTATION_LANDSCAPE, new Point(2424, 1080)); + displaySizes.put(ORIENTATION_SQUARE_PORTRAIT, new Point(2076, 2152)); + displaySizes.put(ORIENTATION_SQUARE_LANDSCAPE, new Point(2152, 2076)); + assertThat(defaultDisplayInfo.defaultDisplaySizes.contentEquals(displaySizes)).isTrue(); + assertThat(defaultDisplayInfo.isFoldable).isTrue(); + assertThat(defaultDisplayInfo.isLargeScreen).isFalse(); + assertThat(defaultDisplayInfo.foldableOrientations).containsExactly( + new FoldableOrientations( + /* foldedOrientation= */ ORIENTATION_PORTRAIT, + /* unfoldedOrientation= */ ORIENTATION_SQUARE_PORTRAIT), + new FoldableOrientations( + /* foldedOrientation= */ ORIENTATION_LANDSCAPE, + /* unfoldedOrientation= */ ORIENTATION_SQUARE_LANDSCAPE)); + } + + @Test + public void defaultDisplayInfo_tablet_shouldHaveExpectedContent() { + doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect displayBounds = new Rect(0, 0, 2560, 1600); + WindowMetrics displayMetrics = + new WindowMetrics(displayBounds, new WindowInsets.Builder().build(), + /* density= */ 2f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(displayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + SparseArray<Point> displaySizes = new SparseArray<>(); + displaySizes.put(ORIENTATION_PORTRAIT, new Point(1600, 2560)); + displaySizes.put(ORIENTATION_LANDSCAPE, new Point(2560, 1600)); + assertThat(defaultDisplayInfo.defaultDisplaySizes.contentEquals(displaySizes)).isTrue(); + assertThat(defaultDisplayInfo.isFoldable).isFalse(); + assertThat(defaultDisplayInfo.isLargeScreen).isTrue(); + assertThat(defaultDisplayInfo.foldableOrientations).isEmpty(); + } + + @Test + public void defaultDisplayInfo_phone_shouldHaveExpectedContent() { + doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect displayBounds = new Rect(0, 0, 1280, 2856); + WindowMetrics displayMetrics = + new WindowMetrics(displayBounds, new WindowInsets.Builder().build(), + /* density= */ 3f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(displayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + SparseArray<Point> displaySizes = new SparseArray<>(); + displaySizes.put(ORIENTATION_PORTRAIT, new Point(1280, 2856)); + displaySizes.put(ORIENTATION_LANDSCAPE, new Point(2856, 1280)); + assertThat(defaultDisplayInfo.defaultDisplaySizes.contentEquals(displaySizes)).isTrue(); + assertThat(defaultDisplayInfo.isFoldable).isFalse(); + assertThat(defaultDisplayInfo.isLargeScreen).isFalse(); + assertThat(defaultDisplayInfo.foldableOrientations).isEmpty(); + } + + @Test + public void defaultDisplayInfo_equals_sameContent_shouldEqual() { + doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152); + Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424); + WindowMetrics innerDisplayMetrics = + new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + WindowMetrics outerDisplayMetrics = + new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + assertThat(defaultDisplayInfo).isEqualTo(otherDefaultDisplayInfo); + } + + @Test + public void defaultDisplayInfo_equals_differentBounds_shouldNotEqual() { + doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152); + Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424); + WindowMetrics innerDisplayMetrics = + new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + WindowMetrics outerDisplayMetrics = + new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + // For the first call + .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics)) + // For the second+ call + .thenReturn(Set.of(innerDisplayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + assertThat(defaultDisplayInfo).isNotEqualTo(otherDefaultDisplayInfo); + } + + @Test + public void defaultDisplayInfo_hashCode_sameContent_shouldEqual() { + doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152); + Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424); + WindowMetrics innerDisplayMetrics = + new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + WindowMetrics outerDisplayMetrics = + new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + assertThat(defaultDisplayInfo.hashCode()).isEqualTo(otherDefaultDisplayInfo.hashCode()); + } + + @Test + public void defaultDisplayInfo_hashCode_differentBounds_shouldNotEqual() { + doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152); + Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424); + WindowMetrics innerDisplayMetrics = + new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + WindowMetrics outerDisplayMetrics = + new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + // For the first call + .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics)) + // For the second+ call + .thenReturn(Set.of(innerDisplayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + assertThat(defaultDisplayInfo.hashCode()).isNotEqualTo(otherDefaultDisplayInfo.hashCode()); + } + + @Test + public void getFoldedOrientation_foldable_shouldReturnExpectedOrientation() { + doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152); + Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424); + WindowMetrics innerDisplayMetrics = + new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + WindowMetrics outerDisplayMetrics = + new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics)); + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_PORTRAIT)) + .isEqualTo(ORIENTATION_PORTRAIT); + assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE)) + .isEqualTo(ORIENTATION_LANDSCAPE); + // Use a folded orientation for a folded orientation should return unknown. + assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_PORTRAIT)) + .isEqualTo(ORIENTATION_UNKNOWN); + assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_LANDSCAPE)) + .isEqualTo(ORIENTATION_UNKNOWN); + } + + @Test + public void getUnfoldedOrientation_foldable_shouldReturnExpectedOrientation() { + doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152); + Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424); + WindowMetrics innerDisplayMetrics = + new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + WindowMetrics outerDisplayMetrics = + new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(), + /* density= */ 2.4375f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics)); + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_PORTRAIT)) + .isEqualTo(ORIENTATION_SQUARE_PORTRAIT); + assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_LANDSCAPE)) + .isEqualTo(ORIENTATION_SQUARE_LANDSCAPE); + // Use an unfolded orientation for an unfolded orientation should return unknown. + assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_PORTRAIT)) + .isEqualTo(ORIENTATION_UNKNOWN); + assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE)) + .isEqualTo(ORIENTATION_UNKNOWN); + } + + @Test + public void getFoldedOrientation_nonFoldable_shouldReturnUnknown() { + doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect displayBounds = new Rect(0, 0, 2560, 1600); + WindowMetrics displayMetrics = + new WindowMetrics(displayBounds, new WindowInsets.Builder().build(), + /* density= */ 2f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(displayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_PORTRAIT)) + .isEqualTo(ORIENTATION_UNKNOWN); + assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE)) + .isEqualTo(ORIENTATION_UNKNOWN); + assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_PORTRAIT)) + .isEqualTo(ORIENTATION_UNKNOWN); + assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_LANDSCAPE)) + .isEqualTo(ORIENTATION_UNKNOWN); + } + + @Test + public void getUnFoldedOrientation_nonFoldable_shouldReturnUnknown() { + doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates)); + Rect displayBounds = new Rect(0, 0, 2560, 1600); + WindowMetrics displayMetrics = + new WindowMetrics(displayBounds, new WindowInsets.Builder().build(), + /* density= */ 2f); + when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())) + .thenReturn(Set.of(displayMetrics)); + + WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo( + mWindowManager, mResources); + + assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_PORTRAIT)) + .isEqualTo(ORIENTATION_UNKNOWN); + assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE)) + .isEqualTo(ORIENTATION_UNKNOWN); + assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_PORTRAIT)) + .isEqualTo(ORIENTATION_UNKNOWN); + assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_LANDSCAPE)) + .isEqualTo(ORIENTATION_UNKNOWN); + } +} diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java index 5165e34c7fcd..fc864dd230d9 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java @@ -22,6 +22,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.spy; @@ -330,31 +331,24 @@ public class BatteryStatsHistoryTest { return invocation.callRealMethod(); }).when(mHistory).readFragmentToParcel(any(), any()); - // Prepare history for iteration - mHistory.iterate(0, MonotonicClock.UNDEFINED); - - Parcel parcel = mHistory.getNextParcel(0, Long.MAX_VALUE); - assertThat(parcel).isNotNull(); - assertThat(mReadFiles).containsExactly("123.bh"); - - // Skip to the end to force reading the next parcel - parcel.setDataPosition(parcel.dataSize()); - mReadFiles.clear(); - parcel = mHistory.getNextParcel(0, Long.MAX_VALUE); - assertThat(parcel).isNotNull(); - assertThat(mReadFiles).containsExactly("1000.bh"); - - parcel.setDataPosition(parcel.dataSize()); - mReadFiles.clear(); - parcel = mHistory.getNextParcel(0, Long.MAX_VALUE); - assertThat(parcel).isNotNull(); - assertThat(mReadFiles).containsExactly("2000.bh"); + int eventsRead = 0; + BatteryStatsHistoryIterator iterator = mHistory.iterate(0, MonotonicClock.UNDEFINED); + while (iterator.hasNext()) { + HistoryItem item = iterator.next(); + if (item.eventCode == HistoryItem.EVENT_JOB_START) { + eventsRead++; + assertThat(mReadFiles).containsExactly("123.bh"); + } else if (item.eventCode == HistoryItem.EVENT_JOB_FINISH) { + eventsRead++; + assertThat(mReadFiles).containsExactly("123.bh", "1000.bh"); + } else if (item.eventCode == HistoryItem.EVENT_ALARM) { + eventsRead++; + assertThat(mReadFiles).containsExactly("123.bh", "1000.bh", "2000.bh"); + } + } - parcel.setDataPosition(parcel.dataSize()); - mReadFiles.clear(); - parcel = mHistory.getNextParcel(0, Long.MAX_VALUE); - assertThat(parcel).isNull(); - assertThat(mReadFiles).isEmpty(); + assertThat(eventsRead).isEqualTo(3); + assertThat(mReadFiles).containsExactly("123.bh", "1000.bh", "2000.bh", "3000.bh"); } @Test @@ -372,25 +366,19 @@ public class BatteryStatsHistoryTest { return invocation.callRealMethod(); }).when(mHistory).readFragmentToParcel(any(), any()); - // Prepare history for iteration - mHistory.iterate(1000, 3000); - - Parcel parcel = mHistory.getNextParcel(1000, 3000); - assertThat(parcel).isNotNull(); - assertThat(mReadFiles).containsExactly("1000.bh"); - - // Skip to the end to force reading the next parcel - parcel.setDataPosition(parcel.dataSize()); - mReadFiles.clear(); - parcel = mHistory.getNextParcel(1000, 3000); - assertThat(parcel).isNotNull(); - assertThat(mReadFiles).containsExactly("2000.bh"); + BatteryStatsHistoryIterator iterator = mHistory.iterate(1000, 3000); + while (iterator.hasNext()) { + HistoryItem item = iterator.next(); + if (item.eventCode == HistoryItem.EVENT_JOB_START) { + fail("Event outside the range"); + } else if (item.eventCode == HistoryItem.EVENT_JOB_FINISH) { + assertThat(mReadFiles).containsExactly("1000.bh"); + } else if (item.eventCode == HistoryItem.EVENT_ALARM) { + fail("Event outside the range"); + } + } - parcel.setDataPosition(parcel.dataSize()); - mReadFiles.clear(); - parcel = mHistory.getNextParcel(1000, 3000); - assertThat(parcel).isNull(); - assertThat(mReadFiles).isEmpty(); + assertThat(mReadFiles).containsExactly("1000.bh", "2000.bh"); } private void prepareMultiFileHistory() { diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java index ea83825cd810..ea25e7992dd9 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java @@ -43,6 +43,7 @@ import android.view.accessibility.AccessibilityManager; import com.android.internal.accessibility.util.AccessibilityUtils; import com.android.server.accessibility.AccessibilityTraceManager; +import com.android.server.accessibility.BaseEventStreamTransformation; import org.junit.After; import org.junit.Before; @@ -70,6 +71,19 @@ public class AutoclickControllerTest { @Mock private WindowManager mMockWindowManager; private AutoclickController mController; + private static class MotionEventCaptor extends BaseEventStreamTransformation { + public MotionEvent downEvent; + + @Override + public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + downEvent = event; + break; + } + } + } + @Before public void setUp() { mTestableLooper = TestableLooper.get(this); @@ -660,6 +674,153 @@ public class AutoclickControllerTest { assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()).isEqualTo(-1); } + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void onMotionEvent_flagOn_lazyInitAutoclickScrollPanel() { + assertThat(mController.mAutoclickScrollPanel).isNull(); + + injectFakeMouseActionHoverMoveEvent(); + + assertThat(mController.mAutoclickScrollPanel).isNotNull(); + } + + @Test + @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void onMotionEvent_flagOff_notInitAutoclickScrollPanel() { + assertThat(mController.mAutoclickScrollPanel).isNull(); + + injectFakeMouseActionHoverMoveEvent(); + + assertThat(mController.mAutoclickScrollPanel).isNull(); + } + + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void onDestroy_flagOn_hideAutoclickScrollPanel() { + injectFakeMouseActionHoverMoveEvent(); + AutoclickScrollPanel mockAutoclickScrollPanel = mock(AutoclickScrollPanel.class); + mController.mAutoclickScrollPanel = mockAutoclickScrollPanel; + + mController.onDestroy(); + + verify(mockAutoclickScrollPanel).hide(); + } + + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void changeFromScrollToOtherClickType_hidesScrollPanel() { + injectFakeMouseActionHoverMoveEvent(); + + // Set active click type to SCROLL. + mController.clickPanelController.handleAutoclickTypeChange( + AutoclickTypePanel.AUTOCLICK_TYPE_SCROLL); + + // Show the scroll panel. + mController.mAutoclickScrollPanel.show(); + assertThat(mController.mAutoclickScrollPanel.isVisible()).isTrue(); + + // Change click type to LEFT_CLICK. + mController.clickPanelController.handleAutoclickTypeChange( + AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK); + + // Verify scroll panel is hidden. + assertThat(mController.mAutoclickScrollPanel.isVisible()).isFalse(); + } + + @Test + public void sendClick_clickType_leftClick() { + MotionEventCaptor motionEventCaptor = new MotionEventCaptor(); + mController.setNext(motionEventCaptor); + + injectFakeMouseActionHoverMoveEvent(); + // Set delay to zero so click is scheduled to run immediately. + mController.mClickScheduler.updateDelay(0); + + // Send hover move event. + MotionEvent hoverMove = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 100, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 30f, + /* y= */ 0f, + /* metaState= */ 0); + hoverMove.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + mTestableLooper.processAllMessages(); + + // Verify left click sent. + assertThat(motionEventCaptor.downEvent).isNotNull(); + assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo( + MotionEvent.BUTTON_PRIMARY); + } + + @Test + public void sendClick_clickType_rightClick() { + MotionEventCaptor motionEventCaptor = new MotionEventCaptor(); + mController.setNext(motionEventCaptor); + + injectFakeMouseActionHoverMoveEvent(); + // Set delay to zero so click is scheduled to run immediately. + mController.mClickScheduler.updateDelay(0); + + // Set click type to right click. + mController.clickPanelController.handleAutoclickTypeChange( + AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK); + + // Send hover move event. + MotionEvent hoverMove = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 100, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 30f, + /* y= */ 0f, + /* metaState= */ 0); + hoverMove.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + mTestableLooper.processAllMessages(); + + // Verify right click sent. + assertThat(motionEventCaptor.downEvent).isNotNull(); + assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo( + MotionEvent.BUTTON_SECONDARY); + } + + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void hoverOnAutoclickPanel_rightClickType_forceTriggerLeftClick() { + MotionEventCaptor motionEventCaptor = new MotionEventCaptor(); + mController.setNext(motionEventCaptor); + + injectFakeMouseActionHoverMoveEvent(); + // Set delay to zero so click is scheduled to run immediately. + mController.mClickScheduler.updateDelay(0); + + // Set click type to right click. + mController.clickPanelController.handleAutoclickTypeChange( + AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK); + // Set mouse to hover panel. + AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class); + when(mockAutoclickTypePanel.isHovered()).thenReturn(true); + mController.mAutoclickTypePanel = mockAutoclickTypePanel; + + // Send hover move event. + MotionEvent hoverMove = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 100, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 30f, + /* y= */ 0f, + /* metaState= */ 0); + hoverMove.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0); + mTestableLooper.processAllMessages(); + + // Verify left click is sent due to the mouse hovering the panel. + assertThat(motionEventCaptor.downEvent).isNotNull(); + assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo( + MotionEvent.BUTTON_PRIMARY); + } + private void injectFakeMouseActionHoverMoveEvent() { MotionEvent event = getFakeMotionHoverMoveEvent(); event.setSource(InputDevice.SOURCE_MOUSE); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java new file mode 100644 index 000000000000..f445b50c7d9c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.accessibility.autoclick; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.times; + +import android.content.Context; +import android.testing.AndroidTestingRunner; +import android.testing.TestableContext; +import android.testing.TestableLooper; +import android.view.WindowManager; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Test cases for {@link AutoclickScrollPanel}. */ +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class AutoclickScrollPanelTest { + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Rule + public TestableContext mTestableContext = + new TestableContext(getInstrumentation().getContext()); + + @Mock private WindowManager mMockWindowManager; + private AutoclickScrollPanel mScrollPanel; + + @Before + public void setUp() { + mTestableContext.addMockSystemService(Context.WINDOW_SERVICE, mMockWindowManager); + mScrollPanel = new AutoclickScrollPanel(mTestableContext, mMockWindowManager); + } + + @Test + public void show_addsViewToWindowManager() { + mScrollPanel.show(); + + // Verify view is added to window manager. + verify(mMockWindowManager).addView(any(), any(WindowManager.LayoutParams.class)); + + // Verify isVisible reflects correct state. + assertThat(mScrollPanel.isVisible()).isTrue(); + } + + @Test + public void show_alreadyVisible_doesNotAddAgain() { + // Show twice. + mScrollPanel.show(); + mScrollPanel.show(); + + // Verify addView was only called once. + verify(mMockWindowManager, times(1)).addView(any(), any()); + } + + @Test + public void hide_removesViewFromWindowManager() { + // First show the panel. + mScrollPanel.show(); + // Then hide it. + mScrollPanel.hide(); + // Verify view is removed from window manager. + verify(mMockWindowManager).removeView(any()); + // Verify scroll panel is hidden. + assertThat(mScrollPanel.isVisible()).isFalse(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java index 1af59daa9c78..5922b12edc1e 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java @@ -37,6 +37,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -46,6 +47,7 @@ import android.content.Context; import android.graphics.PointF; import android.os.Looper; import android.os.SystemClock; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.testing.DexmakerShareClassLoaderRule; @@ -504,6 +506,36 @@ public class TouchExplorerTest { assertThat(sentRawEvent.getDisplayId()).isEqualTo(rawDisplayId); } + @Test + @DisableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION) + public void handleMotionEventStateTouchExploring_pointerUp_doesNotSendToManager() { + mTouchExplorer.getState().setServiceDetectsGestures(true); + mTouchExplorer.getState().clear(); + + mLastEvent = pointerDownEvent(); + mTouchExplorer.getState().startTouchExploring(); + MotionEvent event = fromTouchscreen(pointerUpEvent()); + + mTouchExplorer.onMotionEvent(event, event, /*policyFlags=*/0); + + verify(mMockAms, never()).sendMotionEventToListeningServices(event); + } + + @Test + @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION) + public void handleMotionEventStateTouchExploring_pointerUp_sendsToManager() { + mTouchExplorer.getState().setServiceDetectsGestures(true); + mTouchExplorer.getState().clear(); + + mLastEvent = pointerDownEvent(); + mTouchExplorer.getState().startTouchExploring(); + MotionEvent event = fromTouchscreen(pointerUpEvent()); + + mTouchExplorer.onMotionEvent(event, event, /*policyFlags=*/0); + + verify(mMockAms).sendMotionEventToListeningServices(event); + } + /** * Used to play back event data of a gesture by parsing the log into MotionEvents and sending * them to TouchExplorer. diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java new file mode 100644 index 000000000000..3e7d9fd05327 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.accessibility.gestures; + +import static android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_INTERACTION_END; + +import static com.android.server.accessibility.gestures.TouchState.STATE_CLEAR; +import static com.android.server.accessibility.gestures.TouchState.STATE_TOUCH_EXPLORING; + +import static com.google.common.truth.Truth.assertThat; + +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.view.Display; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.accessibility.AccessibilityManagerService; +import com.android.server.accessibility.Flags; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +@RunWith(AndroidJUnit4.class) +public class TouchStateTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private TouchState mTouchState; + @Mock private AccessibilityManagerService mMockAms; + + @Before + public void setup() { + mTouchState = new TouchState(Display.DEFAULT_DISPLAY, mMockAms); + } + + @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION) + @Test + public void injectedEvent_interactionEnd_pointerDown_startsTouchExploring() { + mTouchState.mReceivedPointerTracker.mReceivedPointersDown = 1; + mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END); + assertThat(mTouchState.getState()).isEqualTo(STATE_TOUCH_EXPLORING); + } + + @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION) + @Test + public void injectedEvent_interactionEnd_pointerUp_clears() { + mTouchState.mReceivedPointerTracker.mReceivedPointersDown = 0; + mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END); + assertThat(mTouchState.getState()).isEqualTo(STATE_CLEAR); + } + + @DisableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION) + @Test + public void injectedEvent_interactionEnd_clears() { + mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END); + assertThat(mTouchState.getState()).isEqualTo(STATE_CLEAR); + } +} diff --git a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java index ae973be17904..56f802b278c6 100644 --- a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java +++ b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpXmlPersistenceTest.java @@ -89,8 +89,7 @@ public class DiscreteAppOpXmlPersistenceTest { int attributionChainId = AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE; mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, null, opFlags, - uidState, accessTime, duration, attributionFlags, attributionChainId, - DiscreteOpsXmlRegistry.ACCESS_TYPE_FINISH_OP); + uidState, accessTime, duration, attributionFlags, attributionChainId); // Verify in-memory object is correct fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime, @@ -121,8 +120,7 @@ public class DiscreteAppOpXmlPersistenceTest { int attributionChainId = 10; mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, null, opFlags, - uidState, accessTime, duration, attributionFlags, attributionChainId, - DiscreteOpsXmlRegistry.ACCESS_TYPE_START_OP); + uidState, accessTime, duration, attributionFlags, attributionChainId); fetchDiscreteOpsAndValidate(uid, packageName, op, deviceId, null, accessTime, duration, uidState, opFlags, attributionFlags, attributionChainId); diff --git a/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java b/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java index 8eea1c73d4f2..6c66f149baa7 100644 --- a/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java +++ b/services/tests/servicestests/src/com/android/server/appop/DiscreteOpsMigrationAndRollbackTest.java @@ -70,7 +70,7 @@ public class DiscreteOpsMigrationAndRollbackTest { opEvent.getDeviceId(), opEvent.getOpCode(), opEvent.getAttributionTag(), opEvent.getOpFlags(), opEvent.getUidState(), opEvent.getAccessTime(), opEvent.getDuration(), opEvent.getAttributionFlags(), - (int) opEvent.getChainId(), DiscreteOpsRegistry.ACCESS_TYPE_NOTE_OP); + (int) opEvent.getChainId()); } xmlRegistry.writeAndClearOldAccessHistory(); assertThat(xmlRegistry.readLargestChainIdFromDiskLocked()).isEqualTo(RECORD_COUNT); @@ -104,7 +104,7 @@ public class DiscreteOpsMigrationAndRollbackTest { opEvent.getDeviceId(), opEvent.getOpCode(), opEvent.getAttributionTag(), opEvent.getOpFlags(), opEvent.getUidState(), opEvent.getAccessTime(), opEvent.getDuration(), opEvent.getAttributionFlags(), - (int) opEvent.getChainId(), DiscreteOpsRegistry.ACCESS_TYPE_NOTE_OP); + (int) opEvent.getChainId()); } // flush records from cache to the database. sqlRegistry.shutdown(); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index 30aa8cebdff6..01bcc2584fe1 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -143,6 +143,7 @@ import android.provider.DeviceConfig; import android.provider.Settings; import android.security.KeyChain; import android.security.keystore.AttestationUtils; +import android.telephony.SubscriptionInfo; import android.telephony.TelephonyManager; import android.telephony.data.ApnSetting; import android.test.MoreAsserts; @@ -8715,6 +8716,47 @@ public class DevicePolicyManagerTest extends DpmTestBase { } } + @RequiresFlagsEnabled(Flags.FLAG_REMOVE_MANAGED_ESIM_ON_WORK_PROFILE_DELETION) + @Test + public void testManagedProfileDeleted_managedEmbeddedSubscriptionDeleted() throws Exception { + // Setup PO mode. + setupProfileOwner(); + // Mock SubscriptionManager to return a subscription managed by the profile owner package. + int managedSubscriptionId = 42; + SubscriptionInfo managedSubscription = new SubscriptionInfo.Builder().setCardId(1).setId( + managedSubscriptionId).setGroupOwner(admin1.getPackageName()).build(); + when(getServices().subscriptionManager.getAvailableSubscriptionInfoList()).thenReturn( + List.of(managedSubscription)); + + // Send a ACTION_MANAGED_PROFILE_REMOVED broadcast to emulate a managed profile being + // removed. + sendBroadcastWithUser(dpms, Intent.ACTION_MANAGED_PROFILE_REMOVED, CALLER_USER_HANDLE); + + // Verify that EuiccManager was called to delete the subscription. + verify(getServices().euiccManager).deleteSubscription(eq(managedSubscriptionId), any()); + } + + @RequiresFlagsDisabled(Flags.FLAG_REMOVE_MANAGED_ESIM_ON_WORK_PROFILE_DELETION) + @Test + public void testManagedProfileDeleted_flagDisabled_managedEmbeddedSubscriptionDeleted() + throws Exception { + // Set up PO mode. + setupProfileOwner(); + // Mock SubscriptionManager to return a subscription managed by the profile owner package. + int managedSubscriptionId = 42; + SubscriptionInfo managedSubscription = new SubscriptionInfo.Builder().setCardId(1).setId( + managedSubscriptionId).setGroupOwner(admin1.getPackageName()).build(); + when(getServices().subscriptionManager.getAvailableSubscriptionInfoList()).thenReturn( + List.of(managedSubscription)); + + // Send a ACTION_MANAGED_PROFILE_REMOVED broadcast to emulate a managed profile being + // removed. + sendBroadcastWithUser(dpms, Intent.ACTION_MANAGED_PROFILE_REMOVED, CALLER_USER_HANDLE); + + // Verify that EuiccManager was not called to delete the subscription. + verifyZeroInteractions(getServices().euiccManager); + } + private void setupVpnAuthorization(String userVpnPackage, int userVpnUid) { final AppOpsManager.PackageOps vpnOp = new AppOpsManager.PackageOps(userVpnPackage, userVpnUid, List.of(new AppOpsManager.OpEntry( diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java index 00b0c558b4e3..479af73bae3c 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java @@ -253,6 +253,8 @@ public class DpmMockContext extends MockContext { return mMockSystemServices.subscriptionManager; case Context.USB_SERVICE: return mMockSystemServices.usbManager; + case Context.EUICC_SERVICE: + return mMockSystemServices.euiccManager; } throw new UnsupportedOperationException(); } @@ -487,6 +489,14 @@ public class DpmMockContext extends MockContext { } @Override + public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user, + IntentFilter filter, String broadcastPermission, Handler scheduler, int flags) { + mMockSystemServices.registerReceiver(receiver, filter, scheduler); + return spiedContext.registerReceiverAsUser(receiver, user, filter, broadcastPermission, + scheduler, flags); + } + + @Override public void unregisterReceiver(BroadcastReceiver receiver) { mMockSystemServices.unregisterReceiver(receiver); spiedContext.unregisterReceiver(receiver); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java index 3e4448c1dafa..d01fa91e22c2 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java @@ -68,6 +68,7 @@ import android.provider.Settings; import android.security.KeyChain; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; +import android.telephony.euicc.EuiccManager; import android.test.mock.MockContentProvider; import android.test.mock.MockContentResolver; import android.util.ArrayMap; @@ -151,6 +152,7 @@ public class MockSystemServices { public final File dataDir; public final PolicyPathProvider pathProvider; public final SupervisionManagerInternal supervisionManagerInternal; + public final EuiccManager euiccManager; private final Map<String, PackageState> mTestPackageStates = new ArrayMap<>(); @@ -206,6 +208,7 @@ public class MockSystemServices { roleManagerForMock = mock(RoleManagerForMock.class); subscriptionManager = mock(SubscriptionManager.class); supervisionManagerInternal = mock(SupervisionManagerInternal.class); + euiccManager = mock(EuiccManager.class); // Package manager is huge, so we use a partial mock instead. packageManager = spy(realContext.getPackageManager()); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java index b0ffebb973a1..aa1d5835bfc8 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java @@ -2295,6 +2295,38 @@ public class HdmiCecLocalDeviceTvTest { .hasSize(1); } + @Test + public void onOneTouchPlay_wakeUp_exist_device() { + HdmiCecMessage requestActiveSource = + HdmiCecMessageBuilder.buildRequestActiveSource(ADDR_TV); + + // Go to standby to trigger RequestActiveSourceAction for playback_1 + mHdmiControlService.onStandby(STANDBY_SCREEN_OFF); + mTestLooper.dispatchAll(); + + mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_1, SendMessageResult.SUCCESS); + mHdmiControlService.onWakeUp(WAKE_UP_SCREEN_ON); + mTestLooper.dispatchAll(); + + // Skip the LauncherX API timeout. + mTestLooper.moveTimeForward(TIMEOUT_WAIT_FOR_TV_ASSERT_ACTIVE_SOURCE_MS); + mTestLooper.dispatchAll(); + + assertThat(mNativeWrapper.getResultMessages()).contains(requestActiveSource); + mNativeWrapper.clearResultMessages(); + + // turn off TV and wake up with one touch play + mHdmiControlService.onStandby(STANDBY_SCREEN_OFF); + mTestLooper.dispatchAll(); + + // FakePowerManagerWrapper#wakeUp() doesn't broadcast Intent.ACTION_SCREEN_ON + // manually trigger onWakeUp to mock OTP + mHdmiControlService.onWakeUp(WAKE_UP_SCREEN_ON); + mTestLooper.dispatchAll(); + + assertThat(mNativeWrapper.getResultMessages()).doesNotContain(requestActiveSource); + } + @Test public void handleReportAudioStatus_SamOnAvrStandby_startSystemAudioActionFromTv() { diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java index 2da2f50447c7..e836780b3f71 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java @@ -16,6 +16,7 @@ package com.android.server.locksettings; +import static android.content.pm.UserInfo.FLAG_FOR_TESTING; import static android.content.pm.UserInfo.FLAG_FULL; import static android.content.pm.UserInfo.FLAG_MAIN; import static android.content.pm.UserInfo.FLAG_PRIMARY; @@ -44,6 +45,8 @@ import static org.mockito.Mockito.when; import android.app.PropertyInvalidatedCache; import android.app.admin.PasswordMetrics; +import android.content.ComponentName; +import android.content.pm.UserInfo; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; @@ -357,6 +360,45 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { } @Test + public void testEscrowDataRetainedWhenManagedUserVerifiesCredential() throws RemoteException { + when(mDeviceStateCache.isUserOrganizationManaged(anyInt())).thenReturn(true); + + LockscreenCredential password = newPassword("password"); + initSpAndSetCredential(PRIMARY_USER_ID, password); + + mService.verifyCredential(password, PRIMARY_USER_ID, 0 /* flags */); + + assertTrue("Escrow data was destroyed", mSpManager.hasEscrowData(PRIMARY_USER_ID)); + } + + @Test + public void testEscrowDataRetainedWhenUnmanagedTestUserVerifiesCredential() + throws RemoteException { + when(mDeviceStateCache.isUserOrganizationManaged(anyInt())).thenReturn(false); + UserInfo userInfo = mUserManagerInternal.getUserInfo(PRIMARY_USER_ID); + userInfo.flags |= FLAG_FOR_TESTING; + + LockscreenCredential password = newPassword("password"); + initSpAndSetCredential(PRIMARY_USER_ID, password); + + mService.verifyCredential(password, PRIMARY_USER_ID, 0 /* flags */); + + assertTrue("Escrow data was destroyed", mSpManager.hasEscrowData(PRIMARY_USER_ID)); + } + + @Test + public void testEscrowDataDeletedWhenUnmanagedUserVerifiesCredential() throws RemoteException { + when(mDeviceStateCache.isUserOrganizationManaged(anyInt())).thenReturn(false); + + LockscreenCredential password = newPassword("password"); + initSpAndSetCredential(PRIMARY_USER_ID, password); + + mService.verifyCredential(password, PRIMARY_USER_ID, 0 /* flags */); + + assertFalse("Escrow data wasn't destroyed", mSpManager.hasAnyEscrowData(PRIMARY_USER_ID)); + } + + @Test public void testTokenBasedClearPassword() throws RemoteException { LockscreenCredential password = newPassword("password"); LockscreenCredential pattern = newPattern("123654"); diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java index f9946604ad5d..b842d3a42f26 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java @@ -389,6 +389,44 @@ public final class UserManagerTest { } @Test + public void testSupervisingProfile() throws Exception { + assumeTrue("Device doesn't support supervising profiles ", + mUserManager.isUserTypeEnabled(UserManager.USER_TYPE_PROFILE_SUPERVISING)); + + final UserTypeDetails userTypeDetails = + UserTypeFactory.getUserTypes().get(UserManager.USER_TYPE_PROFILE_SUPERVISING); + assertWithMessage("No supervising user type on device").that(userTypeDetails).isNotNull(); + + + // Create supervising profile if it doesn't exist + UserInfo supervisingUser = getSupervisingProfile(); + if (supervisingUser == null) { + supervisingUser = createUser("Supervising", + UserManager.USER_TYPE_PROFILE_SUPERVISING, /*flags*/ 0); + } + assertWithMessage("Couldn't create supervising profile").that(supervisingUser).isNotNull(); + UserHandle supervisingHandle = supervisingUser.getUserHandle(); + + // Test that only one supervising profile can be created + final UserInfo secondSupervisingProfile = + createUser("Supervising", UserManager.USER_TYPE_PROFILE_SUPERVISING, + /*flags*/ 0); + assertThat(secondSupervisingProfile).isNull(); + + // Verify that the supervising profile doesn't have a parent + assertThat(mUserManager.getProfileParent(supervisingHandle.getIdentifier())).isNull(); + + // Make sure that the supervising profile can be started in the background, and that it + // is visible + final boolean isStarted = mActivityManager.startProfile(supervisingHandle); + assertWithMessage("Unable to start supervising profile").that(isStarted).isTrue(); + final UserManager umSupervising = (UserManager) mContext.createPackageContextAsUser( + "android", 0, supervisingHandle).getSystemService(Context.USER_SERVICE); + assertWithMessage("Supervising profile not visible").that( + umSupervising.isUserVisible()).isTrue(); + } + + @Test public void testGetProfileAccessibilityString_throwsExceptionForNonProfileUser() { UserInfo user1 = createUser("Guest 1", UserInfo.FLAG_GUEST); assertThat(user1).isNotNull(); @@ -2198,4 +2236,13 @@ public final class UserManagerTest { assertEquals(actual.getLevel(), expected.getLevel()); } + @Nullable + private UserInfo getSupervisingProfile() { + for (UserInfo user : mUserManager.getUsers()) { + if (user.userType.equals(UserManager.USER_TYPE_PROFILE_SUPERVISING)) { + return user; + } + } + return null; + } } diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java index 04335df8c454..2baf0c141c89 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java @@ -247,6 +247,7 @@ public class VibrationThreadTest { assertThat(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes()).isEmpty(); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_singleVibratorWaveform_runsVibrationAndChangesAmplitudes() { mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); @@ -269,7 +270,10 @@ public class VibrationThreadTest { } @Test - @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + @EnableFlags({ + Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED, + Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING, + }) public void vibrate_singleWaveformWithAdaptiveHapticsScaling_scalesAmplitudesProperly() { // No user settings scale. setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, @@ -296,7 +300,10 @@ public class VibrationThreadTest { } @Test - @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + @EnableFlags({ + Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED, + Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING, + }) public void vibrate_withVibrationParamsRequestStalling_timeoutRequestAndApplyNoScaling() { // No user settings scale. setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, @@ -357,6 +364,7 @@ public class VibrationThreadTest { } } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_singleVibratorRepeatingShortAlwaysOnWaveform_turnsVibratorOnForLonger() throws Exception { @@ -380,6 +388,7 @@ public class VibrationThreadTest { .containsExactly(expectedOneShot(5000)).inOrder(); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_singleVibratorPatternWithZeroDurationSteps_skipsZeroDurationSteps() { mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); @@ -399,6 +408,7 @@ public class VibrationThreadTest { .containsExactlyElementsIn(expectedOneShots(100L, 150L)).inOrder(); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_singleVibratorPatternWithZeroDurationAndAmplitude_skipsZeroDurationSteps() { mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); @@ -537,6 +547,7 @@ public class VibrationThreadTest { assertThat(fakeVibrator.getEffectSegments(vibration.id)).hasSize(10); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_singleVibratorRepeatingLongAlwaysOnWaveform_turnsVibratorOnForACycle() throws Exception { @@ -700,6 +711,7 @@ public class VibrationThreadTest { .containsExactly(expectedPrebaked(VibrationEffect.EFFECT_THUD)).inOrder(); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_singleVibratorPrebakedAndUnsupportedEffectWithFallback_runsFallback() { mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); @@ -1247,6 +1259,7 @@ public class VibrationThreadTest { .containsExactly(expected).inOrder(); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_multipleStereo_runsVibrationOnRightVibrators() { mockVibrators(1, 2, 3, 4); @@ -1479,6 +1492,7 @@ public class VibrationThreadTest { assertThat(mVibratorProviders.get(1).getAmplitudes()).isEmpty(); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_multipleWaveforms_playsWaveformsInParallel() throws Exception { mockVibrators(1, 2, 3); @@ -1544,8 +1558,8 @@ public class VibrationThreadTest { VibrationEffect.createOneShot( expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE))); - startThreadAndDispatcher(vibration); long startTime = SystemClock.elapsedRealtime(); + startThreadAndDispatcher(vibration); vibration.waitForEnd(); long vibrationEndTime = SystemClock.elapsedRealtime(); @@ -1554,15 +1568,13 @@ public class VibrationThreadTest { long completionTime = SystemClock.elapsedRealtime(); verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); - // Vibration ends after duration, thread completed after ramp down - assertThat(vibrationEndTime - startTime).isAtLeast(expectedDuration); + // Vibration ends before ramp down, thread completed after ramp down assertThat(vibrationEndTime - startTime).isLessThan(expectedDuration + rampDownDuration); assertThat(completionTime - startTime).isAtLeast(expectedDuration + rampDownDuration); } @Test - public void vibrate_withVibratorCallbackDelayShorterThanTimeout_vibrationFinishedAfterDelay() - throws Exception { + public void vibrate_withVibratorCallbackDelayShorterThanTimeout_vibrationFinishedAfterDelay() { long expectedDuration = 10; long callbackDelay = VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT / 2; @@ -1577,10 +1589,8 @@ public class VibrationThreadTest { long startTime = SystemClock.elapsedRealtime(); startThreadAndDispatcher(vibration); - vibration.waitForEnd(); - long vibrationEndTime = SystemClock.elapsedRealtime(); - waitForCompletion(TEST_TIMEOUT_MILLIS); + long vibrationEndTime = SystemClock.elapsedRealtime(); verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); assertThat(vibrationEndTime - startTime).isAtLeast(expectedDuration + callbackDelay); @@ -1588,8 +1598,7 @@ public class VibrationThreadTest { @LargeTest @Test - public void vibrate_withVibratorCallbackDelayLongerThanTimeout_vibrationFinishedAfterTimeout() - throws Exception { + public void vibrate_withVibratorCallbackDelayLongerThanTimeout_vibrationFinishedAfterTimeout() { long expectedDuration = 10; long callbackTimeout = VibrationStepConductor.CALLBACKS_EXTRA_TIMEOUT; long callbackDelay = callbackTimeout * 2; @@ -1602,21 +1611,17 @@ public class VibrationThreadTest { VibrationEffect.createOneShot( expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE))); - startThreadAndDispatcher(vibration); long startTime = SystemClock.elapsedRealtime(); - - vibration.waitForEnd(); - long vibrationEndTime = SystemClock.elapsedRealtime(); + startThreadAndDispatcher(vibration); waitForCompletion(callbackDelay + TEST_TIMEOUT_MILLIS); - long completionTime = SystemClock.elapsedRealtime(); + long vibrationEndTime = SystemClock.elapsedRealtime(); verify(mControllerCallbacks, never()) .onComplete(eq(VIBRATOR_ID), eq(vibration.id), anyLong()); // Vibration ends and thread completes after timeout, before the HAL callback assertThat(vibrationEndTime - startTime).isAtLeast(expectedDuration + callbackTimeout); assertThat(vibrationEndTime - startTime).isLessThan(expectedDuration + callbackDelay); - assertThat(completionTime - startTime).isLessThan(expectedDuration + callbackDelay); } @LargeTest @@ -1637,8 +1642,8 @@ public class VibrationThreadTest { Arrays.fill(amplitudes, VibrationEffect.DEFAULT_AMPLITUDE); VibrationEffect effect = VibrationEffect.createWaveform(timings, amplitudes, -1); - startThreadAndDispatcher(effect); long startTime = SystemClock.elapsedRealtime(); + startThreadAndDispatcher(effect); waitForCompletion(totalDuration + TEST_TIMEOUT_MILLIS); long delay = Math.abs(SystemClock.elapsedRealtime() - startTime - totalDuration); @@ -1806,6 +1811,7 @@ public class VibrationThreadTest { assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse(); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_waveformWithRampDown_addsRampDownAfterVibrationCompleted() { when(mVibrationConfigMock.getRampDownDurationMs()).thenReturn(15); @@ -1833,6 +1839,7 @@ public class VibrationThreadTest { } } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_waveformWithRampDown_triggersCallbackWhenOriginalVibrationEnds() throws Exception { @@ -1863,6 +1870,7 @@ public class VibrationThreadTest { verify(mManagerHooks).onVibrationThreadReleased(vibration.id); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_waveformCancelledWithRampDown_addsRampDownAfterVibrationCancelled() throws Exception { @@ -2057,6 +2065,7 @@ public class VibrationThreadTest { .containsExactly(expectedPrebaked(EFFECT_CLICK)).inOrder(); } + @EnableFlags(Flags.FLAG_FIX_VIBRATION_THREAD_CALLBACK_HANDLING) @Test public void vibrate_multipleVibratorsSequentialInSession_runsInOrderWithoutDelaysAndNoOffs() { mockVibrators(1, 2, 3); diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java index 3ca019728c2b..fcdf88f16550 100644 --- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java @@ -605,29 +605,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { } @Test - public void testKeyGestureAccessibilityShortcutChord() { - Assert.assertTrue( - sendKeyGestureEventStart( - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD)); - mPhoneWindowManager.moveTimeForward(5000); - Assert.assertTrue( - sendKeyGestureEventCancel( - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD)); - mPhoneWindowManager.assertAccessibilityKeychordCalled(); - } - - @Test - public void testKeyGestureAccessibilityShortcutChordCancelled() { - Assert.assertTrue( - sendKeyGestureEventStart( - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD)); - Assert.assertTrue( - sendKeyGestureEventCancel( - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD)); - mPhoneWindowManager.assertAccessibilityKeychordNotCalled(); - } - - @Test public void testKeyGestureRingerToggleChord() { mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE); Assert.assertTrue( @@ -670,29 +647,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { } @Test - public void testKeyGestureAccessibilityTvShortcutChord() { - Assert.assertTrue( - sendKeyGestureEventStart( - KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD)); - mPhoneWindowManager.moveTimeForward(5000); - Assert.assertTrue( - sendKeyGestureEventCancel( - KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD)); - mPhoneWindowManager.assertAccessibilityKeychordCalled(); - } - - @Test - public void testKeyGestureAccessibilityTvShortcutChordCancelled() { - Assert.assertTrue( - sendKeyGestureEventStart( - KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD)); - Assert.assertTrue( - sendKeyGestureEventCancel( - KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD)); - mPhoneWindowManager.assertAccessibilityKeychordNotCalled(); - } - - @Test public void testKeyGestureTvTriggerBugReport() { Assert.assertTrue( sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT)); diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index f88492477487..e56fd3c6272d 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -750,11 +750,6 @@ class TestPhoneWindowManager { verify(mAccessibilityShortcutController).performAccessibilityShortcut(); } - void assertAccessibilityKeychordNotCalled() { - mTestLooper.dispatchAll(); - verify(mAccessibilityShortcutController, never()).performAccessibilityShortcut(); - } - void assertCloseAllDialogs() { verify(mContext).closeSystemDialogs(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java index 948371f74a9c..ad706e879b72 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java @@ -63,11 +63,23 @@ public class ActivitySnapshotControllerTests extends TaskSnapshotPersisterTestBa super(0.8f /* highResScale */, 0.5f /* lowResScale */); } + private class TestActivitySnapshotController extends ActivitySnapshotController { + TestActivitySnapshotController(WindowManagerService service, + SnapshotPersistQueue persistQueue) { + super(service, persistQueue); + } + @Override + BaseAppSnapshotPersister.PersistInfoProvider createPersistInfoProvider( + WindowManagerService service) { + return mPersister.mPersistInfoProvider; + } + } @Override @Before public void setUp() { super.setUp(); - mActivitySnapshotController = new ActivitySnapshotController(mWm, mSnapshotPersistQueue); + mActivitySnapshotController = new TestActivitySnapshotController( + mWm, mSnapshotPersistQueue); spyOn(mActivitySnapshotController); doReturn(false).when(mActivitySnapshotController).shouldDisableSnapshots(); mActivitySnapshotController.resetTmpFields(); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index e3e9cc426bb3..08b0077c49b3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -797,7 +797,7 @@ public class ActivityStarterTests extends WindowTestsBase { // Create adjacent tasks and put one activity under it final Task parent = new TaskBuilder(mSupervisor).build(); final Task adjacentParent = new TaskBuilder(mSupervisor).build(); - parent.setAdjacentTaskFragment(adjacentParent); + parent.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(parent, adjacentParent)); final ActivityRecord activity = new ActivityBuilder(mAtm) .setParentTask(parent) .setCreateTask(true).build(); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java index a9be47d71213..86d901b640ff 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java @@ -488,14 +488,13 @@ public class ActivityTaskSupervisorTests extends WindowTestsBase { WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false); final TaskFragment tf2 = createChildTaskFragment(/* parent */ rootTask, WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false); - tf1.setAdjacentTaskFragment(tf2); + tf1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf1, tf2)); assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isTrue(); } @Test - @EnableFlags({Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, - Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS}) + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) public void testOpaque_rootTask_nonFillingOpaqueAdjacentChildren_multipleAdjacent_isOpaque() { final Task rootTask = new TaskBuilder(mSupervisor).setOnTop(true).build(); final TaskFragment tf1 = createChildTaskFragment(/* parent */ rootTask, 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 8fe08553db95..cb98b9a490d8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java @@ -243,6 +243,11 @@ class AppCompatActivityRobot { .getAspectRatioOverrides()).getUserMinAspectRatio(); } + void setShouldRefreshActivityForCameraCompat(boolean enabled) { + doReturn(enabled).when(mActivityStack.top().mAppCompatController.getCameraOverrides()) + .shouldRefreshActivityForCameraCompat(); + } + void setIgnoreOrientationRequest(boolean enabled) { mDisplayContent.setIgnoreOrientationRequest(enabled); } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java index 05f6ed644632..7ef85262dfc2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java @@ -64,6 +64,19 @@ class AppCompatConfigurationRobot { .isCameraCompatTreatmentEnabledAtBuildTime(); } + void setCameraCompatAspectRatio(float aspectRatio) { + doReturn(aspectRatio).when(mAppCompatConfiguration).getCameraCompatAspectRatio(); + } + + void enableCameraCompatRefresh(boolean enabled) { + doReturn(enabled).when(mAppCompatConfiguration).isCameraCompatRefreshEnabled(); + } + + void enableCameraCompatRefreshCycleThroughStop(boolean enabled) { + doReturn(enabled).when(mAppCompatConfiguration) + .isCameraCompatRefreshCycleThroughStopEnabled(); + } + void enableUserAppAspectRatioFullscreen(boolean enabled) { doReturn(enabled).when(mAppCompatConfiguration).isUserAppAspectRatioFullscreenEnabled(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index bdee3c323549..dd3e9fcbbdaf 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -343,8 +343,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { // Adjacent + no companion => unable to predict // TF1 | TF2 - tf1.setAdjacentTaskFragment(tf2); - tf2.setAdjacentTaskFragment(tf1); + tf1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf1, tf2)); predictable = BackNavigationController.getAnimatablePrevActivities(task, topAr, outPrevActivities); assertTrue(outPrevActivities.isEmpty()); @@ -393,8 +392,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { // Adjacent => predict for previous activity. // TF2 | TF3 // TF1 - tf2.setAdjacentTaskFragment(tf3); - tf3.setAdjacentTaskFragment(tf2); + tf2.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf2, tf3)); predictable = BackNavigationController.getAnimatablePrevActivities(task, topAr, outPrevActivities); assertTrue(outPrevActivities.contains(prevAr)); @@ -657,8 +655,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { final TaskFragment secondaryTf = createTaskFragmentWithEmbeddedActivity(task, organizer); final ActivityRecord primaryActivity = primaryTf.getTopMostActivity(); final ActivityRecord secondaryActivity = secondaryTf.getTopMostActivity(); - primaryTf.setAdjacentTaskFragment(secondaryTf); - secondaryTf.setAdjacentTaskFragment(primaryTf); + primaryTf.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(primaryTf, secondaryTf)); final WindowState primaryWindow = mock(WindowState.class); final WindowState secondaryWindow = mock(WindowState.class); 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 f5bec04a98d5..6f959812d742 100644 --- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java @@ -21,13 +21,13 @@ import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_LANDSCAPE_ import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE; import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE; import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT; +import static android.app.CameraCompatTaskInfo.FreeformCameraCompatMode; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE; import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_SIMULATE_REQUESTED_ORIENTATION; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT; -import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_FULL_USER; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; @@ -40,18 +40,15 @@ import static android.view.Surface.ROTATION_90; 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.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -59,13 +56,11 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.annotation.NonNull; -import android.app.CameraCompatTaskInfo; import android.app.IApplicationThread; import android.app.WindowConfiguration.WindowingMode; import android.app.servertransaction.RefreshCallbackItem; import android.app.servertransaction.ResumeActivityItem; import android.compat.testing.PlatformCompatChangeRule; -import android.content.ComponentName; import android.content.pm.ActivityInfo.ScreenOrientation; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; @@ -73,17 +68,16 @@ import android.content.res.Configuration.Orientation; import android.graphics.Rect; import android.hardware.camera2.CameraManager; import android.os.Handler; +import android.os.RemoteException; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; -import android.view.DisplayInfo; import android.view.Surface; import androidx.test.filters.SmallTest; import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; @@ -91,6 +85,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * Tests for {@link CameraCompatFreeformPolicy}. @@ -109,30 +104,18 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { private static final String TEST_PACKAGE_1 = "com.android.frameworks.wmtests"; private static final String TEST_PACKAGE_2 = "com.test.package.two"; private static final String CAMERA_ID_1 = "camera-1"; - private AppCompatConfiguration mAppCompatConfiguration; - - private CameraManager.AvailabilityCallback mCameraAvailabilityCallback; - private CameraCompatFreeformPolicy mCameraCompatFreeformPolicy; - private ActivityRecord mActivity; - - // TODO(b/384465100): use a robot structure. - @Before - public void setUp() throws Exception { - setupAppCompatConfiguration(); - setupCameraManager(); - setupHandler(); - doReturn(true).when(() -> DesktopModeHelper.canEnterDesktopMode(any())); - } @Test @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testFeatureDisabled_cameraCompatFreeformPolicyNotCreated() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertNull(mCameraCompatFreeformPolicy); + robot.checkCameraCompatPolicyNotCreated(); + }); } @Test @@ -140,31 +123,37 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT}) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_SIMULATE_REQUESTED_ORIENTATION}) public void testIsCameraRunningAndWindowingModeEligible_disabledViaOverride_returnsFalse() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertFalse(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity)); + robot.checkIsCameraRunningAndWindowingModeEligible(false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testIsCameraRunningAndWindowingModeEligible_cameraNotRunning_returnsFalse() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - assertFalse(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity)); + robot.checkIsCameraRunningAndWindowingModeEligible(false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testIsCameraRunningAndWindowingModeEligible_notFreeformWindowing_returnsFalse() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertFalse(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity)); + robot.checkIsCameraRunningAndWindowingModeEligible(false); + }); } @Test @@ -172,64 +161,76 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testIsCameraRunningAndWindowingModeEligible_optInFreeformCameraRunning_true() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertTrue(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity)); + robot.checkIsCameraRunningAndWindowingModeEligible(true); + }); } @Test @EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING, FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT}) public void testIsCameraRunningAndWindowingModeEligible_freeformCameraRunning_true() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertTrue(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity)); + robot.checkIsCameraRunningAndWindowingModeEligible(true); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT) public void testIsFreeformLetterboxingForCameraAllowed_optInMechanism_notOptedIn_retFalse() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity)); + robot.checkIsFreeformLetterboxingForCameraAllowed(false); + }); } @Test @EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING, FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT}) public void testIsFreeformLetterboxingForCameraAllowed_notOptedOut_returnsTrue() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertTrue(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity)); + robot.checkIsFreeformLetterboxingForCameraAllowed(true); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testIsFreeformLetterboxingForCameraAllowed_cameraNotRunning_returnsFalse() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity)); + robot.checkIsFreeformLetterboxingForCameraAllowed(false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testIsFreeformLetterboxingForCameraAllowed_notFreeformWindowing_returnsFalse() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity)); + robot.checkIsFreeformLetterboxingForCameraAllowed(false); + }); } @Test @@ -237,519 +238,603 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testIsFreeformLetterboxingForCameraAllowed_optInFreeformCameraRunning_true() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertTrue(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity)); + robot.checkIsFreeformLetterboxingForCameraAllowed(true); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testFullscreen_doesNotActivateCameraCompatMode() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); - doReturn(false).when(mActivity).inFreeformWindowingMode(); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); + robot.setInFreeformWindowingMode(false); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertNotInCameraCompatMode(); + robot.assertNotInCameraCompatMode(); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testOrientationUnspecified_doesNotActivateCameraCompatMode() { - configureActivity(SCREEN_ORIENTATION_UNSPECIFIED); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_UNSPECIFIED); - assertNotInCameraCompatMode(); + robot.assertNotInCameraCompatMode(); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testNoCameraConnection_doesNotActivateCameraCompatMode() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - assertNotInCameraCompatMode(); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + + robot.assertNotInCameraCompatMode(); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() throws Exception { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - setDisplayRotation(ROTATION_0); + public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.activity().rotateDisplayForTopActivity(ROTATION_0); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT); - assertActivityRefreshRequested(/* refreshRequested */ false); + robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT); + robot.assertActivityRefreshRequested(/* refreshRequested */ false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testCameraConnected_deviceInLandscape_portraitCameraCompatMode() throws Exception { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - setDisplayRotation(ROTATION_270); + public void testCameraConnected_deviceInLandscape_portraitCameraCompatMode() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.activity().rotateDisplayForTopActivity(ROTATION_270); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE); - assertActivityRefreshRequested(/* refreshRequested */ false); + robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE); + robot.assertActivityRefreshRequested(/* refreshRequested */ false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testCameraConnected_deviceInPortrait_landscapeCameraCompatMode() throws Exception { - configureActivity(SCREEN_ORIENTATION_LANDSCAPE); - setDisplayRotation(ROTATION_0); + public void testCameraConnected_deviceInPortrait_landscapeCameraCompatMode() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_LANDSCAPE); + robot.activity().rotateDisplayForTopActivity(ROTATION_0); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_PORTRAIT); - assertActivityRefreshRequested(/* refreshRequested */ false); + robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_PORTRAIT); + robot.assertActivityRefreshRequested(/* refreshRequested */ false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testCameraConnected_deviceInLandscape_landscapeCameraCompatMode() throws Exception { - configureActivity(SCREEN_ORIENTATION_LANDSCAPE); - setDisplayRotation(ROTATION_270); + public void testCameraConnected_deviceInLandscape_landscapeCameraCompatMode() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_LANDSCAPE); + robot.activity().rotateDisplayForTopActivity(ROTATION_270); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_LANDSCAPE); - assertActivityRefreshRequested(/* refreshRequested */ false); + robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_LANDSCAPE); + robot.assertActivityRefreshRequested(/* refreshRequested */ false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testCameraReconnected_cameraCompatModeAndRefresh() throws Exception { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - setDisplayRotation(ROTATION_270); + public void testCameraReconnected_cameraCompatModeAndRefresh() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.activity().rotateDisplayForTopActivity(ROTATION_270); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - callOnActivityConfigurationChanging(mActivity, /* letterboxNew= */ true, + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.callOnActivityConfigurationChanging(/* letterboxNew= */ true, /* lastLetterbox= */ false); - assertActivityRefreshRequested(/* refreshRequested */ true); - onCameraClosed(CAMERA_ID_1); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - // Activity is letterboxed from the previous configuration change. - callOnActivityConfigurationChanging(mActivity, /* letterboxNew= */ true, - /* lastLetterbox= */ true); + robot.assertActivityRefreshRequested(/* refreshRequested */ true); + robot.onCameraClosed(CAMERA_ID_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + // Activity is letterboxed from the previous configuration change. + robot.callOnActivityConfigurationChanging(/* letterboxNew= */ true, + /* lastLetterbox= */ true); - assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE); - assertActivityRefreshRequested(/* refreshRequested */ true); + robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE); + robot.assertActivityRefreshRequested(/* refreshRequested */ true); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testCameraOpenedForDifferentPackage_notInCameraCompatMode() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2); - assertNotInCameraCompatMode(); + robot.assertNotInCameraCompatMode(); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT) public void testShouldApplyCameraCompatFreeformTreatment_overrideNotEnabled_returnsFalse() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertFalse(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity, - /* checkOrientation */ true)); + robot.checkIsCameraCompatTreatmentActiveForTopActivity(false); + }); } @Test @EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING, FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT}) public void testShouldApplyCameraCompatFreeformTreatment_notOptedOut_returnsTrue() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertTrue(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity, - /* checkOrientation */ true)); + robot.checkIsCameraCompatTreatmentActiveForTopActivity(true); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges(OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT) public void testShouldApplyCameraCompatFreeformTreatment_enabledByOverride_returnsTrue() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertTrue(mActivity.info - .isChangeEnabled(OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT)); - assertTrue(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity, - /* checkOrientation */ true)); + robot.checkIsCameraCompatTreatmentActiveForTopActivity(true); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testShouldRefreshActivity_appBoundsChanged_returnsTrue() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - Configuration oldConfiguration = createConfiguration(/* letterbox= */ false); - Configuration newConfiguration = createConfiguration(/* letterbox= */ true); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - assertTrue(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration, - oldConfiguration)); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + robot.checkShouldRefreshActivity(/* expected= */ true, + robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0), + robot.createConfiguration(/* letterbox= */ false, /* rotation= */ 0)); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testShouldRefreshActivity_displayRotationChanged_returnsTrue() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - Configuration oldConfiguration = createConfiguration(/* letterbox= */ true); - Configuration newConfiguration = createConfiguration(/* letterbox= */ true); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - oldConfiguration.windowConfiguration.setDisplayRotation(0); - newConfiguration.windowConfiguration.setDisplayRotation(90); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertTrue(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration, - oldConfiguration)); + robot.checkShouldRefreshActivity(/* expected= */ true, + robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 90), + robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0)); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testShouldRefreshActivity_appBoundsNorDisplayChanged_returnsFalse() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - Configuration oldConfiguration = createConfiguration(/* letterbox= */ true); - Configuration newConfiguration = createConfiguration(/* letterbox= */ true); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); - oldConfiguration.windowConfiguration.setDisplayRotation(0); - newConfiguration.windowConfiguration.setDisplayRotation(0); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - assertFalse(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration, - oldConfiguration)); + robot.checkShouldRefreshActivity(/* expected= */ false, + robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0), + robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0)); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh() - throws Exception { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - - doReturn(false).when(mActivity.mAppCompatController.getCameraOverrides()) - .shouldRefreshActivityForCameraCompat(); + public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.activity().setShouldRefreshActivityForCameraCompat(false); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - callOnActivityConfigurationChanging(mActivity); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.callOnActivityConfigurationChanging(); - assertActivityRefreshRequested(/* refreshRequested */ false); + robot.assertActivityRefreshRequested(/* refreshRequested */ false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception { - when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) - .thenReturn(false); + public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.conf().enableCameraCompatRefreshCycleThroughStop(false); - configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.callOnActivityConfigurationChanging(); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - callOnActivityConfigurationChanging(mActivity); - - assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false); + robot.assertActivityRefreshRequested(/* refreshRequested */ true, + /* cycleThroughStop */ false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp() - throws Exception { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - doReturn(true).when(mActivity.mAppCompatController.getCameraOverrides()) - .shouldRefreshActivityViaPauseForCameraCompat(); + public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.setShouldRefreshActivityViaPause(true); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - callOnActivityConfigurationChanging(mActivity); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.callOnActivityConfigurationChanging(); - assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false); + robot.assertActivityRefreshRequested(/* refreshRequested */ true, + /* cycleThroughStop */ false); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testGetCameraCompatAspectRatio_activityNotInCameraCompat_returnsDefaultAspRatio() { - configureActivity(SCREEN_ORIENTATION_FULL_USER); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_FULL_USER); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - callOnActivityConfigurationChanging(mActivity); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.callOnActivityConfigurationChanging(); - assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO, - mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity), - /* delta= */ 0.001); + robot.checkCameraCompatAspectRatioEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testGetCameraCompatAspectRatio_activityInCameraCompat_returnsConfigAspectRatio() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - final float configAspectRatio = 1.5f; - mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + final float configAspectRatio = 1.5f; + robot.conf().setCameraCompatAspectRatio(configAspectRatio); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - callOnActivityConfigurationChanging(mActivity); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.callOnActivityConfigurationChanging(); - assertEquals(configAspectRatio, - mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity), - /* delta= */ 0.001); + robot.checkCameraCompatAspectRatioEquals(configAspectRatio); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testGetCameraCompatAspectRatio_inCameraCompatPerAppOverride_returnDefAspectRatio() { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - final float configAspectRatio = 1.5f; - mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio); - doReturn(true).when(mActivity.mAppCompatController.getCameraOverrides()) - .isOverrideMinAspectRatioForCameraEnabled(); + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.conf().setCameraCompatAspectRatio(1.5f); + robot.setOverrideMinAspectRatioEnabled(true); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - callOnActivityConfigurationChanging(mActivity); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.callOnActivityConfigurationChanging(); - assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO, - mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity), - /* delta= */ 0.001); + robot.checkCameraCompatAspectRatioEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testOnCameraOpened_portraitActivity_sandboxesDisplayRotationAndUpdatesApp() throws - Exception { - configureActivity(SCREEN_ORIENTATION_PORTRAIT); - setDisplayRotation(ROTATION_270); + public void testOnCameraOpened_portraitActivity_sandboxesDisplayRotationAndUpdatesApp() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT); + robot.activity().rotateDisplayForTopActivity(ROTATION_270); - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - // This is a portrait rotation for a device with portrait natural orientation (most common, - // currently the only one supported). - assertCompatibilityInfoSentWithDisplayRotation(ROTATION_0); + // This is a portrait rotation for a device with portrait natural orientation (most + // common, currently the only one supported). + robot.assertCompatibilityInfoSentWithDisplayRotation(ROTATION_0); + }); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testOnCameraOpened_landscapeActivity_sandboxesDisplayRotationAndUpdatesApp() throws - Exception { - configureActivity(SCREEN_ORIENTATION_LANDSCAPE); - setDisplayRotation(ROTATION_0); - - onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); - - // This is a landscape rotation for a device with portrait natural orientation (most common, - // currently the only one supported). - assertCompatibilityInfoSentWithDisplayRotation(ROTATION_90); - } - - private void setupAppCompatConfiguration() { - mAppCompatConfiguration = mDisplayContent.mWmService.mAppCompatConfiguration; - spyOn(mAppCompatConfiguration); - when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled()).thenReturn(true); - when(mAppCompatConfiguration.isCameraCompatTreatmentEnabledAtBuildTime()).thenReturn(true); - when(mAppCompatConfiguration.isCameraCompatRefreshEnabled()).thenReturn(true); - when(mAppCompatConfiguration.isCameraCompatSplitScreenAspectRatioEnabled()) - .thenReturn(false); - when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) - .thenReturn(true); - } - - private void setupCameraManager() { - final CameraManager mockCameraManager = mock(CameraManager.class); - doAnswer(invocation -> { - mCameraAvailabilityCallback = invocation.getArgument(1); - return null; - }).when(mockCameraManager).registerAvailabilityCallback( - any(Executor.class), any(CameraManager.AvailabilityCallback.class)); - - when(mContext.getSystemService(CameraManager.class)).thenReturn(mockCameraManager); - } - - private void setupHandler() { - final Handler handler = mDisplayContent.mWmService.mH; - spyOn(handler); - - when(handler.postDelayed(any(Runnable.class), anyLong())).thenAnswer( - invocation -> { - ((Runnable) invocation.getArgument(0)).run(); - return null; - }); - } - - private void configureActivity(@ScreenOrientation int activityOrientation) { - configureActivity(activityOrientation, WINDOWING_MODE_FREEFORM); - } - - private void configureActivity(@ScreenOrientation int activityOrientation, - @WindowingMode int windowingMode) { - configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT, windowingMode); - } - - private void configureActivityAndDisplay(@ScreenOrientation int activityOrientation, - @Orientation int naturalOrientation, @WindowingMode int windowingMode) { - setupDisplayContent(naturalOrientation); - final Task task = setupTask(windowingMode); - setupActivity(task, activityOrientation, windowingMode); - setupMockApplicationThread(); - - mCameraCompatFreeformPolicy = mDisplayContent.mAppCompatCameraPolicy - .mCameraCompatFreeformPolicy; - } - - private void setupDisplayContent(@Orientation int naturalOrientation) { - // Create a new DisplayContent so that the flag values create the camera freeform policy. - mDisplayContent = new TestDisplayContent.Builder(mAtm, mDisplayContent.getSurfaceWidth(), - mDisplayContent.getSurfaceHeight()).build(); - mDisplayContent.setIgnoreOrientationRequest(true); - setDisplayRotation(ROTATION_90); - doReturn(naturalOrientation).when(mDisplayContent).getNaturalOrientation(); - } - - private Task setupTask(@WindowingMode int windowingMode) { - final TaskDisplayArea tda = mDisplayContent.getDefaultTaskDisplayArea(); - spyOn(tda); - doReturn(true).when(tda).supportsNonResizableMultiWindow(); - - final Task task = new TaskBuilder(mSupervisor) - .setDisplay(mDisplayContent) - .setWindowingMode(windowingMode) - .build(); - task.setBounds(0, 0, 1000, 500); - return task; - } - - private void setupActivity(@NonNull Task task, @ScreenOrientation int activityOrientation, - @WindowingMode int windowingMode) { - mActivity = new ActivityBuilder(mAtm) - // Set the component to be that of the test class in order to enable compat changes - .setComponent(ComponentName.createRelative(mContext, - com.android.server.wm.CameraCompatFreeformPolicyTests.class.getName())) - .setScreenOrientation(activityOrientation) - .setResizeMode(RESIZE_MODE_RESIZEABLE) - .setCreateTask(true) - .setOnTop(true) - .setTask(task) - .build(); - mActivity.mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); - - spyOn(mActivity.mAppCompatController.getCameraOverrides()); - spyOn(mActivity.info); - - doReturn(mActivity).when(mDisplayContent).topRunningActivity(anyBoolean()); - doReturn(windowingMode == WINDOWING_MODE_FREEFORM).when(mActivity) - .inFreeformWindowingMode(); - } - - private void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) { - mCameraAvailabilityCallback.onCameraOpened(cameraId, packageName); - waitHandlerIdle(mDisplayContent.mWmService.mH); - } - - private void onCameraClosed(@NonNull String cameraId) { - mCameraAvailabilityCallback.onCameraClosed(cameraId); - waitHandlerIdle(mDisplayContent.mWmService.mH); - } - - private void assertInCameraCompatMode(@CameraCompatTaskInfo.FreeformCameraCompatMode int mode) { - assertEquals(mode, mCameraCompatFreeformPolicy.getCameraCompatMode(mActivity)); - } - - private void assertNotInCameraCompatMode() { - assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_NONE); - } - - private void assertActivityRefreshRequested(boolean refreshRequested) throws Exception { - assertActivityRefreshRequested(refreshRequested, /* cycleThroughStop*/ true); - } - - private void assertActivityRefreshRequested(boolean refreshRequested, - boolean cycleThroughStop) throws Exception { - verify(mActivity.mAppCompatController.getCameraOverrides(), - times(refreshRequested ? 1 : 0)).setIsRefreshRequested(true); - - final RefreshCallbackItem refreshCallbackItem = - new RefreshCallbackItem(mActivity.token, cycleThroughStop ? ON_STOP : ON_PAUSE); - final ResumeActivityItem resumeActivityItem = new ResumeActivityItem(mActivity.token, - /* isForward */ false, /* shouldSendCompatFakeFocus */ false); - - verify(mActivity.mAtmService.getLifecycleManager(), times(refreshRequested ? 1 : 0)) - .scheduleTransactionItems(mActivity.app.getThread(), - refreshCallbackItem, resumeActivityItem); - } - - private void callOnActivityConfigurationChanging(ActivityRecord activity) { - callOnActivityConfigurationChanging(activity, /* letterboxNew= */ true, - /* lastLetterbox= */false); - } - - private void callOnActivityConfigurationChanging(ActivityRecord activity, boolean letterboxNew, - boolean lastLetterbox) { - mDisplayContent.mAppCompatCameraPolicy.mActivityRefresher - .onActivityConfigurationChanging(activity, - /* newConfig */ createConfiguration(letterboxNew), - /* lastReportedConfig */ createConfiguration(lastLetterbox)); - } - - private Configuration createConfiguration(boolean letterbox) { - final Configuration configuration = new Configuration(); - Rect bounds = letterbox ? new Rect(/*left*/ 300, /*top*/ 0, /*right*/ 700, /*bottom*/ 600) - : new Rect(/*left*/ 0, /*top*/ 0, /*right*/ 1000, /*bottom*/ 600); - configuration.windowConfiguration.setAppBounds(bounds); - return configuration; - } - - private void setDisplayRotation(@Surface.Rotation int displayRotation) { - doAnswer(invocation -> { - DisplayInfo displayInfo = new DisplayInfo(); - mDisplayContent.getDisplay().getDisplayInfo(displayInfo); - displayInfo.rotation = displayRotation; - // Set height so that the natural orientation (rotation is 0) is portrait. This is the - // case for most standard phones and tablets. - // TODO(b/365725400): handle landscape natural orientation. - displayInfo.logicalHeight = displayRotation % 180 == 0 ? 800 : 600; - displayInfo.logicalWidth = displayRotation % 180 == 0 ? 600 : 800; - return displayInfo; - }).when(mDisplayContent.mWmService.mDisplayManagerInternal) - .getDisplayInfo(anyInt()); - } - - private void setupMockApplicationThread() { - IApplicationThread mockApplicationThread = mock(IApplicationThread.class); - spyOn(mActivity.app); - doReturn(mockApplicationThread).when(mActivity.app).getThread(); - } - - private void assertCompatibilityInfoSentWithDisplayRotation(@Surface.Rotation int - expectedRotation) throws Exception { - final ArgumentCaptor<CompatibilityInfo> compatibilityInfoArgumentCaptor = - ArgumentCaptor.forClass(CompatibilityInfo.class); - verify(mActivity.app.getThread()).updatePackageCompatibilityInfo(eq(mActivity.packageName), - compatibilityInfoArgumentCaptor.capture()); - - final CompatibilityInfo compatInfo = compatibilityInfoArgumentCaptor.getValue(); - assertTrue(compatInfo.isOverrideDisplayRotationRequired()); - assertEquals(expectedRotation, compatInfo.applicationDisplayRotation); + public void testOnCameraOpened_landscapeActivity_sandboxesDisplayRotationAndUpdatesApp() { + runTestScenario((robot) -> { + robot.configureActivity(SCREEN_ORIENTATION_LANDSCAPE); + robot.activity().rotateDisplayForTopActivity(ROTATION_0); + + robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + // This is a landscape rotation for a device with portrait natural orientation (most + // common, currently the only one supported). + robot.assertCompatibilityInfoSentWithDisplayRotation(ROTATION_90); + }); + } + + /** + * Runs a test scenario providing a Robot. + */ + void runTestScenario(@NonNull Consumer<CameraCompatFreeformPolicyRobotTests> consumer) { + final CameraCompatFreeformPolicyRobotTests robot = + new CameraCompatFreeformPolicyRobotTests(mWm, mAtm, mSupervisor, this); + consumer.accept(robot); + } + + private static class CameraCompatFreeformPolicyRobotTests extends AppCompatRobotBase { + private final WindowTestsBase mWindowTestsBase; + + private CameraManager.AvailabilityCallback mCameraAvailabilityCallback; + + CameraCompatFreeformPolicyRobotTests(@NonNull WindowManagerService wm, + @NonNull ActivityTaskManagerService atm, + @NonNull ActivityTaskSupervisor supervisor, + @NonNull WindowTestsBase windowTestsBase) { + super(wm, atm, supervisor); + mWindowTestsBase = windowTestsBase; + setupCameraManager(); + setupAppCompatConfiguration(); + } + + @Override + void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) { + super.onPostDisplayContentCreation(displayContent); + spyOn(displayContent.mAppCompatCameraPolicy); + if (displayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy != null) { + spyOn(displayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy); + } + } + + @Override + void onPostActivityCreation(@NonNull ActivityRecord activity) { + super.onPostActivityCreation(activity); + setupCameraManager(); + setupHandler(); + setupMockApplicationThread(); + } + + private void setupMockApplicationThread() { + IApplicationThread mockApplicationThread = mock(IApplicationThread.class); + spyOn(activity().top().app); + doReturn(mockApplicationThread).when(activity().top().app).getThread(); + } + + private Configuration createConfiguration(boolean letterbox, int rotation) { + final Configuration configuration = createConfiguration(letterbox); + configuration.windowConfiguration.setDisplayRotation(rotation); + return configuration; + } + + private Configuration createConfiguration(boolean letterbox) { + final Configuration configuration = new Configuration(); + Rect bounds = letterbox ? new Rect(/*left*/ 300, /*top*/ 0, /*right*/ 700, /*bottom*/ + 600) + : new Rect(/*left*/ 0, /*top*/ 0, /*right*/ 1000, /*bottom*/ 600); + configuration.windowConfiguration.setAppBounds(bounds); + return configuration; + } + + private void setupAppCompatConfiguration() { + applyOnConf((c) -> { + c.enableCameraCompatTreatment(true); + c.enableCameraCompatTreatmentAtBuildTime(true); + c.enableCameraCompatRefresh(true); + c.enableCameraCompatRefreshCycleThroughStop(true); + c.enableCameraCompatSplitScreenAspectRatio(false); + }); + } + + private void setupCameraManager() { + final CameraManager mockCameraManager = mock(CameraManager.class); + doAnswer(invocation -> { + mCameraAvailabilityCallback = invocation.getArgument(1); + return null; + }).when(mockCameraManager).registerAvailabilityCallback( + any(Executor.class), any(CameraManager.AvailabilityCallback.class)); + + doReturn(mockCameraManager).when(mWindowTestsBase.mWm.mContext).getSystemService( + CameraManager.class); + } + + private void setupHandler() { + final Handler handler = activity().top().mWmService.mH; + spyOn(handler); + + doAnswer(invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }).when(handler).postDelayed(any(Runnable.class), anyLong()); + } + + private void configureActivity(@ScreenOrientation int activityOrientation) { + configureActivity(activityOrientation, WINDOWING_MODE_FREEFORM); + } + + private void configureActivity(@ScreenOrientation int activityOrientation, + @WindowingMode int windowingMode) { + configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT, windowingMode); + } + + private void configureActivityAndDisplay(@ScreenOrientation int activityOrientation, + @Orientation int naturalOrientation, @WindowingMode int windowingMode) { + applyOnActivity(a -> { + dw().allowEnterDesktopMode(true); + a.createActivityWithComponentInNewTaskAndDisplay(); + a.setIgnoreOrientationRequest(true); + a.rotateDisplayForTopActivity(ROTATION_90); + a.configureTopActivity(/* minAspect */ -1, /* maxAspect */ -1, + activityOrientation, /* isUnresizable */ false); + a.top().setWindowingMode(windowingMode); + a.displayContent().setWindowingMode(windowingMode); + a.setDisplayNaturalOrientation(naturalOrientation); + spyOn(a.top().mAppCompatController.getCameraOverrides()); + spyOn(a.top().info); + doReturn(a.displayContent().getDisplayInfo()).when( + a.displayContent().mWmService.mDisplayManagerInternal).getDisplayInfo( + a.displayContent().mDisplayId); + }); + } + + private void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) { + mCameraAvailabilityCallback.onCameraOpened(cameraId, packageName); + waitHandlerIdle(); + } + + private void onCameraClosed(@NonNull String cameraId) { + mCameraAvailabilityCallback.onCameraClosed(cameraId); + } + + private void waitHandlerIdle() { + mWindowTestsBase.waitHandlerIdle(activity().displayContent().mWmService.mH); + } + + void setInFreeformWindowingMode(boolean inFreeform) { + doReturn(inFreeform).when(activity().top()).inFreeformWindowingMode(); + } + + void setShouldRefreshActivityViaPause(boolean enabled) { + doReturn(enabled).when(activity().top().mAppCompatController.getCameraOverrides()) + .shouldRefreshActivityViaPauseForCameraCompat(); + } + + void checkShouldRefreshActivity(boolean expected, Configuration newConfig, + Configuration oldConfig) { + assertEquals(expected, cameraCompatFreeformPolicy().shouldRefreshActivity( + activity().top(), newConfig, oldConfig)); + } + + void checkCameraCompatPolicyNotCreated() { + assertNull(cameraCompatFreeformPolicy()); + } + + void checkIsCameraRunningAndWindowingModeEligible(boolean expected) { + assertEquals(expected, cameraCompatFreeformPolicy() + .isCameraRunningAndWindowingModeEligible(activity().top())); + } + + void checkIsFreeformLetterboxingForCameraAllowed(boolean expected) { + assertEquals(expected, cameraCompatFreeformPolicy() + .isFreeformLetterboxingForCameraAllowed(activity().top())); + } + + void checkCameraCompatAspectRatioEquals(float aspectRatio) { + assertEquals(aspectRatio, + cameraCompatFreeformPolicy().getCameraCompatAspectRatio(activity().top()), + /* delta= */ 0.001); + } + + private void assertInCameraCompatMode(@FreeformCameraCompatMode int mode) { + assertEquals(mode, cameraCompatFreeformPolicy().getCameraCompatMode(activity().top())); + } + + private void assertNotInCameraCompatMode() { + assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_NONE); + } + + private void assertActivityRefreshRequested(boolean refreshRequested) { + assertActivityRefreshRequested(refreshRequested, /* cycleThroughStop*/ true); + } + + private void assertActivityRefreshRequested(boolean refreshRequested, + boolean cycleThroughStop) { + verify(activity().top().mAppCompatController.getCameraOverrides(), + times(refreshRequested ? 1 : 0)).setIsRefreshRequested(true); + + final RefreshCallbackItem refreshCallbackItem = + new RefreshCallbackItem(activity().top().token, + cycleThroughStop ? ON_STOP : ON_PAUSE); + final ResumeActivityItem resumeActivityItem = new ResumeActivityItem( + activity().top().token, + /* isForward */ false, /* shouldSendCompatFakeFocus */ false); + try { + verify(activity().top().mAtmService.getLifecycleManager(), + times(refreshRequested ? 1 : 0)) + .scheduleTransactionItems(activity().top().app.getThread(), + refreshCallbackItem, resumeActivityItem); + } catch (RemoteException e) { + fail(e.getMessage()); + } + } + + private void callOnActivityConfigurationChanging() { + callOnActivityConfigurationChanging(/* letterboxNew= */ true, + /* lastLetterbox= */false); + } + + private void callOnActivityConfigurationChanging(boolean letterboxNew, + boolean lastLetterbox) { + activity().displayContent().mAppCompatCameraPolicy.mActivityRefresher + .onActivityConfigurationChanging(activity().top(), + /* newConfig */ createConfiguration(letterboxNew), + /* lastReportedConfig */ createConfiguration(lastLetterbox)); + } + + void checkIsCameraCompatTreatmentActiveForTopActivity(boolean active) { + assertEquals(active, + cameraCompatFreeformPolicy().isTreatmentEnabledForActivity(activity().top(), + /* checkOrientation */ true)); + } + + void setOverrideMinAspectRatioEnabled(boolean enabled) { + doReturn(enabled).when(activity().top().mAppCompatController.getCameraOverrides()) + .isOverrideMinAspectRatioForCameraEnabled(); + } + + void assertCompatibilityInfoSentWithDisplayRotation(@Surface.Rotation int + expectedRotation) { + final ArgumentCaptor<CompatibilityInfo> compatibilityInfoArgumentCaptor = + ArgumentCaptor.forClass(CompatibilityInfo.class); + try { + verify(activity().top().app.getThread()).updatePackageCompatibilityInfo( + eq(activity().top().packageName), + compatibilityInfoArgumentCaptor.capture()); + } catch (RemoteException e) { + fail(e.getMessage()); + } + + final CompatibilityInfo compatInfo = compatibilityInfoArgumentCaptor.getValue(); + assertTrue(compatInfo.isOverrideDisplayRotationRequired()); + assertEquals(expectedRotation, compatInfo.applicationDisplayRotation); + } + + CameraCompatFreeformPolicy cameraCompatFreeformPolicy() { + return activity().displayContent().mAppCompatCameraPolicy.mCameraCompatFreeformPolicy; + } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java index bc37496d14a7..e87e107cd793 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java @@ -21,6 +21,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; 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.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_MEDIUM_VALUE; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_SMALL_VALUE; @@ -157,7 +158,7 @@ public class DesktopModeLaunchParamsModifierTests extends @Test @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, Flags.FLAG_DISABLE_DESKTOP_LAUNCH_PARAMS_OUTSIDE_DESKTOP_BUG_FIX}) - public void testReturnsContinueIfVisibleFreeformTaskExists() { + public void testReturnsContinueIfFreeformTaskExists() { setupDesktopModeLaunchParamsModifier(); when(mTarget.isEnteringDesktopMode(any(), any(), any())).thenCallRealMethod(); @@ -165,7 +166,7 @@ public class DesktopModeLaunchParamsModifierTests extends final Task existingFreeformTask = new TaskBuilder(mSupervisor).setCreateActivity(true) .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); doReturn(existingFreeformTask.getRootActivity()).when(dc) - .getTopMostVisibleFreeformActivity(); + .getTopMostFreeformActivity(); final Task launchingTask = new TaskBuilder(mSupervisor).build(); launchingTask.onDisplayChanged(dc); @@ -269,6 +270,38 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES}) + public void testInheritTaskBoundsFromExistingInstanceIfClosing() { + setupDesktopModeLaunchParamsModifier(); + + final String packageName = "com.same.package"; + // Setup existing task. + final DisplayContent dc = spy(createNewDisplay()); + final Task existingFreeformTask = new TaskBuilder(mSupervisor).setCreateActivity(true) + .setWindowingMode(WINDOWING_MODE_FREEFORM).setPackage(packageName).build(); + existingFreeformTask.setBounds( + /* left */ 0, + /* top */ 0, + /* right */ 500, + /* bottom */ 500); + doReturn(existingFreeformTask.getRootActivity()).when(dc) + .getTopMostVisibleFreeformActivity(); + // Set up new instance of already existing task. By default multi instance is not supported + // so first instance will close. + final Task launchingTask = new TaskBuilder(mSupervisor).setPackage(packageName) + .setCreateActivity(true).build(); + launchingTask.onDisplayChanged(dc); + launchingTask.intent.addFlags(FLAG_ACTIVITY_NEW_TASK); + + // New instance should inherit task bounds of old instance. + assertEquals(RESULT_DONE, + new CalculateRequestBuilder().setTask(launchingTask) + .setActivity(launchingTask.getRootActivity()).calculate()); + assertEquals(existingFreeformTask.getBounds(), mResult.mBounds); + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) public void testUsesDesiredBoundsIfEmptyLayoutAndActivityOptionsBounds() { diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java new file mode 100644 index 000000000000..1445a6982c60 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.wm; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.window.flags.Flags.FLAG_ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS; + +import static junit.framework.Assert.assertFalse; + +import static org.junit.Assert.assertTrue; + +import android.compat.testing.PlatformCompatChangeRule; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.Presubmit; +import android.view.DisplayInfo; + +import androidx.test.filters.MediumTest; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +/** + * Build/Install/Run: + * atest WmTests:DisplayCompatTests + */ +@MediumTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class DisplayCompatTests extends WindowTestsBase { + + @Rule + public TestRule compatChangeRule = new PlatformCompatChangeRule(); + + @EnableFlags(FLAG_ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS) + @Test + public void testFixedMiscConfigurationWhenMovingToDisplay() { + // Create an app on the default display, at which point the restart menu isn't enabled. + final Task task = createTask(mDefaultDisplay); + final ActivityRecord activity = createActivityRecord(task); + assertFalse(task.getTaskInfo().appCompatTaskInfo.isRestartMenuEnabledForDisplayMove()); + + // Move the app to a secondary display, and the restart menu must get enabled. + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.copyFrom(mDisplayInfo); + displayInfo.displayId = DEFAULT_DISPLAY + 1; + final DisplayContent secondaryDisplay = createNewDisplay(displayInfo); + task.reparent(secondaryDisplay.getDefaultTaskDisplayArea(), true); + assertTrue(task.getTaskInfo().appCompatTaskInfo.isRestartMenuEnabledForDisplayMove()); + + // Once the app gets restarted, the restart menu must be gone. + activity.restartProcessIfVisible(); + assertFalse(task.getTaskInfo().appCompatTaskInfo.isRestartMenuEnabledForDisplayMove()); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java index 71e34ef220d3..3c6a89842af9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java @@ -93,7 +93,7 @@ public class InsetsPolicyTest extends WindowTestsBase { final Task task1 = createTask(mDisplayContent); final Task task2 = createTask(mDisplayContent); - task1.setAdjacentTaskFragment(task2); + task1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(task1, task2)); final WindowState win = createAppWindow(task1, WINDOWING_MODE_MULTI_WINDOW, "app"); final InsetsSourceControl[] controls = addWindowAndGetControlsForDispatch(win); 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 33a48aadbd70..e2c4a1d2dfea 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -572,8 +572,8 @@ public class SizeCompatTests extends WindowTestsBase { new TestDisplayContent.Builder(mAtm, 1000, 2000).build(); final InputDevice device = new InputDevice.Builder() .setAssociatedDisplayId(newDisplay.mDisplayId) - .setSources(InputDevice.SOURCE_TOUCHSCREEN | InputDevice.SOURCE_TRACKBALL - | InputDevice.KEYBOARD_TYPE_ALPHABETIC) + .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC) + .setSources(InputDevice.SOURCE_TOUCHSCREEN | InputDevice.SOURCE_TRACKBALL) .build(); final InputDevice[] devices = {device}; doReturn(true).when(newDisplay.mWmService.mInputManager) @@ -596,6 +596,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(originalTouchscreen, newConfiguration.touchscreen); assertEquals(originalNavigation, newConfiguration.navigation); assertEquals(originalKeyboard, newConfiguration.keyboard); + // TODO(b/399749909): assert keyboardHidden, hardkeyboardHidden, and navigationHidden too. } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java index 3776b03695d5..b558fad84efa 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java +++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java @@ -78,6 +78,7 @@ import android.view.SurfaceControl; import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.internal.os.BackgroundThread; +import com.android.internal.protolog.PerfettoProtoLogImpl; import com.android.internal.protolog.ProtoLog; import com.android.internal.protolog.WmProtoLogGroups; import com.android.server.AnimationThread; @@ -187,7 +188,10 @@ public class SystemServicesTestRule implements TestRule { } private void setUp() { - ProtoLog.init(WmProtoLogGroups.values()); + if (ProtoLog.getSingleInstance() == null) { + ProtoLog.init(WmProtoLogGroups.values()); + PerfettoProtoLogImpl.waitForInitialization(); + } if (mOnBeforeServicesCreated != null) { mOnBeforeServicesCreated.run(); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java index 986532ce5897..ec83c50e95aa 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java @@ -87,7 +87,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase { mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD); adjacentRootTask.mCreatedByOrganizer = true; final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea(); - adjacentRootTask.setAdjacentTaskFragment(rootTask); + adjacentRootTask.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(adjacentRootTask, rootTask)); taskDisplayArea.setLaunchAdjacentFlagRootTask(adjacentRootTask); Task actualRootTask = taskDisplayArea.getLaunchRootTask( @@ -113,7 +114,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase { final Task adjacentRootTask = createTask( mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD); adjacentRootTask.mCreatedByOrganizer = true; - adjacentRootTask.setAdjacentTaskFragment(rootTask); + adjacentRootTask.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(adjacentRootTask, rootTask)); taskDisplayArea.setLaunchRootTask(rootTask, new int[]{WINDOWING_MODE_MULTI_WINDOW}, new int[]{ACTIVITY_TYPE_STANDARD}); @@ -135,7 +137,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase { adjacentRootTask.mCreatedByOrganizer = true; createActivityRecord(adjacentRootTask); final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea(); - adjacentRootTask.setAdjacentTaskFragment(rootTask); + adjacentRootTask.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(adjacentRootTask, rootTask)); taskDisplayArea.setLaunchAdjacentFlagRootTask(adjacentRootTask); final Task actualRootTask = taskDisplayArea.getLaunchRootTask( @@ -821,7 +824,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase { adjacentRootTask.mCreatedByOrganizer = true; final Task candidateTask = createTaskInRootTask(rootTask, 0 /* userId*/); final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea(); - adjacentRootTask.setAdjacentTaskFragment(rootTask); + adjacentRootTask.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(adjacentRootTask, rootTask)); // Verify the launch root with candidate task Task actualRootTask = taskDisplayArea.getLaunchRootTask(WINDOWING_MODE_UNDEFINED, diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index ab76ae8e378a..76660bdc7355 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -784,7 +784,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { .setFragmentToken(fragmentToken2) .build(); mWindowOrganizerController.mLaunchTaskFragments.put(fragmentToken2, taskFragment2); - mTaskFragment.setAdjacentTaskFragment(taskFragment2); + mTaskFragment.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(mTaskFragment, taskFragment2)); mTransaction.clearAdjacentTaskFragments(mFragmentToken); mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, @@ -1267,7 +1268,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @Test - public void testTaskFragmentInPip_setAdjacentTaskFragment() { + public void testTaskFragmentInPip_setAdjacentTaskFragments() { setupTaskFragmentInPip(); spyOn(mWindowOrganizerController); @@ -1279,7 +1280,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), eq(mErrorToken), eq(mTaskFragment), eq(OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS), any(IllegalArgumentException.class)); - verify(mTaskFragment, never()).setAdjacentTaskFragment(any()); + verify(mTaskFragment, never()).setAdjacentTaskFragments(any()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index cc2a76dcc9f2..7c1d7fec819b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -67,7 +67,6 @@ import android.content.res.Configuration; import android.graphics.Color; import android.graphics.Rect; import android.os.Binder; -import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.view.SurfaceControl; import android.view.View; @@ -363,7 +362,7 @@ public class TaskFragmentTest extends WindowTestsBase { doReturn(true).when(primaryActivity).supportsPictureInPicture(); doReturn(false).when(secondaryActivity).supportsPictureInPicture(); - primaryTf.setAdjacentTaskFragment(secondaryTf); + primaryTf.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(primaryTf, secondaryTf)); primaryActivity.setState(RESUMED, "test"); secondaryActivity.setState(RESUMED, "test"); @@ -390,7 +389,8 @@ public class TaskFragmentTest extends WindowTestsBase { task.setWindowingMode(WINDOWING_MODE_FULLSCREEN); taskFragment0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); taskFragment0.setBounds(taskFragmentBounds); - taskFragment0.setAdjacentTaskFragment(taskFragment1); + taskFragment0.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(taskFragment0, taskFragment1)); taskFragment0.setCompanionTaskFragment(taskFragment1); taskFragment0.setAnimationParams(new TaskFragmentAnimationParams.Builder() .setAnimationBackgroundColor(Color.GREEN) @@ -779,7 +779,7 @@ public class TaskFragmentTest extends WindowTestsBase { .setOrganizer(mOrganizer) .setFragmentToken(new Binder()) .build(); - tf0.setAdjacentTaskFragment(tf1); + tf0.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf0, tf1)); tf0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); tf1.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); task.setBounds(0, 0, 1200, 1000); @@ -834,7 +834,7 @@ public class TaskFragmentTest extends WindowTestsBase { final Task task = createTask(mDisplayContent); final TaskFragment tf0 = createTaskFragmentWithActivity(task); final TaskFragment tf1 = createTaskFragmentWithActivity(task); - tf0.setAdjacentTaskFragment(tf1); + tf0.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf0, tf1)); tf0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); tf1.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); task.setBounds(0, 0, 1200, 1000); @@ -982,7 +982,8 @@ public class TaskFragmentTest extends WindowTestsBase { .setOrganizer(mOrganizer) .setFragmentToken(new Binder()) .build(); - taskFragmentLeft.setAdjacentTaskFragment(taskFragmentRight); + taskFragmentLeft.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(taskFragmentLeft, taskFragmentRight)); taskFragmentLeft.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); taskFragmentRight.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); task.setBounds(0, 0, 1200, 1000); @@ -1051,8 +1052,8 @@ public class TaskFragmentTest extends WindowTestsBase { .setParentTask(task) .createActivityCount(1) .build(); - taskFragmentRight.setAdjacentTaskFragment(taskFragmentLeft); - taskFragmentLeft.setAdjacentTaskFragment(taskFragmentRight); + taskFragmentRight.setAdjacentTaskFragments( + new TaskFragment.AdjacentSet(taskFragmentLeft, taskFragmentRight)); final ActivityRecord appLeftTop = taskFragmentLeft.getTopMostActivity(); final ActivityRecord appRightTop = taskFragmentRight.getTopMostActivity(); @@ -1103,7 +1104,6 @@ public class TaskFragmentTest extends WindowTestsBase { Math.min(outConfig.screenWidthDp, outConfig.screenHeightDp)); } - @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS) @Test public void testAdjacentSetForTaskFragments() { final Task task = createTask(mDisplayContent); @@ -1119,7 +1119,6 @@ public class TaskFragmentTest extends WindowTestsBase { () -> new TaskFragment.AdjacentSet(tf0, tf1, tf2)); } - @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS) @Test public void testSetAdjacentTaskFragments() { final Task task0 = createTask(mDisplayContent); @@ -1148,7 +1147,6 @@ public class TaskFragmentTest extends WindowTestsBase { assertFalse(task2.hasAdjacentTaskFragment()); } - @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS) @Test public void testClearAdjacentTaskFragments() { final Task task0 = createTask(mDisplayContent); @@ -1167,7 +1165,6 @@ public class TaskFragmentTest extends WindowTestsBase { assertFalse(task2.hasAdjacentTaskFragment()); } - @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS) @Test public void testRemoveFromAdjacentTaskFragments() { final Task task0 = createTask(mDisplayContent); @@ -1190,7 +1187,6 @@ public class TaskFragmentTest extends WindowTestsBase { assertFalse(task1.isAdjacentTo(task1)); } - @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS) @Test public void testRemoveFromAdjacentTaskFragmentsWhenRemove() { final Task task0 = createTask(mDisplayContent); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java index 51ea498811fc..f22ecb5eb3f9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java @@ -68,11 +68,8 @@ public class TaskSnapshotLowResDisabledTest extends TaskSnapshotPersisterTestBas public void testPersistAndLoadSnapshot() { mPersister.persistSnapshot(1, mTestUserId, createSnapshot()); mSnapshotPersistQueue.waitForQueueEmpty(); - final File[] files = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/1.proto"), - new File(FILES_DIR.getPath() + "/snapshots/1.jpg")}; - final File[] nonExistsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg")}; + final File[] files = convertFilePath("1.proto", "1.jpg"); + final File[] nonExistsFiles = convertFilePath("1_reduced.proto"); assertTrueForFiles(files, File::exists, " must exist"); assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist"); final TaskSnapshot snapshot = mLoader.loadTask(1, mTestUserId, false /* isLowResolution */); @@ -92,14 +89,9 @@ public class TaskSnapshotLowResDisabledTest extends TaskSnapshotPersisterTestBas taskIds.add(1); mPersister.removeObsoleteFiles(taskIds, new int[]{mTestUserId}); mSnapshotPersistQueue.waitForQueueEmpty(); - final File[] existsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/1.proto"), - new File(FILES_DIR.getPath() + "/snapshots/1.jpg")}; - final File[] nonExistsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/2.proto"), - new File(FILES_DIR.getPath() + "/snapshots/2.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/2_reduced.jpg")}; + final File[] existsFiles = convertFilePath("1.proto", "1.jpg"); + final File[] nonExistsFiles = convertFilePath("1_reduced.proto", "2.proto", "2.jpg", + "2_reduced.jpg"); assertTrueForFiles(existsFiles, File::exists, " must exist"); assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist"); } @@ -112,14 +104,8 @@ public class TaskSnapshotLowResDisabledTest extends TaskSnapshotPersisterTestBas mPersister.removeObsoleteFiles(taskIds, new int[]{mTestUserId}); mPersister.persistSnapshot(2, mTestUserId, createSnapshot()); mSnapshotPersistQueue.waitForQueueEmpty(); - final File[] existsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/1.proto"), - new File(FILES_DIR.getPath() + "/snapshots/1.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/2.proto"), - new File(FILES_DIR.getPath() + "/snapshots/2.jpg")}; - final File[] nonExistsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/2_reduced.jpg")}; + final File[] existsFiles = convertFilePath("1.proto", "1.jpg", "2.proto", "2.jpg"); + final File[] nonExistsFiles = convertFilePath("1_reduced.jpg", "2_reduced.jpg"); assertTrueForFiles(existsFiles, File::exists, " must exist"); assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist"); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java index 4b54e4464ca7..af06c14516a1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterLoaderTest.java @@ -75,9 +75,7 @@ public class TaskSnapshotPersisterLoaderTest extends TaskSnapshotPersisterTestBa public void testPersistAndLoadSnapshot() { mPersister.persistSnapshot(1, mTestUserId, createSnapshot()); mSnapshotPersistQueue.waitForQueueEmpty(); - final File[] files = new File[]{new File(FILES_DIR.getPath() + "/snapshots/1.proto"), - new File(FILES_DIR.getPath() + "/snapshots/1.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg")}; + final File[] files = convertFilePath("1.proto", "1.jpg", "1_reduced.jpg"); assertTrueForFiles(files, File::exists, " must exist"); final TaskSnapshot snapshot = mLoader.loadTask(1, mTestUserId, false /* isLowResolution */); assertNotNull(snapshot); @@ -140,13 +138,8 @@ public class TaskSnapshotPersisterLoaderTest extends TaskSnapshotPersisterTestBa mSnapshotPersistQueue.waitForQueueEmpty(); // Make sure 1,2 were purged but removeObsoleteFiles wasn't. - final File[] existsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/3.proto"), - new File(FILES_DIR.getPath() + "/snapshots/4.proto")}; - final File[] nonExistsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/100.proto"), - new File(FILES_DIR.getPath() + "/snapshots/1.proto"), - new File(FILES_DIR.getPath() + "/snapshots/1.proto")}; + final File[] existsFiles = convertFilePath("3.proto", "4.proto"); + final File[] nonExistsFiles = convertFilePath("100.proto", "1.proto", "2.proto"); assertTrueForFiles(existsFiles, File::exists, " must exist"); assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist"); } @@ -427,14 +420,8 @@ public class TaskSnapshotPersisterLoaderTest extends TaskSnapshotPersisterTestBa taskIds.add(1); mPersister.removeObsoleteFiles(taskIds, new int[]{mTestUserId}); mSnapshotPersistQueue.waitForQueueEmpty(); - final File[] existsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/1.proto"), - new File(FILES_DIR.getPath() + "/snapshots/1.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg")}; - final File[] nonExistsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/2.proto"), - new File(FILES_DIR.getPath() + "/snapshots/2.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/2_reduced.jpg")}; + final File[] existsFiles = convertFilePath("1.proto", "1.jpg", "1_reduced.jpg"); + final File[] nonExistsFiles = convertFilePath("2.proto", "2.jpg", "2_reduced.jpg"); assertTrueForFiles(existsFiles, File::exists, " must exist"); assertTrueForFiles(nonExistsFiles, file -> !file.exists(), " must not exist"); } @@ -447,13 +434,8 @@ public class TaskSnapshotPersisterLoaderTest extends TaskSnapshotPersisterTestBa mPersister.removeObsoleteFiles(taskIds, new int[]{mTestUserId}); mPersister.persistSnapshot(2, mTestUserId, createSnapshot()); mSnapshotPersistQueue.waitForQueueEmpty(); - final File[] existsFiles = new File[]{ - new File(FILES_DIR.getPath() + "/snapshots/1.proto"), - new File(FILES_DIR.getPath() + "/snapshots/1.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/1_reduced.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/2.proto"), - new File(FILES_DIR.getPath() + "/snapshots/2.jpg"), - new File(FILES_DIR.getPath() + "/snapshots/2_reduced.jpg")}; + final File[] existsFiles = convertFilePath("1.proto", "1.jpg", "1_reduced.jpg", "2.proto", + "2.jpg", "2_reduced.jpg"); assertTrueForFiles(existsFiles, File::exists, " must exist"); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java index 1e16c97de647..b2c195e8ebaa 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; +import android.annotation.NonNull; import android.content.ComponentName; import android.content.ContextWrapper; import android.content.res.Resources; @@ -46,6 +47,7 @@ import android.window.TaskSnapshot; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider; +import com.android.window.flags.Flags; import org.junit.After; import org.junit.AfterClass; @@ -129,12 +131,33 @@ class TaskSnapshotPersisterTestBase extends WindowTestsBase { return; } for (File file : files) { - if (!file.isDirectory()) { - file.delete(); + if (file.isDirectory()) { + final File[] subFiles = file.listFiles(); + if (subFiles == null) { + continue; + } + for (File subFile : subFiles) { + subFile.delete(); + } } + file.delete(); } } + File[] convertFilePath(@NonNull String... fileNames) { + final File[] files = new File[fileNames.length]; + final String path; + if (Flags.scrambleSnapshotFileName()) { + path = mPersister.mPersistInfoProvider.getDirectory(mTestUserId).getPath(); + } else { + path = FILES_DIR.getPath() + "/snapshots/"; + } + for (int i = 0; i < fileNames.length; i++) { + files[i] = new File(path + fileNames[i]); + } + return files; + } + TaskSnapshot createSnapshot() { return new TaskSnapshotBuilder().setTopActivityComponent(getUniqueComponentName()).build(); } 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 724d7e7c111c..044aacc4b988 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -1750,8 +1750,7 @@ public class TaskTests extends WindowTestsBase { primary.mVisibleRequested = true; secondary.mVisibleRequested = true; - primary.setAdjacentTaskFragment(secondary); - secondary.setAdjacentTaskFragment(primary); + primary.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(primary, secondary)); primary.setEmbeddedDimArea(EMBEDDED_DIM_AREA_PARENT_TASK); doReturn(true).when(primary).shouldBoostDimmer(); task.assignChildLayers(t); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index 7030d986494f..ae6144713a1d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -929,7 +929,6 @@ public class WindowOrganizerTests extends WindowTestsBase { assertEquals(dc.getDefaultTaskDisplayArea().mLaunchAdjacentFlagRootTask, null); } - @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS) @Test public void testSetAdjacentLaunchRootSet() { final DisplayContent dc = mWm.mRoot.getDisplayContent(Display.DEFAULT_DISPLAY); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index 57ab13ffee89..471b065aebb4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -122,7 +122,6 @@ import android.window.WindowContainerTransaction; import com.android.internal.policy.AttributeCache; import com.android.internal.util.ArrayUtils; import com.android.internal.util.test.FakeSettingsProvider; -import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils; import com.android.server.wm.DisplayWindowSettings.SettingsProvider.SettingsEntry; import org.junit.After; @@ -289,18 +288,6 @@ public class WindowTestsBase extends SystemServiceTestsBase { mAtm.mWindowManager.mAppCompatConfiguration .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(false); - // Setup WallpaperController crop utils with a simple center-align strategy - WallpaperCropUtils cropUtils = (displaySize, bitmapSize, suggestedCrops, rtl) -> { - Rect crop = new Rect(0, 0, displaySize.x, displaySize.y); - crop.scale(Math.min( - ((float) bitmapSize.x) / displaySize.x, - ((float) bitmapSize.y) / displaySize.y)); - crop.offset((bitmapSize.x - crop.width()) / 2, (bitmapSize.y - crop.height()) / 2); - return crop; - }; - mDisplayContent.mWallpaperController.setWallpaperCropUtils(cropUtils); - mDefaultDisplay.mWallpaperController.setWallpaperCropUtils(cropUtils); - checkDeviceSpecificOverridesNotApplied(); } @@ -1890,7 +1877,7 @@ public class WindowTestsBase extends SystemServiceTestsBase { mSecondary = mService.mTaskOrganizerController.createRootTask( display, WINDOWING_MODE_MULTI_WINDOW, null); - mPrimary.setAdjacentTaskFragment(mSecondary); + mPrimary.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(mPrimary, mSecondary)); display.getDefaultTaskDisplayArea().setLaunchAdjacentFlagRootTask(mSecondary); final Rect primaryBounds = new Rect(); diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index 89b98509de34..a727df7a7efb 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -43,6 +43,8 @@ import android.media.AudioManagerInternal; import android.media.permission.Identity; import android.os.Binder; import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; import android.os.IRemoteCallback; import android.os.ParcelFileDescriptor; @@ -837,6 +839,13 @@ final class HotwordDetectionConnection { private final int mBindingFlags; private final int mInstanceNumber; + private static final HandlerThread mHandler; + + static { + mHandler = new HandlerThread("Sandbox detection connector"); + mHandler.start(); + } + private boolean mRespectServiceConnectionStatusChanged = true; private boolean mIsBound = false; private boolean mIsLoggedFirstConnect = false; @@ -883,6 +892,11 @@ final class HotwordDetectionConnection { } } + @Override // from ServiceConnector.Impl + protected Handler getJobHandler() { + return mHandler.getThreadHandler(); + } + @Override protected long getAutoDisconnectTimeoutMs() { return -1; @@ -1151,14 +1165,12 @@ final class HotwordDetectionConnection { } private void updateServiceIdentity(ServiceConnection connection) { - connection.run(service -> service.ping(new IRemoteCallback.Stub() { + connection.run(service -> service.ping(new ISandboxedDetectionService.IPingMe.Stub() { @Override - public void sendResult(Bundle bundle) throws RemoteException { + public void onPing() throws RemoteException { // TODO: Exit if the service has been unbound already (though there's a very low // chance this happens). - if (DEBUG) { - Slog.d(TAG, "updating hotword UID " + Binder.getCallingUid()); - } + Slog.d(TAG, "updating hotword UID " + Binder.getCallingUid()); // TODO: Have the provider point to the current state stored in // VoiceInteractionManagerServiceImpl. final int uid = Binder.getCallingUid(); diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java index 29d394201f39..ebe00782319a 100644 --- a/telecomm/java/android/telecom/Connection.java +++ b/telecomm/java/android/telecom/Connection.java @@ -1237,6 +1237,10 @@ public abstract class Connection extends Conferenceable { builder.append(isLong ? " PROPERTY_IS_DOWNGRADED_CONFERENCE" : " dngrd_conf"); } + if ((properties & PROPERTY_CROSS_SIM) == PROPERTY_CROSS_SIM) { + builder.append(isLong ? " PROPERTY_CROSS_SIM" : " xsim"); + } + builder.append("]"); return builder.toString(); } diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index fbba999bfb36..14d567d141cb 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -3371,14 +3371,13 @@ public class TelephonyManager { return telephony.getDataNetworkTypeForSubscriber(subId, getOpPackageName(), getAttributionTag()); } else { - // This can happen when the ITelephony interface is not up yet. + Log.e(TAG, "getDataNetworkType: ITelephony interface is not up yet"); return NETWORK_TYPE_UNKNOWN; } - } catch(RemoteException ex) { - // This shouldn't happen in the normal case - return NETWORK_TYPE_UNKNOWN; - } catch (NullPointerException ex) { - // This could happen before phone restarts due to crashing + } catch (RemoteException // Shouldn't happen in the normal case + | NullPointerException ex // Could happen before phone restarts due to crashing + ) { + Log.e(TAG, "getDataNetworkType: " + ex.getMessage()); return NETWORK_TYPE_UNKNOWN; } } diff --git a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml index 685ae9a5fef2..c5e7188e6efe 100644 --- a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml +++ b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml @@ -47,6 +47,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml index 5f92d7fe830b..22bd458e2751 100644 --- a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml +++ b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml @@ -47,6 +47,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml index 1b90e99a8ba2..541ce26d1435 100644 --- a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml +++ b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml @@ -47,6 +47,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml index ffdbb02984a7..d2e02193f0fb 100644 --- a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml +++ b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml @@ -47,6 +47,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/FlickerTests/IME/AndroidTestTemplate.xml b/tests/FlickerTests/IME/AndroidTestTemplate.xml index ac704e5e7c39..e112c82f0661 100644 --- a/tests/FlickerTests/IME/AndroidTestTemplate.xml +++ b/tests/FlickerTests/IME/AndroidTestTemplate.xml @@ -49,6 +49,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/FlickerTests/Notification/AndroidTestTemplate.xml b/tests/FlickerTests/Notification/AndroidTestTemplate.xml index e2ac5a9579ae..e5700c03cf77 100644 --- a/tests/FlickerTests/Notification/AndroidTestTemplate.xml +++ b/tests/FlickerTests/Notification/AndroidTestTemplate.xml @@ -47,6 +47,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml index 1a4feb6e9eca..4c41a4c01180 100644 --- a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml +++ b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml @@ -47,6 +47,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml index 1b2007deae27..27fc249e36b9 100644 --- a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml +++ b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml @@ -47,6 +47,8 @@ <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> <!-- Disable AOD --> <option name="run-command" value="settings put secure doze_always_on 0"/> + <!-- Disable explore hub mode --> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt index e99c81493394..794fd0255726 100644 --- a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt +++ b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt @@ -214,9 +214,5 @@ class KeyGestureEventHandlerTest { ): Boolean { return handler(event, focusedToken) } - - override fun isKeyGestureSupported(gestureType: Int): Boolean { - return true - } } } diff --git a/tests/Input/src/com/android/server/input/BatteryControllerTests.kt b/tests/Input/src/com/android/server/input/BatteryControllerTests.kt index 044f11d6904c..890c346ea015 100644 --- a/tests/Input/src/com/android/server/input/BatteryControllerTests.kt +++ b/tests/Input/src/com/android/server/input/BatteryControllerTests.kt @@ -184,6 +184,8 @@ class BatteryControllerTests { @get:Rule val rule = MockitoJUnit.rule()!! @get:Rule + val context = TestableContext(ApplicationProvider.getApplicationContext()) + @get:Rule val inputManagerRule = MockInputManagerRule() @Mock @@ -194,7 +196,6 @@ class BatteryControllerTests { private lateinit var bluetoothBatteryManager: BluetoothBatteryManager private lateinit var batteryController: BatteryController - private lateinit var context: TestableContext private lateinit var testLooper: TestLooper private lateinit var devicesChangedListener: IInputDevicesChangedListener private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession @@ -202,7 +203,6 @@ class BatteryControllerTests { @Before fun setup() { - context = TestableContext(ApplicationProvider.getApplicationContext()) testLooper = TestLooper() val inputManager = InputManager(context) context.addMockSystemService(InputManager::class.java, inputManager) diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index 2799f6c79215..4f1fb6487b19 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -32,6 +32,7 @@ import android.hardware.input.InputGestureData import android.hardware.input.InputManager import android.hardware.input.InputManagerGlobal import android.hardware.input.KeyGestureEvent +import android.os.Handler import android.os.IBinder import android.os.Process import android.os.SystemClock @@ -48,9 +49,11 @@ import android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE import androidx.test.core.app.ApplicationProvider import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.internal.R +import com.android.internal.accessibility.AccessibilityShortcutController import com.android.internal.annotations.Keep import com.android.internal.util.FrameworkStatsLog import com.android.modules.utils.testing.ExtendedMockitoRule +import com.android.server.input.InputManagerService.WindowManagerCallbacks import java.io.File import java.io.FileOutputStream import java.io.InputStream @@ -67,6 +70,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito +import org.mockito.kotlin.never +import org.mockito.kotlin.times /** * Tests for {@link KeyGestureController}. @@ -102,6 +107,7 @@ class KeyGestureControllerTests { const val SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0 const val SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL = 1 const val SETTINGS_KEY_BEHAVIOR_NOTHING = 2 + const val TEST_PID = 10 } @JvmField @@ -116,11 +122,10 @@ class KeyGestureControllerTests { @Rule val rule = SetFlagsRule() - @Mock - private lateinit var iInputManager: IInputManager - - @Mock - private lateinit var packageManager: PackageManager + @Mock private lateinit var iInputManager: IInputManager + @Mock private lateinit var packageManager: PackageManager + @Mock private lateinit var wmCallbacks: WindowManagerCallbacks + @Mock private lateinit var accessibilityShortcutController: AccessibilityShortcutController private var currentPid = 0 private lateinit var context: Context @@ -207,8 +212,34 @@ class KeyGestureControllerTests { private fun setupKeyGestureController() { keyGestureController = - KeyGestureController(context, testLooper.looper, testLooper.looper, inputDataStore) - Mockito.`when`(iInputManager.getAppLaunchBookmarks()) + KeyGestureController( + context, + testLooper.looper, + testLooper.looper, + inputDataStore, + object : KeyGestureController.Injector() { + override fun getAccessibilityShortcutController( + context: Context?, + handler: Handler? + ): AccessibilityShortcutController { + return accessibilityShortcutController + } + }) + Mockito.`when`(iInputManager.registerKeyGestureHandler(Mockito.any())) + .thenAnswer { + val args = it.arguments + if (args[0] != null) { + keyGestureController.registerKeyGestureHandler( + args[0] as IKeyGestureHandler, + TEST_PID + ) + } + } + keyGestureController.setWindowManagerCallbacks(wmCallbacks) + Mockito.`when`(wmCallbacks.isKeyguardLocked(Mockito.anyInt())).thenReturn(false) + Mockito.`when`(accessibilityShortcutController + .isAccessibilityShortcutAvailable(Mockito.anyBoolean())).thenReturn(true) + Mockito.`when`(iInputManager.appLaunchBookmarks) .thenReturn(keyGestureController.appLaunchBookmarks) keyGestureController.systemRunning() testLooper.dispatchAll() @@ -1270,9 +1301,9 @@ class KeyGestureControllerTests { ) ), TestData( - "BACK + DPAD_DOWN -> TV Accessibility Chord", + "BACK + DPAD_DOWN -> Accessibility Chord(for TV)", intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN), - KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD, + KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD, intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN), 0, intArrayOf( @@ -1622,6 +1653,52 @@ class KeyGestureControllerTests { ) } + @Test + fun testAccessibilityShortcutChordPressed() { + setupKeyGestureController() + + sendKeys( + intArrayOf(KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN), + // Assuming this value is always greater than the accessibility shortcut timeout, which + // currently defaults to 3000ms + timeDelayMs = 10000 + ) + Mockito.verify(accessibilityShortcutController, times(1)).performAccessibilityShortcut() + } + + @Test + fun testAccessibilityTvShortcutChordPressed() { + setupKeyGestureController() + + sendKeys( + intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN), + timeDelayMs = 10000 + ) + Mockito.verify(accessibilityShortcutController, times(1)).performAccessibilityShortcut() + } + + @Test + fun testAccessibilityShortcutChordPressedForLessThanTimeout() { + setupKeyGestureController() + + sendKeys( + intArrayOf(KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN), + timeDelayMs = 0 + ) + Mockito.verify(accessibilityShortcutController, never()).performAccessibilityShortcut() + } + + @Test + fun testAccessibilityTvShortcutChordPressedForLessThanTimeout() { + setupKeyGestureController() + + sendKeys( + intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN), + timeDelayMs = 0 + ) + Mockito.verify(accessibilityShortcutController, never()).performAccessibilityShortcut() + } + private fun testKeyGestureInternal(test: TestData) { val handledEvents = mutableListOf<KeyGestureEvent>() val handler = KeyGestureHandler { event, _ -> @@ -1683,7 +1760,11 @@ class KeyGestureControllerTests { assertEquals("Test: $testName should not produce Key gesture", 0, handledEvents.size) } - private fun sendKeys(testKeys: IntArray, assertNotSentToApps: Boolean = false) { + private fun sendKeys( + testKeys: IntArray, + assertNotSentToApps: Boolean = false, + timeDelayMs: Long = 0 + ) { var metaState = 0 val now = SystemClock.uptimeMillis() for (key in testKeys) { @@ -1699,6 +1780,11 @@ class KeyGestureControllerTests { testLooper.dispatchAll() } + if (timeDelayMs > 0) { + testLooper.moveTimeForward(timeDelayMs) + testLooper.dispatchAll() + } + for (key in testKeys.reversed()) { val upEvent = KeyEvent( now, now, KeyEvent.ACTION_UP, key, 0 /*repeat*/, metaState, @@ -1742,9 +1828,5 @@ class KeyGestureControllerTests { override fun handleKeyGesture(event: AidlKeyGestureEvent, token: IBinder?): Boolean { return handler(event, token) } - - override fun isKeyGestureSupported(gestureType: Int): Boolean { - return true - } } } diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java index 5875520cd259..20528f23cc8c 100644 --- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java +++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java @@ -23,7 +23,6 @@ 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.Rect; import android.hardware.input.InputManager; import android.testing.AndroidTestingRunner; @@ -60,9 +59,12 @@ public class TouchpadDebugViewControllerTests { private static final String TAG = "TouchpadDebugViewController"; @Rule + public final TestableContext mTestableContext = + new TestableContext(InstrumentationRegistry.getInstrumentation().getContext()); + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); - private Context mContext; private TouchpadDebugViewController mTouchpadDebugViewController; @Mock private InputManager mInputManagerMock; @@ -74,8 +76,6 @@ public class TouchpadDebugViewControllerTests { @Before public void setup() throws Exception { - mContext = InstrumentationRegistry.getInstrumentation().getContext(); - TestableContext mTestableContext = new TestableContext(mContext); mTestableContext.addMockSystemService(WindowManager.class, mWindowManagerMock); Rect bounds = new Rect(0, 0, 2560, 1600); diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java index 60fa52f85e34..1c366a134300 100644 --- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java +++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java @@ -26,7 +26,6 @@ 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.Color; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; @@ -51,6 +50,7 @@ import com.android.server.input.TouchpadHardwareProperties; import com.android.server.input.TouchpadHardwareState; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -70,6 +70,10 @@ public class TouchpadDebugViewTest { private TouchpadDebugView mTouchpadDebugView; private WindowManager.LayoutParams mWindowLayoutParams; + @Rule + public final TestableContext mTestableContext = + new TestableContext(InstrumentationRegistry.getInstrumentation().getContext()); + @Mock WindowManager mWindowManager; @Mock @@ -77,14 +81,10 @@ public class TouchpadDebugViewTest { Rect mWindowBounds; WindowMetrics mWindowMetrics; - TestableContext mTestableContext; @Before public void setUp() { MockitoAnnotations.initMocks(this); - Context context = InstrumentationRegistry.getInstrumentation().getContext(); - mTestableContext = new TestableContext(context); - mTestableContext.addMockSystemService(WindowManager.class, mWindowManager); mTestableContext.addMockSystemService(InputManager.class, mInputManager); diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java index 4d379e45a81a..bb54a26036db 100644 --- a/tests/utils/testutils/java/android/os/test/TestLooper.java +++ b/tests/utils/testutils/java/android/os/test/TestLooper.java @@ -68,16 +68,7 @@ public class TestLooper { * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection. */ private static boolean isAtLeastBaklava() { - Method[] methods = TestLooperManager.class.getMethods(); - for (Method method : methods) { - if (method.getName().equals("peekWhen")) { - return true; - } - } - return false; - // TODO(shayba): delete the above, uncomment the below. - // SDK_INT has not yet ramped to Baklava in all 25Q2 builds. - // return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA; + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA; } static { diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h index 0d261abd728d..e51477c668dd 100644 --- a/tools/aapt2/Resource.h +++ b/tools/aapt2/Resource.h @@ -249,6 +249,8 @@ struct ResourceFile { // Flag std::optional<FeatureFlagAttribute> flag; + + bool uses_readwrite_feature_flags = false; }; /** diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp index 5435cba290fc..db7dddc49a99 100644 --- a/tools/aapt2/ResourceTable.cpp +++ b/tools/aapt2/ResourceTable.cpp @@ -664,6 +664,7 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag) if (!config_value->value) { // Resource does not exist, add it now. config_value->value = std::move(res.value); + config_value->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags; } else { // When validation is enabled, ensure that a resource cannot have multiple values defined for // the same configuration unless protected by flags. @@ -681,12 +682,14 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag) ConfigKey{&res.config, res.product}, lt_config_key_ref()), util::make_unique<ResourceConfigValue>(res.config, res.product)); (*it)->value = std::move(res.value); + (*it)->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags; break; } case CollisionResult::kTakeNew: // Take the incoming value. config_value->value = std::move(res.value); + config_value->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags; break; case CollisionResult::kConflict: @@ -843,6 +846,12 @@ NewResourceBuilder& NewResourceBuilder::SetAllowMangled(bool allow_mangled) { return *this; } +NewResourceBuilder& NewResourceBuilder::SetUsesReadWriteFeatureFlags( + bool uses_readwrite_feature_flags) { + res_.uses_readwrite_feature_flags = uses_readwrite_feature_flags; + return *this; +} + NewResource NewResourceBuilder::Build() { return std::move(res_); } diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h index b0e185536d16..778b43adb50b 100644 --- a/tools/aapt2/ResourceTable.h +++ b/tools/aapt2/ResourceTable.h @@ -104,6 +104,9 @@ class ResourceConfigValue { // The actual Value. std::unique_ptr<Value> value; + // Whether the value uses read/write feature flags + bool uses_readwrite_feature_flags = false; + ResourceConfigValue(const android::ConfigDescription& config, android::StringPiece product) : config(config), product(product) { } @@ -284,6 +287,7 @@ struct NewResource { std::optional<AllowNew> allow_new; std::optional<StagedId> staged_id; bool allow_mangled = false; + bool uses_readwrite_feature_flags = false; }; struct NewResourceBuilder { @@ -297,6 +301,7 @@ struct NewResourceBuilder { NewResourceBuilder& SetAllowNew(AllowNew allow_new); NewResourceBuilder& SetStagedId(StagedId id); NewResourceBuilder& SetAllowMangled(bool allow_mangled); + NewResourceBuilder& SetUsesReadWriteFeatureFlags(bool uses_feature_flags); NewResource Build(); private: diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 2a7921600477..755dbb6f8e42 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -673,11 +673,13 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv // Update the output format of this XML file. file_ref->type = XmlFileTypeForOutputFormat(options_.output_format); - bool result = table->AddResource(NewResourceBuilder(file.name) - .SetValue(std::move(file_ref), file.config) - .SetAllowMangled(true) - .Build(), - context_->GetDiagnostics()); + bool result = table->AddResource( + NewResourceBuilder(file.name) + .SetValue(std::move(file_ref), file.config) + .SetAllowMangled(true) + .SetUsesReadWriteFeatureFlags(doc->file.uses_readwrite_feature_flags) + .Build(), + context_->GetDiagnostics()); if (!result) { return false; } diff --git a/tools/aapt2/format/binary/BinaryResourceParser.cpp b/tools/aapt2/format/binary/BinaryResourceParser.cpp index 2e20e8175213..bac871b8bdc3 100644 --- a/tools/aapt2/format/binary/BinaryResourceParser.cpp +++ b/tools/aapt2/format/binary/BinaryResourceParser.cpp @@ -414,6 +414,8 @@ bool BinaryResourceParser::ParseType(const ResourceTablePackage* package, .SetId(res_id, OnIdConflict::CREATE_ENTRY) .SetAllowMangled(true); + res_builder.SetUsesReadWriteFeatureFlags(entry->uses_feature_flags()); + if (entry->flags() & ResTable_entry::FLAG_PUBLIC) { Visibility visibility{Visibility::Level::kPublic}; diff --git a/tools/aapt2/format/binary/ResEntryWriter.cpp b/tools/aapt2/format/binary/ResEntryWriter.cpp index 9dc205f4c1ba..0be392164453 100644 --- a/tools/aapt2/format/binary/ResEntryWriter.cpp +++ b/tools/aapt2/format/binary/ResEntryWriter.cpp @@ -199,6 +199,10 @@ void WriteEntry(const FlatEntry* entry, T* out_result, bool compact = false) { flags |= ResTable_entry::FLAG_WEAK; } + if (entry->uses_readwrite_feature_flags) { + flags |= ResTable_entry::FLAG_USES_FEATURE_FLAGS; + } + if constexpr (std::is_same_v<ResTable_entry_ext, T>) { flags |= ResTable_entry::FLAG_COMPLEX; } diff --git a/tools/aapt2/format/binary/ResEntryWriter.h b/tools/aapt2/format/binary/ResEntryWriter.h index c11598ec12f7..f54b29aa8f2a 100644 --- a/tools/aapt2/format/binary/ResEntryWriter.h +++ b/tools/aapt2/format/binary/ResEntryWriter.h @@ -38,6 +38,8 @@ struct FlatEntry { // The entry string pool index to the entry's name. uint32_t entry_key; + + bool uses_readwrite_feature_flags; }; // Pair of ResTable_entry and Res_value. These pairs are stored sequentially in values buffer. diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index 1a82021bce71..50144ae816b6 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -502,7 +502,8 @@ class PackageFlattener { // Group values by configuration. for (auto& config_value : entry.values) { config_to_entry_list_map[config_value->config].push_back( - FlatEntry{&entry, config_value->value.get(), local_key_index}); + FlatEntry{&entry, config_value->value.get(), local_key_index, + config_value->uses_readwrite_feature_flags}); } } diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp index 0f1168514c4a..9156b96b67ec 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -1069,4 +1069,23 @@ TEST_F(TableFlattenerTest, FlattenTypeEntryWithNameCollapseInExemption) { testing::IsTrue()); } +TEST_F(TableFlattenerTest, UsesReadWriteFeatureFlagSerializesCorrectly) { + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .Add(NewResourceBuilder("com.app.a:color/foo") + .SetValue(util::make_unique<BinaryPrimitive>( + uint8_t(Res_value::TYPE_INT_COLOR_ARGB8), 0xffaabbcc)) + .SetUsesReadWriteFeatureFlags(true) + .SetId(0x7f020000) + .Build()) + .Build(); + ResTable res_table; + TableFlattenerOptions options; + ASSERT_TRUE(Flatten(context_.get(), options, table.get(), &res_table)); + + uint32_t flags; + ASSERT_TRUE(res_table.getResourceEntryFlags(0x7f020000, &flags)); + ASSERT_EQ(flags, ResTable_entry::FLAG_USES_FEATURE_FLAGS); +} + } // namespace aapt diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp index dbef77615515..47a71fe36e9f 100644 --- a/tools/aapt2/link/FlaggedResources_test.cpp +++ b/tools/aapt2/link/FlaggedResources_test.cpp @@ -14,6 +14,9 @@ * limitations under the License. */ +#include <regex> +#include <string> + #include "LoadedApk.h" #include "cmd/Dump.h" #include "io/StringStream.h" @@ -183,4 +186,49 @@ TEST_F(FlaggedResourcesTest, ReadWriteFlagInPathFails) { "Only read only flags may be used with resources: test.package.rwFlag")); } +TEST_F(FlaggedResourcesTest, ReadWriteFlagInXmlGetsFlagged) { + auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"}); + auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag); + + std::string output; + DumpChunksToString(loaded_apk.get(), &output); + + // The actual line looks something like: + // [ResTable_entry] id: 0x0000 name: layout1 keyIndex: 14 size: 8 flags: 0x0010 + // + // This regex matches that line and captures the name and the flag value for checking. + std::regex regex("[0-9a-zA-Z:_\\]\\[ ]+name: ([0-9a-zA-Z]+)[0-9a-zA-Z: ]+flags: (0x\\d{4})"); + std::smatch match; + + std::stringstream ss(output); + std::string line; + bool found = false; + int fields_flagged = 0; + while (std::getline(ss, line)) { + bool first_line = false; + if (line.contains("config: v36")) { + std::getline(ss, line); + first_line = true; + } + if (!line.contains("flags")) { + continue; + } + if (std::regex_search(line, match, regex) && (match.size() == 3)) { + unsigned int hex_value; + std::stringstream hex_ss; + hex_ss << std::hex << match[2]; + hex_ss >> hex_value; + if (hex_value & android::ResTable_entry::FLAG_USES_FEATURE_FLAGS) { + fields_flagged++; + if (first_line && match[1] == "layout1") { + found = true; + } + } + } + } + ASSERT_TRUE(found) << "No entry for layout1 at v36 with FLAG_USES_FEATURE_FLAGS bit set"; + // There should only be 1 entry that has the FLAG_USES_FEATURE_FLAGS bit of flags set to 1 + ASSERT_EQ(fields_flagged, 1); +} + } // namespace aapt diff --git a/tools/aapt2/link/FlaggedXmlVersioner.cpp b/tools/aapt2/link/FlaggedXmlVersioner.cpp index 75c6f17dcb51..8a3337c446cb 100644 --- a/tools/aapt2/link/FlaggedXmlVersioner.cpp +++ b/tools/aapt2/link/FlaggedXmlVersioner.cpp @@ -66,6 +66,28 @@ class AllDisabledFlagsVisitor : public xml::Visitor { bool had_flags_ = false; }; +// An xml visitor that goes through the a doc and determines if any elements are behind a flag. +class FindFlagsVisitor : public xml::Visitor { + public: + void Visit(xml::Element* node) override { + if (had_flags_) { + return; + } + auto* attr = node->FindAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag); + if (attr != nullptr) { + had_flags_ = true; + return; + } + VisitChildren(node); + } + + bool HadFlags() const { + return had_flags_; + } + + bool had_flags_ = false; +}; + std::vector<std::unique_ptr<xml::XmlResource>> FlaggedXmlVersioner::Process(IAaptContext* context, xml::XmlResource* doc) { std::vector<std::unique_ptr<xml::XmlResource>> docs; @@ -74,15 +96,20 @@ std::vector<std::unique_ptr<xml::XmlResource>> FlaggedXmlVersioner::Process(IAap // Support for read/write flags was added in baklava so if the doc will only get used on // baklava or later we can just return the original doc. docs.push_back(doc->Clone()); + FindFlagsVisitor visitor; + doc->root->Accept(&visitor); + docs.back()->file.uses_readwrite_feature_flags = visitor.HadFlags(); } else { auto preBaklavaVersion = doc->Clone(); AllDisabledFlagsVisitor visitor; preBaklavaVersion->root->Accept(&visitor); + preBaklavaVersion->file.uses_readwrite_feature_flags = false; docs.push_back(std::move(preBaklavaVersion)); if (visitor.HadFlags()) { auto baklavaVersion = doc->Clone(); baklavaVersion->file.config.sdkVersion = SDK_BAKLAVA; + baklavaVersion->file.uses_readwrite_feature_flags = true; docs.push_back(std::move(baklavaVersion)); } } diff --git a/tools/processors/intdef_mappings/src/android/processor/IntDefProcessor.kt b/tools/processors/intdef_mappings/src/android/processor/IntDefProcessor.kt index 4995eebdd79e..fe72dae9ff34 100644 --- a/tools/processors/intdef_mappings/src/android/processor/IntDefProcessor.kt +++ b/tools/processors/intdef_mappings/src/android/processor/IntDefProcessor.kt @@ -155,35 +155,35 @@ class IntDefProcessor : AbstractProcessor() { ) { val indent = " " - writer.appendln("{") + writer.appendLine("{") val intDefTypesCount = annotationTypeToIntDefMapping.size var currentIntDefTypesCount = 0 for ((field, intDefMapping) in annotationTypeToIntDefMapping) { - writer.appendln("""$indent"$field": {""") + writer.appendLine("""$indent"$field": {""") // Start IntDef - writer.appendln("""$indent$indent"flag": ${intDefMapping.flag},""") + writer.appendLine("""$indent$indent"flag": ${intDefMapping.flag},""") - writer.appendln("""$indent$indent"values": {""") + writer.appendLine("""$indent$indent"values": {""") intDefMapping.entries.joinTo(writer, separator = ",\n") { (value, identifier) -> """$indent$indent$indent"$value": "$identifier"""" } - writer.appendln() - writer.appendln("$indent$indent}") + writer.appendLine() + writer.appendLine("$indent$indent}") // End IntDef writer.append("$indent}") if (++currentIntDefTypesCount < intDefTypesCount) { - writer.appendln(",") + writer.appendLine(",") } else { - writer.appendln("") + writer.appendLine("") } } - writer.appendln("}") + writer.appendLine("}") } } } |