diff options
330 files changed, 11291 insertions, 4312 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/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/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/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 9c74471c65f1..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); diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java index d5b2f980e1a6..5f57a41675c9 100644 --- a/core/java/android/animation/AnimationHandler.java +++ b/core/java/android/animation/AnimationHandler.java @@ -110,7 +110,8 @@ public class AnimationHandler { } }; - public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>(); + public static final ThreadLocal<AnimationHandler> sAnimatorHandler = + ThreadLocal.withInitial(AnimationHandler::new); private static AnimationHandler sTestHandler = null; private boolean mListDirty = false; @@ -118,9 +119,6 @@ public class AnimationHandler { if (sTestHandler != null) { return sTestHandler; } - if (sAnimatorHandler.get() == null) { - sAnimatorHandler.set(new AnimationHandler()); - } return sAnimatorHandler.get(); } 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/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/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 7f4fd3eff57e..7f57f5dbf0ab 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -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/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/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 4bd54a173601..34272b17cf54 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -385,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 f91056dbce30..da4709b4b8b1 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -10617,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 @@ -12533,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/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/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 cf582176a9f7..4f89c831a05f 100644 --- a/core/java/android/window/DesktopExperienceFlags.java +++ b/core/java/android/window/DesktopExperienceFlags.java @@ -47,17 +47,17 @@ 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_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( @@ -66,9 +66,11 @@ public enum DesktopExperienceFlags { false), ENABLE_PER_DISPLAY_PACKAGE_CONTEXT_CACHE_IN_STATUSBAR_NOTIF( Flags::enablePerDisplayPackageContextCacheInStatusbarNotif, false), - ENABLE_TASKBAR_CONNECTED_DISPLAYS(Flags::enableTaskbarConnectedDisplays, false), + ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE(Flags::enableProjectedDisplayDesktopMode, 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/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 ce3a101a7519..e706af999117 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -938,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/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/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/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml index fe8875e413c4..63f32e3b3cd2 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_base.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml @@ -125,15 +125,7 @@ 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 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 9fb63f6eff13..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> 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 7e84292b0924..629af77b3dda 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_media.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml @@ -126,15 +126,7 @@ 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 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/strings.xml b/core/res/res/values/strings.xml index faa660a65d34..d94d659446ac 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -6710,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/symbols.xml b/core/res/res/values/symbols.xml index ee74208e65c6..3de30f7f25a8 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" /> 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/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/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index 2e33253b5e09..ed5e0c608675 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -17,7 +17,9 @@ package com.android.wm.shell.shared.desktopmode; import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED; +import static android.window.DesktopExperienceFlags.ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE; +import static com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement; import static com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper.enableBubbleToFullscreen; import android.annotation.NonNull; @@ -224,7 +226,7 @@ public class DesktopModeStatus { /** * Return {@code true} if the current device can host desktop sessions on its internal display. */ - public static boolean canInternalDisplayHostDesktops(@NonNull Context context) { + private static boolean canInternalDisplayHostDesktops(@NonNull Context context) { return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops); } @@ -269,6 +271,29 @@ public class DesktopModeStatus { } /** + * Check to see if a display should have desktop mode enabled or not. Internal + * and external displays have separate logic. + */ + public static boolean isDesktopModeSupportedOnDisplay(Context context, Display display) { + if (!canEnterDesktopMode(context)) { + return false; + } + if (display.getType() == Display.TYPE_INTERNAL) { + return canInternalDisplayHostDesktops(context); + } + + // TODO (b/395014779): Change this to use WM API + if ((display.getType() == Display.TYPE_EXTERNAL + || display.getType() == Display.TYPE_OVERLAY) + && enableDisplayContentModeManagement()) { + final WindowManager wm = context.getSystemService(WindowManager.class); + return wm != null && wm.shouldShowSystemDecors(display.getDisplayId()); + } + + return false; + } + + /** * Returns whether the multiple desktops feature is enabled for this device (both backend and * frontend implementations). */ @@ -341,8 +366,11 @@ public class DesktopModeStatus { if (!enforceDeviceRestrictions()) { return true; } - final boolean desktopModeSupported = isDesktopModeSupported(context) - && canInternalDisplayHostDesktops(context); + // If projected display is enabled, #canInternalDisplayHostDesktops is no longer a + // requirement. + final boolean desktopModeSupported = ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE.isTrue() + ? isDesktopModeSupported(context) : (isDesktopModeSupported(context) + && canInternalDisplayHostDesktops(context)); final boolean desktopModeSupportedByDevOptions = Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionSupported(context); 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/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/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/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/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index d230425680ce..73be2d7ac49d 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 @@ -171,6 +171,7 @@ import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel; import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel; import com.android.wm.shell.windowdecor.WindowDecorViewModel; import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer; +import com.android.wm.shell.windowdecor.common.AppHandleAndHeaderVisibilityHelper; import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader; import com.android.wm.shell.windowdecor.common.viewhost.DefaultWindowDecorViewHostSupplier; import com.android.wm.shell.windowdecor.common.viewhost.PooledWindowDecorViewHostSupplier; @@ -1021,6 +1022,7 @@ public abstract class WMShellModule { Optional<DesktopTasksLimiter> desktopTasksLimiter, AppHandleEducationController appHandleEducationController, AppToWebEducationController appToWebEducationController, + AppHandleAndHeaderVisibilityHelper appHandleAndHeaderVisibilityHelper, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, FocusTransitionObserver focusTransitionObserver, @@ -1044,10 +1046,10 @@ public abstract class WMShellModule { rootTaskDisplayAreaOrganizer, interactionJankMonitor, genericLinksParser, assistContentRequester, windowDecorViewHostSupplier, multiInstanceHelper, desktopTasksLimiter, appHandleEducationController, appToWebEducationController, - windowDecorCaptionHandleRepository, activityOrientationChangeHandler, - focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, - taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy, - desktopTilingDecorViewModel, + appHandleAndHeaderVisibilityHelper, windowDecorCaptionHandleRepository, + activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger, + desktopModeUiEventLogger, taskResourceLoader, recentsTransitionHandler, + desktopModeCompatPolicy, desktopTilingDecorViewModel, multiDisplayDragMoveIndicatorController)); } @@ -1075,6 +1077,16 @@ public abstract class WMShellModule { @WMSingleton @Provides + static AppHandleAndHeaderVisibilityHelper provideAppHandleAndHeaderVisibilityHelper( + @NonNull Context context, + @NonNull DisplayController displayController, + @NonNull DesktopModeCompatPolicy desktopModeCompatPolicy) { + return new AppHandleAndHeaderVisibilityHelper(context, displayController, + desktopModeCompatPolicy); + } + + @WMSingleton + @Provides static WindowDecorTaskResourceLoader provideWindowDecorTaskResourceLoader( @NonNull Context context, @NonNull ShellInit shellInit, @NonNull ShellController shellController, 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/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/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 2835a18cd809..dbc599be57af 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 @@ -238,6 +240,10 @@ class DesktopTasksController( removeVisualIndicator() } + override fun onTransitionInterrupted() { + removeVisualIndicator() + } + private fun removeVisualIndicator() { visualIndicator?.fadeOutIndicator { releaseVisualIndicator() } } @@ -1198,6 +1204,11 @@ class DesktopTasksController( return } + if (splitScreenController.isTaskInSplitScreen(task.taskId)) { + moveSplitPairToDisplay(task, displayId) + return + } + val wct = WindowContainerTransaction() val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId) if (displayAreaInfo == null) { @@ -1205,39 +1216,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") @@ -1299,6 +1277,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). @@ -2261,11 +2286,19 @@ class DesktopTasksController( 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) @@ -2501,9 +2534,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) 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 4f511a901756..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() @@ -357,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 @@ -539,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 @@ -590,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, @@ -617,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( @@ -783,7 +902,7 @@ sealed class DragToDesktopTransitionHandler( } ?: false } - private fun startCancelAnimation() { + private fun startCancelAnimation(): Animator { val state = requireTransitionState() val dragToDesktopAnimator = state.dragAnimator @@ -800,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 -> @@ -818,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() @@ -910,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 { @@ -930,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, @@ -945,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() @@ -962,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/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 05185cc5cdf0..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 @@ -17,11 +17,9 @@ package com.android.wm.shell.windowdecor; import static android.app.ActivityTaskManager.INVALID_TASK_ID; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; -import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.InputDevice.SOURCE_TOUCHSCREEN; import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_HOVER_ENTER; @@ -79,7 +77,6 @@ import android.view.SurfaceControl.Transaction; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewRootImpl; -import android.view.WindowManager; import android.window.DesktopModeFlags; import android.window.TaskSnapshot; import android.window.WindowContainerToken; @@ -121,7 +118,6 @@ import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition; import com.android.wm.shell.desktopmode.DesktopTasksLimiter; import com.android.wm.shell.desktopmode.DesktopUserRepositories; -import com.android.wm.shell.desktopmode.DesktopWallpaperActivity; import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction; import com.android.wm.shell.desktopmode.common.ToggleTaskSizeUtilsKt; @@ -146,6 +142,7 @@ import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionRegionListener; +import com.android.wm.shell.windowdecor.common.AppHandleAndHeaderVisibilityHelper; import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier; @@ -207,6 +204,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final Optional<DesktopTasksLimiter> mDesktopTasksLimiter; private final AppHandleEducationController mAppHandleEducationController; private final AppToWebEducationController mAppToWebEducationController; + private final AppHandleAndHeaderVisibilityHelper mAppHandleAndHeaderVisibilityHelper; private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory; private boolean mTransitionDragActive; @@ -294,6 +292,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, Optional<DesktopTasksLimiter> desktopTasksLimiter, AppHandleEducationController appHandleEducationController, AppToWebEducationController appToWebEducationController, + AppHandleAndHeaderVisibilityHelper appHandleAndHeaderVisibilityHelper, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, FocusTransitionObserver focusTransitionObserver, @@ -338,6 +337,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, desktopTasksLimiter, appHandleEducationController, appToWebEducationController, + appHandleAndHeaderVisibilityHelper, windowDecorCaptionHandleRepository, activityOrientationChangeHandler, new TaskPositionerFactory(), @@ -386,6 +386,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, Optional<DesktopTasksLimiter> desktopTasksLimiter, AppHandleEducationController appHandleEducationController, AppToWebEducationController appToWebEducationController, + AppHandleAndHeaderVisibilityHelper appHandleAndHeaderVisibilityHelper, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, TaskPositionerFactory taskPositionerFactory, @@ -431,6 +432,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDesktopTasksLimiter = desktopTasksLimiter; mAppHandleEducationController = appHandleEducationController; mAppToWebEducationController = appToWebEducationController; + mAppHandleAndHeaderVisibilityHelper = appHandleAndHeaderVisibilityHelper; mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository; mActivityOrientationChangeHandler = activityOrientationChangeHandler; mAssistContentRequester = assistContentRequester; @@ -528,6 +530,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, @Override public void setSplitScreenController(SplitScreenController splitScreenController) { mSplitScreenController = splitScreenController; + mAppHandleAndHeaderVisibilityHelper.setSplitScreenController(splitScreenController); } @Override @@ -1724,32 +1727,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, } private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { - if (mDisplayController.getDisplay(taskInfo.displayId) == null) { - // If DisplayController doesn't have it tracked, it could be a private/managed display. - return false; - } - if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true; - if (mSplitScreenController != null - && mSplitScreenController.isTaskRootOrStageRoot(taskInfo.taskId)) { - return false; - } - if (mDesktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(taskInfo)) { - return false; - } - final boolean isOnLargeScreen = - mDisplayController.getDisplay(taskInfo.displayId).getMinSizeDimensionDp() - >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP; - if (!DesktopModeStatus.canEnterDesktopMode(mContext) - && DesktopModeStatus.overridesShowAppHandle(mContext) && !isOnLargeScreen) { - // Devices with multiple screens may enable the app handle but it should not show on - // small screens - return false; - } - return DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(mContext) - && !DesktopWallpaperActivity.isWallpaperTask(taskInfo) - && taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED - && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD - && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop(); + return mAppHandleAndHeaderVisibilityHelper.shouldShowAppHandleOrHeader(taskInfo); } private void createWindowDecoration( 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/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/AppHandleAndHeaderVisibilityHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt new file mode 100644 index 000000000000..39ccf5bd03a7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt @@ -0,0 +1,100 @@ +/* + * 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.windowdecor.common + +import android.app.ActivityManager +import android.app.WindowConfiguration +import android.content.Context +import android.view.WindowManager +import android.window.DesktopExperienceFlags.ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.desktopmode.DesktopWallpaperActivity.Companion.isWallpaperTask +import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.splitscreen.SplitScreenController + +/** + * Resolves whether, given a task and its associated display that it is currently on, to show the + * app handle/header or not. + */ +class AppHandleAndHeaderVisibilityHelper ( + private val context: Context, + private val displayController: DisplayController, + private val desktopModeCompatPolicy: DesktopModeCompatPolicy +) { + var splitScreenController: SplitScreenController? = null + + /** + * Returns, given a task's attribute and its display attribute, whether the app + * handle/header should show or not for this task. + */ + fun shouldShowAppHandleOrHeader(taskInfo: ActivityManager.RunningTaskInfo): Boolean { + if (!ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY.isTrue) { + return allowedForTask(taskInfo) + } + return allowedForTask(taskInfo) && allowedForDisplay(taskInfo.displayId) + } + + private fun allowedForTask(taskInfo: ActivityManager.RunningTaskInfo): Boolean { + // TODO (b/382023296): Remove once we no longer rely on + // Flags.enableBugFixesForSecondaryDisplay as it is taken care of in #allowedForDisplay + if (displayController.getDisplay(taskInfo.displayId) == null) { + // If DisplayController doesn't have it tracked, it could be a private/managed display. + return false + } + if (taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM) return true + if (splitScreenController?.isTaskRootOrStageRoot(taskInfo.taskId) == true) { + return false + } + + if (desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(taskInfo)) { + return false + } + + // TODO (b/382023296): Remove once we no longer rely on + // Flags.enableBugFixesForSecondaryDisplay as it is taken care of in #allowedForDisplay + val isOnLargeScreen = + displayController.getDisplay(taskInfo.displayId).minSizeDimensionDp >= + WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP + if (!DesktopModeStatus.canEnterDesktopMode(context) + && DesktopModeStatus.overridesShowAppHandle(context) + && !isOnLargeScreen + ) { + // Devices with multiple screens may enable the app handle but it should not show on + // small screens + return false + } + return DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context) + && !isWallpaperTask(taskInfo) + && taskInfo.windowingMode != WindowConfiguration.WINDOWING_MODE_PINNED + && taskInfo.activityType == WindowConfiguration.ACTIVITY_TYPE_STANDARD + && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop + } + + private fun allowedForDisplay(displayId: Int): Boolean { + // If DisplayController doesn't have it tracked, it could be a private/managed display. + val display = displayController.getDisplay(displayId) + if (display == null) return false + + if (DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, display)) { + return true + } + // If on default display and on Large Screen (unfolded), show app handle + return DesktopModeStatus.overridesShowAppHandle(context) + && display.minSizeDimensionDp >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP + } +}
\ No newline at end of file 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/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/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/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index d093629000f3..75308442d76a 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 @@ -31,6 +31,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 @@ -1133,6 +1134,54 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @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 @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) fun handleRequest_newFreeformTaskLaunch_cascadeApplied() { setUpLandscapeDisplay() 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/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/desktopmode/DesktopModeStatusTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt index fb62ba75e056..edf91fe62e7d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt @@ -234,14 +234,25 @@ class DesktopModeStatusTest : ShellTestCase() { assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() } + @DisableFlags(Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE) @Test fun isDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktopOff_returnsFalse() { doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) - doReturn(false).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)) + doReturn(false).whenever(mockResources) + .getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)) assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() } + @EnableFlags(Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE) + @Test + fun isPDDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktopOff_returnsTrue() { + doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + doReturn(false).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)) + + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue() + } + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test 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/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index e89a122595d5..d69509faf4ec 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -115,7 +115,8 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest .spyStatic(DragPositioningCallbackUtility::class.java) .startMocking() - doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(Mockito.any()) } + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupportedOnDisplay(Mockito.any(), + Mockito.any()) } doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(Mockito.any()) } doReturn(false).`when` { DesktopModeStatus.overridesShowAppHandle(Mockito.any()) } @@ -394,7 +395,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN) - doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(any()) } + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupportedOnDisplay(any(), any()) } setUpMockDecorationsForTasks(task) onTaskOpening(task) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt index 81dfaed56b6f..a1f40fdefee9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt @@ -79,6 +79,7 @@ import com.android.wm.shell.transition.Transitions import com.android.wm.shell.util.StubTransaction import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener +import com.android.wm.shell.windowdecor.common.AppHandleAndHeaderVisibilityHelper import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier @@ -174,6 +175,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { internal lateinit var desktopModeOnKeyguardChangedListener: DesktopModeKeyguardChangeListener protected lateinit var desktopModeWindowDecorViewModel: DesktopModeWindowDecorViewModel protected lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy + protected lateinit var appHandleAndHeaderVisibilityHelper: AppHandleAndHeaderVisibilityHelper fun setUpCommon() { spyContext = spy(mContext) @@ -185,9 +187,13 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { whenever(mockDesktopUserRepositories.current).thenReturn(mockDesktopRepository) whenever(mockDisplayController.getDisplayContext(any())).thenReturn(spyContext) whenever(mockDisplayController.getDisplay(any())).thenReturn(display) + whenever(display.type).thenReturn(Display.TYPE_INTERNAL) whenever(mockDesktopUserRepositories.getProfile(anyInt())) .thenReturn(mockDesktopRepository) desktopModeCompatPolicy = DesktopModeCompatPolicy(spyContext) + appHandleAndHeaderVisibilityHelper = + AppHandleAndHeaderVisibilityHelper(spyContext, mockDisplayController, + desktopModeCompatPolicy) desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel( spyContext, testShellExecutor, @@ -222,6 +228,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { Optional.of(mockTasksLimiter), mockAppHandleEducationController, mockAppToWebEducationController, + appHandleAndHeaderVisibilityHelper, mockCaptionHandleRepository, Optional.of(mockActivityOrientationChangeHandler), mockTaskPositionerFactory, 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/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/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/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/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/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 0c64fa7d34dd..d55bbb3bbdbf 100644 --- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java +++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java @@ -447,6 +447,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/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/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/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index f0a0483aa17c..a2291123e192 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -230,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, @@ -267,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, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index b5de7e719043..a4325344709a 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -324,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, @@ -430,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"})); 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/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/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/keyevent/domain/interactor/KeyEventInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt index 684af6fc8040..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 @@ -64,10 +64,10 @@ class KeyEventInteractorTest : SysuiTestCase() { val isPowerButtonLongPressed by collectLastValue( underTest.isPowerButtonLongPressed) - repository.setPowerButtonBeingLongPressed(false) + repository.setPowerButtonLongPressed(false) assertThat(isPowerButtonLongPressed).isFalse() - repository.setPowerButtonBeingLongPressed(true) + 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/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/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/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/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/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/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/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/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt index e22acd53e584..9804932918dc 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) } 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..6415f8c25a37 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 @@ -60,11 +60,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; 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/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/SqueezeEffectInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectInteractorTest.kt new file mode 100644 index 000000000000..fb19a884a210 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectInteractorTest.kt @@ -0,0 +1,64 @@ +/* + * 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 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.android.systemui.topwindoweffects.domain.interactor.SqueezeEffectInteractor +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/topwindoweffects/SqueezeEffectRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectRepositoryTest.kt new file mode 100644 index 000000000000..5d8d3f90e13c --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectRepositoryTest.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.topwindoweffects + +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.topwindoweffects.data.repository.SqueezeEffectRepositoryImpl +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/TopLevelWindowEffectsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt new file mode 100644 index 000000000000..83dc45c8c511 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.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.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.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 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> + + private val Kosmos.underTest by Kosmos.Fixture { + TopLevelWindowEffects( + context = mContext, + applicationScope = testScope.backgroundScope, + windowManager = ViewCaptureAwareWindowManager( + windowManager = windowManager, + lazyViewCapture = viewCapture, + isViewCaptureEnabled = false + ), + 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 + + 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/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/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/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/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 19e05871c035..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. --> 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/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 19caead525c9..fb3bc620ee68 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java @@ -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; @@ -166,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; @@ -178,6 +181,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, mProfileManager = localBluetoothManager.getProfileManager(); mUiEventLogger = uiEventLogger; mLaunchSourceId = launchSourceId; + mQSSettingsPackageRepository = qsSettingsPackageRepository; } @Override @@ -193,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)); } @@ -396,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)); }); @@ -518,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/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/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index 1ef3a950ba0d..11b42a8eafd6 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -36,7 +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.effects.dagger.TopLevelWindowEffectsModule; +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; @@ -161,6 +162,7 @@ import javax.inject.Named; StatusBarPhoneModule.class, SystemActionsModule.class, ShadeModule.class, + SqueezeEffectRepositoryModule.class, StartCentralSurfacesModule.class, SceneContainerFrameworkModule.class, SysUICoroutinesModule.class, 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/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/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/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/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/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/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..795e811db2bc 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,13 +63,21 @@ 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); + mMainExecutor = mainExecutor; + mBackgroundExecutor = backgroundExecutor; mIsInitVolumeFirstTime = true; } @@ -181,9 +191,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 +203,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); } @@ -297,8 +307,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 +328,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 +342,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() { @@ -503,9 +513,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 +545,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 +596,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 +612,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 +624,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 +727,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 +820,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 02dce406bbee..9e6fa48d6f98 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. @@ -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, @@ -230,20 +228,7 @@ public class MediaSwitchingController mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName); 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); @@ -568,26 +553,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,34 +572,6 @@ 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); @@ -1097,7 +1043,7 @@ public class MediaSwitchingController mVolumePanelGlobalStateInteractor, mUserTracker); MediaOutputBroadcastDialog dialog = new MediaOutputBroadcastDialog(mContext, true, - broadcastSender, controller); + broadcastSender, controller, mMainExecutor, mBackgroundExecutor); dialog.show(); } 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/effects/TopLevelWindowEffects.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaActionState.kt index 91530078e378..c3ce5035accc 100644 --- a/packages/SystemUI/src/com/android/systemui/effects/TopLevelWindowEffects.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaActionState.kt @@ -14,15 +14,10 @@ * limitations under the License. */ -package com.android.systemui.effects; +package com.android.systemui.media.remedia.shared.model -import com.android.systemui.CoreStartable -import com.android.systemui.dagger.SysUISingleton; -import javax.inject.Inject - -@SysUISingleton -class TopLevelWindowEffects @Inject constructor() : CoreStartable { - override fun start() { - - } +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..9c6568057d6f 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 @@ -46,6 +47,7 @@ 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 @@ -65,12 +67,11 @@ import androidx.compose.material3.SliderState import androidx.compose.material3.Text 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 +91,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 +107,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 +258,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 +277,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 +315,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 +326,7 @@ private fun ContentScope.CardForeground( viewModel = viewModel, threeRows = threeRows, fillHeight = fillHeight, + colorScheme = colorScheme, modifier = Modifier.graphicsLayer { compositingStrategy = CompositingStrategy.ModulateAlpha @@ -198,6 +336,7 @@ private fun ContentScope.CardForeground( CardGuts( viewModel = viewModel.guts, + colorScheme = colorScheme, modifier = Modifier.graphicsLayer { compositingStrategy = CompositingStrategy.ModulateAlpha @@ -232,6 +371,7 @@ private fun ContentScope.CardForegroundContent( viewModel: MediaCardViewModel, threeRows: Boolean, fillHeight: Boolean, + colorScheme: AnimatedColorScheme, modifier: Modifier = Modifier, ) { Column( @@ -249,12 +389,16 @@ private fun ContentScope.CardForegroundContent( // Icon. Icon( icon = viewModel.icon, - tint = LocalAndroidColorScheme.current.primaryFixed, + tint = colorScheme.primary, modifier = Modifier.size(24.dp).clip(CircleShape), ) Spacer(modifier = Modifier.weight(1f)) viewModel.outputSwitcherChips.fastForEach { chip -> - OutputSwitcherChip(viewModel = chip, modifier = Modifier.padding(start = 8.dp)) + OutputSwitcherChip( + viewModel = chip, + colorScheme = colorScheme, + modifier = Modifier.padding(start = 8.dp), + ) } } @@ -280,14 +424,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 }, + ) + } } } @@ -297,7 +443,14 @@ private fun ContentScope.CardForegroundContent( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 24.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, @@ -321,18 +474,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 +544,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 +567,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 +609,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 +663,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 +814,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 +853,7 @@ private fun CardGuts(viewModel: MediaCardGutsViewModel, modifier: Modifier = Mod ) { Text( text = checkNotNull(viewModel.primaryAction.text), - color = LocalAndroidColorScheme.current.onPrimaryFixed, + color = colorScheme.onPrimary, ) } @@ -740,28 +911,22 @@ 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 - ), + colors = ButtonDefaults.buttonColors(containerColor = colorScheme.primary), contentPadding = PaddingValues(start = 8.dp, end = 12.dp, top = 4.dp, bottom = 4.dp), modifier = modifier.height(24.dp), ) { - Icon( - icon = viewModel.icon, - tint = LocalAndroidColorScheme.current.onPrimaryFixed, - modifier = Modifier.size(16.dp), - ) + Icon(icon = viewModel.icon, tint = colorScheme.onPrimary, 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, + color = colorScheme.onPrimary, ) } } @@ -785,7 +950,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 +959,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 +1004,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 +1020,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 +1048,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/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/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/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/settings/brightness/ComposeDialogComposableProvider.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt index 22449c73c921..962a3bd2376b 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt @@ -25,6 +25,7 @@ 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 @@ -46,7 +47,11 @@ private fun BrightnessSliderForDialog( rememberViewModel(traceName = "BrightnessDialog.viewModel") { brightnessSliderViewModelFactory.create(false) } - BrightnessSliderContainer(viewModel = viewModel, Modifier.fillMaxWidth().padding(8.dp)) + 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/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/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/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/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index 76d8fb8d3c15..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,7 +128,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView updateColors(); } - private void updateColors() { + protected void updateColors() { if (notificationRowTransparency()) { if (mIsBlurSupported) { mNormalColor = SurfaceEffectColors.surfaceEffect1(getContext()); 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 4e21885d7b78..86509a91f134 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 @@ -114,7 +114,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; @@ -123,6 +122,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; @@ -185,7 +185,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private boolean mShowSnooze = false; private boolean mIsFaded; - private boolean mIsPromotedOngoing = false; private boolean mHasStatusBarChipDuringHeadsUpAnimation = false; @Nullable @@ -871,7 +870,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 +978,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } else if (isAboveShelf() != wasAboveShelf) { mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf); } - updateBackgroundOpacity(); + updateColors(); } /** @@ -1350,7 +1349,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mIsSummaryWithChildren) { return mChildrenContainer.getIntrinsicHeight(); } - if (isPromotedOngoing()) { + if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { return getMaxExpandHeight(); } if (mExpandedWhenPinned) { @@ -2939,7 +2938,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (mIsSummaryWithChildren && !shouldShowPublic()) { return !mChildrenExpanded; } - if (isPromotedOngoing()) { + if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { return false; } return mEnableNonGroupedNotificationExpand && mExpandable; @@ -2950,17 +2949,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. @@ -3061,7 +3049,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public void setUserLocked(boolean userLocked) { - if (isPromotedOngoing()) return; + if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) return; mUserLocked = userLocked; mPrivateLayout.setUserExpanding(userLocked); @@ -3121,7 +3109,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mChildrenContainer.setOnKeyguard(onKeyguard); } } - updateBackgroundOpacity(); + updateColors(); } } @@ -3247,7 +3235,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) { @@ -3309,7 +3306,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public boolean isExpanded(boolean allowOnKeyguard) { - if (isPromotedOngoing()) { + if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) { return isPromotedNotificationExpanded(allowOnKeyguard); } @@ -4396,9 +4393,7 @@ 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()); @@ -4630,11 +4625,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/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..509f836ffb8d 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 @@ -1520,10 +1519,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..d7dd7ec08583 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 @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.notification.shared import android.app.PendingIntent import android.graphics.drawable.Icon -import android.util.Log import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.stack.PriorityBucket @@ -86,14 +85,6 @@ data class ActiveNotificationModel( */ val promotedContent: PromotedNotificationContentModel?, ) : ActiveNotificationEntryModel() { - init { - if (!PromotedNotificationContentModel.featureFlagEnabled()) { - if (promotedContent != null) { - Log.wtf(TAG, "passing non-null promoted content without feature flag enabled") - } - } - } - companion object { private const val TAG = "ActiveNotificationEntryModel" } 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/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/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..7dc6fa8e9f70 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt @@ -0,0 +1,85 @@ +/* + * 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.topwindoweffects.domain.interactor.SqueezeEffectInteractor +import com.android.systemui.topwindoweffects.ui.compose.EffectsWindowRoot +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 +) : 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 && root == null) { + root = EffectsWindowRoot(context) + root?.let { windowManager.addView(it, getWindowManagerLayoutParams()) } + } else if (root?.isAttachedToWindow == true) { + windowManager.removeView(root) + root = null + } + } + } + } + + 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/effects/dagger/TopLevelWindowEffectsModule.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/TopLevelWindowEffectsModule.kt index 70c4636e25c5..6fbfedc94774 100644 --- a/packages/SystemUI/src/com/android/systemui/effects/dagger/TopLevelWindowEffectsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/TopLevelWindowEffectsModule.kt @@ -14,10 +14,10 @@ * limitations under the License. */ -package com.android.systemui.effects.dagger +package com.android.systemui.topwindoweffects.dagger import com.android.systemui.CoreStartable -import com.android.systemui.effects.TopLevelWindowEffects +import com.android.systemui.topwindoweffects.TopLevelWindowEffects import dagger.Binds import dagger.Module import dagger.multibindings.ClassKey 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..0826917c5cb5 --- /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.content.Context +import android.util.AttributeSet +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.AbstractComposeView +import com.android.systemui.compose.ComposeInitializer + +class EffectsWindowRoot : AbstractComposeView { + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor( + context: Context, + attrs: AttributeSet, + defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + override fun onAttachedToWindow() { + ComposeInitializer.onAttachedToWindow(this) + super.onAttachedToWindow() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + ComposeInitializer.onDetachedFromWindow(this) + } + + @Composable + override fun Content() { + SqueezeEffect() + } +}
\ 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..124724000aaf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.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.topwindoweffects.ui.compose + +import androidx.compose.runtime.Composable + +@Composable +fun SqueezeEffect() { + // TODO: add squeeze effect +}
\ No newline at end of file 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/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/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/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/keyevent/data/repository/FakeKeyEventRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt index 807bc82cbf4e..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 @@ -34,7 +34,7 @@ class FakeKeyEventRepository @Inject constructor() : KeyEventRepository { _isPowerButtonDown.value = isDown } - fun setPowerButtonBeingLongPressed(isLongPressed: Boolean) { + fun setPowerButtonLongPressed(isLongPressed: Boolean) { _isPowerButtonLongPressed.value = isLongPressed } } 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/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/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt index ff4fbf9f0e94..7f012ba25ab9 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 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/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/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/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java index 739ea0df87ab..cc93d0887d89 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -248,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() { @@ -495,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. @@ -648,6 +654,7 @@ public class AutoclickController extends BaseEventStreamTransformation { } mLastMotionEvent = MotionEvent.obtain(event); mEventPolicyFlags = policyFlags; + mHoveredState = isHovered(); if (useAsAnchor) { final int pointerIndex = mLastMotionEvent.getActionIndex(); @@ -729,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/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 6fdb2b6b83f7..6515b237519a 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; @@ -1416,6 +1417,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 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 1263146fe405..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) { 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/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/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 b7599f6e40c3..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,25 +26,20 @@ 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; @@ -51,14 +49,15 @@ import android.annotation.Nullable; import android.app.AppOpsManager; 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 java.io.PrintWriter; import java.text.SimpleDateFormat; import java.time.Duration; +import java.util.Arrays; import java.util.Date; import java.util.Set; @@ -95,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(); @@ -121,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 @@ -191,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) { @@ -221,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) { @@ -243,7 +289,7 @@ 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; } diff --git a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java index c897891d02c3..0e1fbf3a6d1a 100644 --- a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java @@ -180,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) { @@ -213,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/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/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/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/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/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/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 cb122f2080a2..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; @@ -532,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/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/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/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java index dc42b32967e2..d91fca9e2816 100644 --- a/services/core/java/com/android/server/wm/DesktopModeHelper.java +++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java @@ -17,6 +17,7 @@ package com.android.server.wm; import static android.app.Flags.enableConnectedDisplaysWallpaper; +import static android.window.DesktopExperienceFlags.ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE; import android.annotation.NonNull; import android.content.Context; @@ -66,7 +67,7 @@ public final class DesktopModeHelper { * Return {@code true} if the current device can hosts desktop sessions on its internal display. */ @VisibleForTesting - static boolean canInternalDisplayHostDesktops(@NonNull Context context) { + private static boolean canInternalDisplayHostDesktops(@NonNull Context context) { return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops); } @@ -83,8 +84,11 @@ public final class DesktopModeHelper { if (!shouldEnforceDeviceRestrictions()) { return true; } - final boolean desktopModeSupported = isDesktopModeSupported(context) - && canInternalDisplayHostDesktops(context); + // If projected display is enabled, #canInternalDisplayHostDesktops is no longer a + // requirement. + final boolean desktopModeSupported = ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE.isTrue() + ? isDesktopModeSupported(context) : (isDesktopModeSupported(context) + && canInternalDisplayHostDesktops(context)); final boolean desktopModeSupportedByDevOptions = Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionsSupported(context); 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/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/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/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/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 0ad976c38565..51ed6bb2aa40 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -3806,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); } @@ -19716,7 +19717,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } mInjector.getActivityManagerInternal() - .setSwitchingFromSystemUserMessage(startUserSessionMessageString); + .setSwitchingFromUserMessage(caller.getUserId(), startUserSessionMessageString); } @Override @@ -19741,7 +19742,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } mInjector.getActivityManagerInternal() - .setSwitchingToSystemUserMessage(endUserSessionMessageString); + .setSwitchingToUserMessage(caller.getUserId(), endUserSessionMessageString); } @Override 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/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/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/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java index 69877c372442..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); @@ -713,6 +727,100 @@ public class AutoclickControllerTest { 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/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/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/DesktopModeHelperTest.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java index 1e91bedb5c18..43755ea3165e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java @@ -181,6 +181,7 @@ public class DesktopModeHelperTest { assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue(); } + @DisableFlags(Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE) @Test public void isDeviceEligibleForDesktopMode_configDEModeOffAndIntDispHostsDesktop_returnsFalse() { doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); 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/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/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/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/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); |