diff options
496 files changed, 13112 insertions, 4636 deletions
diff --git a/Android.bp b/Android.bp index 127556f8e075..e7c20418bcef 100644 --- a/Android.bp +++ b/Android.bp @@ -427,6 +427,7 @@ java_defaults { "framework-permission-aidl-java", "spatializer-aidl-java", "audiopolicy-aidl-java", + "volumegroupcallback-aidl-java", "sounddose-aidl-java", "modules-utils-expresslog", "perfetto_trace_javastream_protos_jarjar", diff --git a/boot/boot-image-profile.txt b/boot/boot-image-profile.txt index d7c409fd54b4..4c3e5dce7a95 100644 --- a/boot/boot-image-profile.txt +++ b/boot/boot-image-profile.txt @@ -28829,7 +28829,6 @@ Landroid/media/audiopolicy/AudioProductStrategy$AudioAttributesGroup; Landroid/media/audiopolicy/AudioProductStrategy; Landroid/media/audiopolicy/AudioVolumeGroup$1; Landroid/media/audiopolicy/AudioVolumeGroup; -Landroid/media/audiopolicy/AudioVolumeGroupChangeHandler; Landroid/media/audiopolicy/IAudioPolicyCallback$Stub$Proxy; Landroid/media/audiopolicy/IAudioPolicyCallback$Stub; Landroid/media/audiopolicy/IAudioPolicyCallback; diff --git a/boot/preloaded-classes b/boot/preloaded-classes index 7f4b3244c164..048687781774 100644 --- a/boot/preloaded-classes +++ b/boot/preloaded-classes @@ -5509,7 +5509,6 @@ android.media.audiopolicy.AudioProductStrategy$AudioAttributesGroup android.media.audiopolicy.AudioProductStrategy android.media.audiopolicy.AudioVolumeGroup$1 android.media.audiopolicy.AudioVolumeGroup -android.media.audiopolicy.AudioVolumeGroupChangeHandler android.media.audiopolicy.IAudioPolicyCallback$Stub$Proxy android.media.audiopolicy.IAudioPolicyCallback$Stub android.media.audiopolicy.IAudioPolicyCallback diff --git a/cmds/incidentd/src/IncidentService.cpp b/cmds/incidentd/src/IncidentService.cpp index 5ebf3e2c3047..de35ffc3fdb9 100644 --- a/cmds/incidentd/src/IncidentService.cpp +++ b/cmds/incidentd/src/IncidentService.cpp @@ -52,7 +52,11 @@ enum { #define SKIPPED_DUMPSTATE_SECTIONS { \ 1100, 1101, 1102, 1103, 1104, 1105, 1106, 1107, 1108, /* Logs */ \ 1200, 1201, 1202, /* Native, hal, java traces */ \ - 3018, /* dumpsys meminfo*/ } + /* dumpsys sections except for odpm data (3054- 3056) which are still needed */ \ + 3000, 3001, 3002, 3003, 3004, 3005, 3006, 3007, 3008, 3009, 3010, 3011, 3012, 3013, \ + 3014, 3015, 3016, 3017, 3018, 3019, 3020, 3021, 3022, 3023, 3024, 3027, 3028, 3029, \ + 3030, 3031, 3032, 3033, 3034, 3035, 3036, 3037, 3038, 3039, 3040, 3041, 3042, 3043, \ + 3044, 3045, 3046, 3047, 3048, 3049, 3050, 3051, 3052, 3053, 4000, 4001,} namespace android { namespace os { diff --git a/config/preloaded-classes b/config/preloaded-classes index 707acb00b102..3f0e00be75a7 100644 --- a/config/preloaded-classes +++ b/config/preloaded-classes @@ -5514,7 +5514,6 @@ android.media.audiopolicy.AudioProductStrategy$AudioAttributesGroup android.media.audiopolicy.AudioProductStrategy android.media.audiopolicy.AudioVolumeGroup$1 android.media.audiopolicy.AudioVolumeGroup -android.media.audiopolicy.AudioVolumeGroupChangeHandler android.media.audiopolicy.IAudioPolicyCallback$Stub$Proxy android.media.audiopolicy.IAudioPolicyCallback$Stub android.media.audiopolicy.IAudioPolicyCallback diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 526a213a6003..98570172e43c 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -304,6 +304,8 @@ package android.net { field public static final int TYPE_VPN_LEGACY = 3; // 0x3 field public static final int TYPE_VPN_NONE = -1; // 0xffffffff field public static final int TYPE_VPN_OEM = 4; // 0x4 + field @FlaggedApi("android.net.platform.flags.vpn_type_oem_service_and_legacy") public static final int TYPE_VPN_OEM_LEGACY = 6; // 0x6 + field @FlaggedApi("android.net.platform.flags.vpn_type_oem_service_and_legacy") public static final int TYPE_VPN_OEM_SERVICE = 5; // 0x5 field public static final int TYPE_VPN_PLATFORM = 2; // 0x2 field public static final int TYPE_VPN_SERVICE = 1; // 0x1 } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 2ce3609d77e7..95b9b49dae3d 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2940,6 +2940,13 @@ package android.app.smartspace.uitemplatedata { package android.app.supervision { + @FlaggedApi("android.app.supervision.flags.enable_supervision_app_service") public class SupervisionAppService extends android.app.Service { + ctor public SupervisionAppService(); + method @Nullable public final android.os.IBinder onBind(@Nullable android.content.Intent); + method @FlaggedApi("android.app.supervision.flags.enable_supervision_app_service") public void onDisabled(); + method @FlaggedApi("android.app.supervision.flags.enable_supervision_app_service") public void onEnabled(); + } + @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager { method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public android.content.Intent createConfirmSupervisionCredentialsIntent(); method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isSupervisionEnabled(); @@ -7435,7 +7442,7 @@ package android.media { method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void muteAwaitConnection(@NonNull int[], @NonNull android.media.AudioDeviceAttributes, long, @NonNull java.util.concurrent.TimeUnit) throws java.lang.IllegalStateException; method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int registerAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void registerMuteAwaitConnectionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.MuteAwaitConnectionCallback); - method public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback); + method @FlaggedApi("android.media.audio.register_volume_callback_api_hardening") @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeAssistantServicesUids(@NonNull int[]); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean removeDeviceAsNonDefaultForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull android.media.AudioDeviceAttributes); method @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_AUDIO_ROUTING, "android.permission.QUERY_AUDIO_STATE"}) public void removeOnDevicesForAttributesChangedListener(@NonNull android.media.AudioManager.OnDevicesForAttributesChangedListener); @@ -7466,7 +7473,7 @@ package android.media { method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicyAsync(@NonNull android.media.audiopolicy.AudioPolicy); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterMuteAwaitConnectionCallback(@NonNull android.media.AudioManager.MuteAwaitConnectionCallback); - method public void unregisterVolumeGroupCallback(@NonNull android.media.AudioManager.VolumeGroupCallback); + method @FlaggedApi("android.media.audio.register_volume_callback_api_hardening") @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public void unregisterVolumeGroupCallback(@NonNull android.media.AudioManager.VolumeGroupCallback); field public static final String ACTION_VOLUME_CHANGED = "android.media.VOLUME_CHANGED_ACTION"; field public static final int AUDIOFOCUS_FLAG_DELAY_OK = 1; // 0x1 field public static final int AUDIOFOCUS_FLAG_LOCK = 4; // 0x4 diff --git a/core/java/android/app/AppCompatTaskInfo.java b/core/java/android/app/AppCompatTaskInfo.java index 3fd9d8b26611..85621c9c3fab 100644 --- a/core/java/android/app/AppCompatTaskInfo.java +++ b/core/java/android/app/AppCompatTaskInfo.java @@ -136,7 +136,7 @@ public class AppCompatTaskInfo implements Parcelable { private static final int FLAGS_ORGANIZER_INTERESTED = FLAG_IS_FROM_LETTERBOX_DOUBLE_TAP | FLAG_ELIGIBLE_FOR_USER_ASPECT_RATIO_BUTTON | FLAG_FULLSCREEN_OVERRIDE_SYSTEM | FLAG_FULLSCREEN_OVERRIDE_USER | FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE - | FLAG_OPT_OUT_EDGE_TO_EDGE; + | FLAG_OPT_OUT_EDGE_TO_EDGE | FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE; @TopActivityFlag private static final int FLAGS_COMPAT_UI_INTERESTED = FLAGS_ORGANIZER_INTERESTED @@ -179,7 +179,8 @@ public class AppCompatTaskInfo implements Parcelable { */ public boolean hasCompatUI() { return isTopActivityInSizeCompat() || eligibleForLetterboxEducation() - || isLetterboxDoubleTapEnabled() || eligibleForUserAspectRatioButton(); + || isLetterboxDoubleTapEnabled() || eligibleForUserAspectRatioButton() + || isRestartMenuEnabledForDisplayMove(); } /** diff --git a/core/java/android/app/LocaleConfig.java b/core/java/android/app/LocaleConfig.java index f56bf4d434e7..cbfd7fccb7c6 100644 --- a/core/java/android/app/LocaleConfig.java +++ b/core/java/android/app/LocaleConfig.java @@ -144,15 +144,12 @@ public class LocaleConfig implements Parcelable { } } Resources res = context.getResources(); - //Get the resource id int resId = context.getApplicationInfo().getLocaleConfigRes(); if (resId == 0) { mStatus = STATUS_NOT_SPECIFIED; return; } - try { - //Get the parser to read XML data - XmlResourceParser parser = res.getXml(resId); + try (XmlResourceParser parser = res.getXml(resId)) { parseLocaleConfig(parser, res); } catch (Resources.NotFoundException e) { Slog.w(TAG, "The resource file pointed to by the given resource ID isn't found."); @@ -208,22 +205,22 @@ public class LocaleConfig implements Parcelable { String defaultLocale = null; if (android.content.res.Flags.defaultLocale()) { // Read the defaultLocale attribute of the LocaleConfig element - TypedArray att = res.obtainAttributes( - attrs, com.android.internal.R.styleable.LocaleConfig); - defaultLocale = att.getString( - R.styleable.LocaleConfig_defaultLocale); - att.recycle(); + try (TypedArray att = res.obtainAttributes( + attrs, com.android.internal.R.styleable.LocaleConfig)) { + defaultLocale = att.getString( + R.styleable.LocaleConfig_defaultLocale); + } } Set<String> localeNames = new HashSet<>(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { if (TAG_LOCALE.equals(parser.getName())) { - final TypedArray attributes = res.obtainAttributes( - attrs, com.android.internal.R.styleable.LocaleConfig_Locale); - String nameAttr = attributes.getString( - com.android.internal.R.styleable.LocaleConfig_Locale_name); - localeNames.add(nameAttr); - attributes.recycle(); + try (TypedArray attributes = res.obtainAttributes( + attrs, com.android.internal.R.styleable.LocaleConfig_Locale)) { + String nameAttr = attributes.getString( + com.android.internal.R.styleable.LocaleConfig_Locale_name); + localeNames.add(nameAttr); + } } else { XmlUtils.skipCurrentTag(parser); } diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index 01868cc601fe..927d46999284 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -58,8 +58,10 @@ import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.IUndoMediaTransferCallback; import com.android.internal.statusbar.NotificationVisibility; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -119,6 +121,7 @@ public class StatusBarManager { | DISABLE_SEARCH | DISABLE_ONGOING_CALL_CHIP; /** @hide */ + @Target(ElementType.TYPE_USE) @IntDef(flag = true, prefix = {"DISABLE_"}, value = { DISABLE_NONE, DISABLE_EXPAND, @@ -161,6 +164,7 @@ public class StatusBarManager { | DISABLE2_NOTIFICATION_SHADE | DISABLE2_GLOBAL_ACTIONS | DISABLE2_ROTATE_SUGGESTIONS; /** @hide */ + @Target(ElementType.TYPE_USE) @IntDef(flag = true, prefix = { "DISABLE2_" }, value = { DISABLE2_NONE, DISABLE2_MASK, diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig index 29c84ee00b44..a26bfa4f586c 100644 --- a/core/java/android/app/activity_manager.aconfig +++ b/core/java/android/app/activity_manager.aconfig @@ -190,3 +190,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enable_process_observer_broadcast_on_process_started" + namespace: "system_performance" + description: "Enable ProcessObserver's onProcessStarted callbacks." + bug: "323959187" +} diff --git a/core/java/android/app/supervision/SupervisionAppService.java b/core/java/android/app/supervision/SupervisionAppService.java index 4530be5c270a..93eb96204444 100644 --- a/core/java/android/app/supervision/SupervisionAppService.java +++ b/core/java/android/app/supervision/SupervisionAppService.java @@ -16,7 +16,11 @@ package android.app.supervision; +import android.annotation.FlaggedApi; +import android.annotation.Nullable; +import android.annotation.SystemApi; import android.app.Service; +import android.app.supervision.flags.Flags; import android.content.Intent; import android.os.IBinder; @@ -26,31 +30,43 @@ import android.os.IBinder; * * @hide */ +@SystemApi +@FlaggedApi(Flags.FLAG_ENABLE_SUPERVISION_APP_SERVICE) public class SupervisionAppService extends Service { - private final ISupervisionAppService mBinder = new ISupervisionAppService.Stub() { - @Override - public void onEnabled() { - SupervisionAppService.this.onEnabled(); - } + private final ISupervisionAppService mBinder = + new ISupervisionAppService.Stub() { + @Override + public void onEnabled() { + SupervisionAppService.this.onEnabled(); + } - @Override - public void onDisabled() { - SupervisionAppService.this.onDisabled(); - } - }; + @Override + public void onDisabled() { + SupervisionAppService.this.onDisabled(); + } + }; + @Nullable @Override - public final IBinder onBind(Intent intent) { + public final IBinder onBind(@Nullable Intent intent) { return mBinder.asBinder(); } /** * Called when supervision is enabled. + * + * @hide */ + @SystemApi + @FlaggedApi(Flags.FLAG_ENABLE_SUPERVISION_APP_SERVICE) public void onEnabled() {} /** * Called when supervision is disabled. + * + * @hide */ + @SystemApi + @FlaggedApi(Flags.FLAG_ENABLE_SUPERVISION_APP_SERVICE) public void onDisabled() {} } diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index 615a6dffdf99..161f05bc5139 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -166,3 +166,10 @@ flag { bug: "393517834" is_exported: true } + +flag { + name: "external_virtual_cameras" + namespace: "virtual_devices" + description: "Allow external virtual cameras visible only in the Context of the virtual device" + bug: "375609768" +} diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index bb62ac321202..a253613e060c 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -680,6 +680,7 @@ public class Intent implements Parcelable, Cloneable { private static final String ATTR_COMPONENT = "component"; private static final String ATTR_DATA = "data"; private static final String ATTR_FLAGS = "flags"; + private static final String ATTR_PACKAGE = "package"; // --------------------------------------------------------------------- // --------------------------------------------------------------------- @@ -12893,6 +12894,9 @@ public class Intent implements Parcelable, Cloneable { if (mComponent != null) { out.attribute(null, ATTR_COMPONENT, mComponent.flattenToShortString()); } + if (android.content.flags.Flags.intentSaveToXmlPackage() && mPackage != null) { + out.attribute(null, ATTR_PACKAGE, mPackage); + } out.attribute(null, ATTR_FLAGS, Integer.toHexString(getFlags())); if (mCategories != null) { @@ -12926,6 +12930,9 @@ public class Intent implements Parcelable, Cloneable { intent.setComponent(ComponentName.unflattenFromString(attrValue)); } else if (ATTR_FLAGS.equals(attrName)) { intent.setFlags(Integer.parseInt(attrValue, 16)); + } else if (android.content.flags.Flags.intentSaveToXmlPackage() + && ATTR_PACKAGE.equals(attrName)) { + intent.setPackage(attrValue); } else { Log.e(TAG, "restoreFromXml: unknown attribute=" + attrName); } diff --git a/core/java/android/content/flags/flags.aconfig b/core/java/android/content/flags/flags.aconfig index aac04b3a9d15..148532b62c36 100644 --- a/core/java/android/content/flags/flags.aconfig +++ b/core/java/android/content/flags/flags.aconfig @@ -7,4 +7,14 @@ flag { namespace: "machine_learning" description: "This flag enables the newly added flag for binding package-private isolated processes." bug: "312706530" -}
\ No newline at end of file +} + +flag { + namespace: "system_performance" + name: "intent_save_to_xml_package" + description: "Add package to saveToXml so save then restore passes filterEquals." + bug: "369856202" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index b1ea6e9b68eb..219b20428d7a 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -2628,15 +2628,6 @@ public class PackageParser { return Build.VERSION_CODES.CUR_DEVELOPMENT; } - // STOPSHIP: hack for the pre-release SDK - if (platformSdkCodenames.length == 0 - && Build.VERSION.KNOWN_CODENAMES.stream().max(String::compareTo).orElse("").equals( - targetCode)) { - Slog.w(TAG, "Package requires development platform " + targetCode - + ", returning current version " + Build.VERSION.SDK_INT); - return Build.VERSION.SDK_INT; - } - // Otherwise, we're looking at an incompatible pre-release SDK. if (platformSdkCodenames.length > 0) { outError[0] = "Requires development platform " + targetCode @@ -2708,15 +2699,6 @@ public class PackageParser { return Build.VERSION_CODES.CUR_DEVELOPMENT; } - // STOPSHIP: hack for the pre-release SDK - if (platformSdkCodenames.length == 0 - && Build.VERSION.KNOWN_CODENAMES.stream().max(String::compareTo).orElse("").equals( - minCode)) { - Slog.w(TAG, "Package requires min development platform " + minCode - + ", returning current version " + Build.VERSION.SDK_INT); - return Build.VERSION.SDK_INT; - } - // Otherwise, we're looking at an incompatible pre-release SDK. if (platformSdkCodenames.length > 0) { outError[0] = "Requires development platform " + minCode diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java index ded35b23608d..1ddab2c86ec2 100644 --- a/core/java/android/content/pm/RegisteredServicesCache.java +++ b/core/java/android/content/pm/RegisteredServicesCache.java @@ -104,14 +104,6 @@ public abstract class RegisteredServicesCache<V> { private final Handler mBackgroundHandler; - private final Runnable mClearServiceInfoCachesRunnable = new Runnable() { - public void run() { - synchronized (mUserIdToServiceInfoCaches) { - mUserIdToServiceInfoCaches.clear(); - } - } - }; - private static class UserServices<V> { @GuardedBy("mServicesLock") final Map<V, Integer> persistentServices = Maps.newHashMap(); @@ -565,9 +557,11 @@ public abstract class RegisteredServicesCache<V> { if (Flags.optimizeParsingInRegisteredServicesCache()) { synchronized (mUserIdToServiceInfoCaches) { - if (mUserIdToServiceInfoCaches.numMaps() > 0) { - mBackgroundHandler.removeCallbacks(mClearServiceInfoCachesRunnable); - mBackgroundHandler.postDelayed(mClearServiceInfoCachesRunnable, + if (mUserIdToServiceInfoCaches.numElementsForKey(userId) > 0) { + final Integer token = Integer.valueOf(userId); + mBackgroundHandler.removeCallbacksAndEqualMessages(token); + mBackgroundHandler.postDelayed( + new ClearServiceInfoCachesTimeoutRunnable(userId), token, SERVICE_INFO_CACHES_TIMEOUT_MILLIS); } } @@ -953,4 +947,19 @@ public abstract class RegisteredServicesCache<V> { return BackgroundThread.getHandler(); } } + + class ClearServiceInfoCachesTimeoutRunnable implements Runnable { + final int mUserId; + + ClearServiceInfoCachesTimeoutRunnable(int userId) { + this.mUserId = userId; + } + + @Override + public void run() { + synchronized (mUserIdToServiceInfoCaches) { + mUserIdToServiceInfoCaches.delete(mUserId); + } + } + } } diff --git a/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java b/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java index c7403c0ea98c..153dd9a93490 100644 --- a/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java +++ b/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java @@ -316,15 +316,6 @@ public class FrameworkParsingPackageUtils { return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT); } - // STOPSHIP: hack for the pre-release SDK - if (platformSdkCodenames.length == 0 - && Build.VERSION.KNOWN_CODENAMES.stream().max(String::compareTo).orElse("").equals( - minCode)) { - Slog.w(TAG, "Parsed package requires min development platform " + minCode - + ", returning current version " + Build.VERSION.SDK_INT); - return input.success(Build.VERSION.SDK_INT); - } - // Otherwise, we're looking at an incompatible pre-release SDK. if (platformSdkCodenames.length > 0) { return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, @@ -377,27 +368,19 @@ public class FrameworkParsingPackageUtils { return input.success(targetVers); } - // If it's a pre-release SDK and the codename matches this platform, it - // definitely targets this SDK. - if (matchTargetCode(platformSdkCodenames, targetCode)) { - return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT); - } - - // STOPSHIP: hack for the pre-release SDK - if (platformSdkCodenames.length == 0 - && Build.VERSION.KNOWN_CODENAMES.stream().max(String::compareTo).orElse("").equals( - targetCode)) { - Slog.w(TAG, "Parsed package requires development platform " + targetCode - + ", returning current version " + Build.VERSION.SDK_INT); - return input.success(Build.VERSION.SDK_INT); - } - try { if (allowUnknownCodenames && UnboundedSdkLevel.isAtMost(targetCode)) { return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT); } } catch (IllegalArgumentException e) { - return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, "Bad package SDK"); + // isAtMost() throws it when encountering an older SDK codename + return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, e.getMessage()); + } + + // If it's a pre-release SDK and the codename matches this platform, it + // definitely targets this SDK. + if (matchTargetCode(platformSdkCodenames, targetCode)) { + return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT); } // Otherwise, we're looking at an incompatible pre-release SDK. diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig index 4815f3e4f524..061e6f44c9c7 100644 --- a/core/java/android/hardware/biometrics/flags.aconfig +++ b/core/java/android/hardware/biometrics/flags.aconfig @@ -64,3 +64,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "move_fm_api_to_bm" + is_exported: true + namespace: "biometrics_framework" + description: "Feature flag for moving some FingerprintManager APIs to BiometricManager to unblock FM removal." + bug: "323957939" +} diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 1c2150f3c09f..5537135f7bfa 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -273,7 +273,7 @@ interface IInputManager { @PermissionManuallyEnforced @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.MANAGE_KEY_GESTURES)") - void registerKeyGestureHandler(IKeyGestureHandler handler); + void registerKeyGestureHandler(in int[] keyGesturesToHandle, IKeyGestureHandler handler); @PermissionManuallyEnforced @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " diff --git a/core/java/android/hardware/input/IKeyGestureHandler.aidl b/core/java/android/hardware/input/IKeyGestureHandler.aidl index 4da991ee85b1..08b015892710 100644 --- a/core/java/android/hardware/input/IKeyGestureHandler.aidl +++ b/core/java/android/hardware/input/IKeyGestureHandler.aidl @@ -20,12 +20,12 @@ import android.hardware.input.AidlKeyGestureEvent; import android.os.IBinder; /** @hide */ -interface IKeyGestureHandler { +oneway interface IKeyGestureHandler { /** - * Called when a key gesture starts, ends, or is cancelled. If a handler returns {@code true}, - * it means they intend to handle the full gesture and should handle all the events pertaining - * to that gesture. + * Called when a key gesture starts, ends, or is cancelled. It is only sent to the handler that + * registered the callback for that particular gesture type. + * {@see IInputManager#registerKeyGestureHandler(int[], IKeyGestureHandler)} */ - boolean handleKeyGesture(in AidlKeyGestureEvent event, in IBinder focusedToken); + void handleKeyGesture(in AidlKeyGestureEvent event, in IBinder focusedToken); } diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index d6419afb2a5a..a66ac76d7597 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -1446,16 +1446,18 @@ public final class InputManager { /** * Registers a key gesture event handler for {@link KeyGestureEvent} handling. * + * @param keyGesturesToHandle list of KeyGestureTypes to listen to * @param handler the {@link KeyGestureEventHandler} - * @throws IllegalArgumentException if {@code handler} has already been registered previously. + * @throws IllegalArgumentException if {@code handler} has already been registered previously + * or key gestures provided are already registered by some other gesture handler. * @throws NullPointerException if {@code handler} or {@code executor} is null. * @hide * @see #unregisterKeyGestureEventHandler(KeyGestureEventHandler) */ @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES) - public void registerKeyGestureEventHandler(@NonNull KeyGestureEventHandler handler) - throws IllegalArgumentException { - mGlobal.registerKeyGestureEventHandler(handler); + public void registerKeyGestureEventHandler(List<Integer> keyGesturesToHandle, + @NonNull KeyGestureEventHandler handler) throws IllegalArgumentException { + mGlobal.registerKeyGestureEventHandler(keyGesturesToHandle, handler); } /** @@ -1463,7 +1465,7 @@ public final class InputManager { * * @param handler the {@link KeyGestureEventHandler} * @hide - * @see #registerKeyGestureEventHandler(KeyGestureEventHandler) + * @see #registerKeyGestureEventHandler(List, KeyGestureEventHandler) */ @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES) public void unregisterKeyGestureEventHandler(@NonNull KeyGestureEventHandler handler) { @@ -1741,7 +1743,7 @@ public final class InputManager { * {@see KeyGestureEventListener} which is to listen to successfully handled key gestures, this * interface allows system components to register handler for handling key gestures. * - * @see #registerKeyGestureEventHandler(KeyGestureEventHandler) + * @see #registerKeyGestureEventHandler(List, KeyGestureEventHandler) * @see #unregisterKeyGestureEventHandler(KeyGestureEventHandler) * * <p> NOTE: All callbacks will occur on system main and input threads, so the caller needs @@ -1750,14 +1752,11 @@ public final class InputManager { */ public interface KeyGestureEventHandler { /** - * Called when a key gesture event starts, is completed, or is cancelled. If a handler - * returns {@code true}, it implies that the handler intends to handle the key gesture and - * only this handler will receive the future events for this key gesture. + * Called when a key gesture event starts, is completed, or is cancelled. * * @param event the gesture event */ - boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event, - @Nullable IBinder focusedToken); + void handleKeyGestureEvent(@NonNull KeyGestureEvent event, @Nullable IBinder focusedToken); } /** @hide */ diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java index c4b4831ba76e..754182ce3d11 100644 --- a/core/java/android/hardware/input/InputManagerGlobal.java +++ b/core/java/android/hardware/input/InputManagerGlobal.java @@ -25,8 +25,8 @@ import android.hardware.BatteryState; import android.hardware.SensorManager; import android.hardware.input.InputManager.InputDeviceBatteryListener; import android.hardware.input.InputManager.InputDeviceListener; -import android.hardware.input.InputManager.KeyGestureEventHandler; import android.hardware.input.InputManager.KeyEventActivityListener; +import android.hardware.input.InputManager.KeyGestureEventHandler; import android.hardware.input.InputManager.KeyGestureEventListener; import android.hardware.input.InputManager.KeyboardBacklightListener; import android.hardware.input.InputManager.OnTabletModeChangedListener; @@ -49,6 +49,7 @@ import android.os.ServiceManager; import android.os.VibrationEffect; import android.os.Vibrator; import android.os.VibratorManager; +import android.util.IntArray; import android.util.Log; import android.util.SparseArray; import android.view.Display; @@ -132,13 +133,13 @@ public final class InputManagerGlobal { @Nullable private IKeyEventActivityListener mKeyEventActivityListener; - private final Object mKeyGestureEventHandlerLock = new Object(); - @GuardedBy("mKeyGestureEventHandlerLock") - @Nullable - private ArrayList<KeyGestureEventHandler> mKeyGestureEventHandlers; - @GuardedBy("mKeyGestureEventHandlerLock") + @GuardedBy("mKeyGesturesToHandlerMap") @Nullable private IKeyGestureHandler mKeyGestureHandler; + @GuardedBy("mKeyGesturesToHandlerMap") + private final SparseArray<KeyGestureEventHandler> mKeyGesturesToHandlerMap = + new SparseArray<>(); + // InputDeviceSensorManager gets notified synchronously from the binder thread when input // devices change, so it must be synchronized with the input device listeners. @@ -1177,50 +1178,69 @@ public final class InputManagerGlobal { private class LocalKeyGestureHandler extends IKeyGestureHandler.Stub { @Override - public boolean handleKeyGesture(@NonNull AidlKeyGestureEvent ev, IBinder focusedToken) { - synchronized (mKeyGestureEventHandlerLock) { - if (mKeyGestureEventHandlers == null) { - return false; - } - final int numHandlers = mKeyGestureEventHandlers.size(); - final KeyGestureEvent event = new KeyGestureEvent(ev); - for (int i = 0; i < numHandlers; i++) { - KeyGestureEventHandler handler = mKeyGestureEventHandlers.get(i); - if (handler.handleKeyGestureEvent(event, focusedToken)) { - return true; - } + public void handleKeyGesture(@NonNull AidlKeyGestureEvent ev, IBinder focusedToken) { + synchronized (mKeyGesturesToHandlerMap) { + KeyGestureEventHandler handler = mKeyGesturesToHandlerMap.get(ev.gestureType); + if (handler == null) { + Log.w(TAG, "Key gesture event " + ev.gestureType + + " occurred without a registered handler!"); + return; } + handler.handleKeyGestureEvent(new KeyGestureEvent(ev), focusedToken); } - return false; } } /** - * @see InputManager#registerKeyGestureEventHandler(KeyGestureEventHandler) + * @see InputManager#registerKeyGestureEventHandler(List, KeyGestureEventHandler) */ @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES) - void registerKeyGestureEventHandler(@NonNull KeyGestureEventHandler handler) - throws IllegalArgumentException { + void registerKeyGestureEventHandler(List<Integer> keyGesturesToHandle, + @NonNull KeyGestureEventHandler handler) throws IllegalArgumentException { + Objects.requireNonNull(keyGesturesToHandle, "List of gestures should not be null"); Objects.requireNonNull(handler, "handler should not be null"); - synchronized (mKeyGestureEventHandlerLock) { - if (mKeyGestureHandler == null) { - mKeyGestureEventHandlers = new ArrayList<>(); - mKeyGestureHandler = new LocalKeyGestureHandler(); + if (keyGesturesToHandle.isEmpty()) { + throw new IllegalArgumentException("No key gestures provided!"); + } - try { - mIm.registerKeyGestureHandler(mKeyGestureHandler); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + synchronized (mKeyGesturesToHandlerMap) { + IntArray newKeyGestures = new IntArray( + keyGesturesToHandle.size() + mKeyGesturesToHandlerMap.size()); + + // Check if the handler already exists + for (int i = 0; i < mKeyGesturesToHandlerMap.size(); i++) { + KeyGestureEventHandler h = mKeyGesturesToHandlerMap.valueAt(i); + if (h == handler) { + throw new IllegalArgumentException("Handler has already been registered!"); } + newKeyGestures.add(mKeyGesturesToHandlerMap.keyAt(i)); } - final int numHandlers = mKeyGestureEventHandlers.size(); - for (int i = 0; i < numHandlers; i++) { - if (mKeyGestureEventHandlers.get(i) == handler) { - throw new IllegalArgumentException("Handler has already been registered!"); + + // Check if any of the key gestures are already handled by existing handlers + for (int gesture : keyGesturesToHandle) { + if (mKeyGesturesToHandlerMap.contains(gesture)) { + throw new IllegalArgumentException("Key gesture " + gesture + + " is already registered by another handler!"); + } + newKeyGestures.add(gesture); + } + + try { + // If handler was already registered for this process, we need to unregister and + // re-register it for the new set of gestures + if (mKeyGestureHandler != null) { + mIm.unregisterKeyGestureHandler(mKeyGestureHandler); + } else { + mKeyGestureHandler = new LocalKeyGestureHandler(); + } + mIm.registerKeyGestureHandler(newKeyGestures.toArray(), mKeyGestureHandler); + for (int gesture : keyGesturesToHandle) { + mKeyGesturesToHandlerMap.put(gesture, handler); } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } - mKeyGestureEventHandlers.add(handler); } } @@ -1231,18 +1251,21 @@ public final class InputManagerGlobal { void unregisterKeyGestureEventHandler(@NonNull KeyGestureEventHandler handler) { Objects.requireNonNull(handler, "handler should not be null"); - synchronized (mKeyGestureEventHandlerLock) { - if (mKeyGestureEventHandlers == null) { + synchronized (mKeyGesturesToHandlerMap) { + if (mKeyGestureHandler == null) { return; } - mKeyGestureEventHandlers.removeIf(existingHandler -> existingHandler == handler); - if (mKeyGestureEventHandlers.isEmpty()) { + for (int i = mKeyGesturesToHandlerMap.size() - 1; i >= 0; i--) { + if (mKeyGesturesToHandlerMap.valueAt(i) == handler) { + mKeyGesturesToHandlerMap.removeAt(i); + } + } + if (mKeyGesturesToHandlerMap.size() == 0) { try { mIm.unregisterKeyGestureHandler(mKeyGestureHandler); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } - mKeyGestureEventHandlers = null; mKeyGestureHandler = null; } } diff --git a/core/java/android/net/VpnManager.java b/core/java/android/net/VpnManager.java index c50bc569de72..4ef293a90a80 100644 --- a/core/java/android/net/VpnManager.java +++ b/core/java/android/net/VpnManager.java @@ -20,6 +20,7 @@ import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; import static com.android.internal.util.Preconditions.checkNotNull; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -32,6 +33,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.res.Resources; +import android.net.platform.flags.Flags; import android.os.RemoteException; import com.android.internal.net.LegacyVpnInfo; @@ -85,13 +87,33 @@ public class VpnManager { public static final int TYPE_VPN_LEGACY = 3; /** - * An VPN created by OEM code through other means than {@link VpnService} or {@link VpnManager}. + * A VPN created by OEM code through other means than {@link VpnService} or {@link VpnManager}. * @hide */ @SystemApi(client = MODULE_LIBRARIES) public static final int TYPE_VPN_OEM = 4; /** + * A VPN created by OEM code using {@link VpnService}, and which OEM code desires to + * differentiate from other VPN types. The core networking stack will treat this VPN type + * similarly to {@link #TYPE_VPN_SERVICE}. + * @hide + */ + @FlaggedApi(Flags.FLAG_VPN_TYPE_OEM_SERVICE_AND_LEGACY) + @SystemApi(client = MODULE_LIBRARIES) + public static final int TYPE_VPN_OEM_SERVICE = 5; + + /** + * A VPN created by OEM code using the legacy VPN mechanisms, and which OEM code desires to + * differentiate from other VPN types. The core networking stack will treat this VPN type + * similarly to {@link #TYPE_VPN_LEGACY}. + * @hide + */ + @FlaggedApi(Flags.FLAG_VPN_TYPE_OEM_SERVICE_AND_LEGACY) + @SystemApi(client = MODULE_LIBRARIES) + public static final int TYPE_VPN_OEM_LEGACY = 6; + + /** * Channel for VPN notifications. * @hide */ @@ -308,7 +330,7 @@ public class VpnManager { /** @hide */ @IntDef(value = {TYPE_VPN_NONE, TYPE_VPN_SERVICE, TYPE_VPN_PLATFORM, TYPE_VPN_LEGACY, - TYPE_VPN_OEM}) + TYPE_VPN_OEM, TYPE_VPN_OEM_SERVICE, TYPE_VPN_OEM_LEGACY}) @Retention(RetentionPolicy.SOURCE) public @interface VpnType {} diff --git a/core/java/android/net/flags.aconfig b/core/java/android/net/flags.aconfig index 8d12b76e23ff..519729bc1c88 100644 --- a/core/java/android/net/flags.aconfig +++ b/core/java/android/net/flags.aconfig @@ -37,3 +37,11 @@ flag { description: "Flag for MDNS quality, reliability and performance improvement in 25Q2" bug: "373270045" } + +flag { + name: "vpn_type_oem_service_and_legacy" + namespace: "android_core_networking" + is_exported: false + description: "Flags the TYPE_VPN_OEM_SERVICE and TYPE_VPN_OEM_LEGACY VpnManager API constants" + bug: "389829981" +} diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java index c3ec96d17437..c21959b16fbb 100644 --- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java +++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java @@ -144,6 +144,12 @@ public final class MessageQueue { return; } + // Holdback study. + if (Flags.messageQueueForceLegacy()) { + sIsProcessAllowedToUseConcurrent = false; + return; + } + if (Flags.forceConcurrentMessageQueue()) { // b/379472827: Robolectric tests use reflection to access MessageQueue.mMessages. // This is a hack to allow Robolectric tests to use the legacy implementation. diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index 0150d171d51c..b52a454ea956 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -4,6 +4,15 @@ container: "system" # keep-sorted start block=yes newline_separated=yes flag { + # Holdback study for concurrent MessageQueue. + # Do not promote beyond trunkfood. + namespace: "system_performance" + name: "message_queue_force_legacy" + description: "Whether to holdback concurrent MessageQueue (force legacy)." + bug: "336880969" +} + +flag { name: "adpf_gpu_report_actual_work_duration" is_exported: true namespace: "game" diff --git a/core/java/android/os/image/flags/trade_in_mode_flags.aconfig b/core/java/android/os/image/flags/trade_in_mode_flags.aconfig index e2e56ef70d62..c1adfe50db9e 100644 --- a/core/java/android/os/image/flags/trade_in_mode_flags.aconfig +++ b/core/java/android/os/image/flags/trade_in_mode_flags.aconfig @@ -9,3 +9,12 @@ flag { bug: "332683751" is_fixed_read_only: true } + +flag { + name: "trade_in_mode_2025q4" + is_exported: true + namespace: "phoenix" + description: "Enable Trade-in Mode 2025Q4 changes" + bug: "397154502" + is_fixed_read_only: true +} diff --git a/core/java/android/util/ArrayMap.java b/core/java/android/util/ArrayMap.java index 7ee0ff15c5ad..c59907937d6a 100644 --- a/core/java/android/util/ArrayMap.java +++ b/core/java/android/util/ArrayMap.java @@ -129,7 +129,7 @@ public final class ArrayMap<K, V> implements Map<K, V> { return ContainerHelpers.binarySearch(hashes, N, hash); } catch (ArrayIndexOutOfBoundsException e) { if (CONCURRENT_MODIFICATION_EXCEPTIONS) { - throw new ConcurrentModificationException(); + throw new ConcurrentModificationException(e); } else { throw e; // the cache is poisoned at this point, there's not much we can do } diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 7e9dfe6d972a..4c578fb93600 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -2039,8 +2039,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } else if (Flags.refactorInsetsController()) { if ((typesToReport & ime()) != 0 && mImeSourceConsumer != null) { InsetsSourceControl control = mImeSourceConsumer.getControl(); - if (control != null && control.getLeash() == null) { - // If the IME was requested twice, and we didn't receive the controls + if (control == null || control.getLeash() == null) { + // If the IME was requested to show twice, and we didn't receive the controls // yet, this request will not continue. It should be cancelled here, as // it would time out otherwise. ImeTracker.forLogging().onCancelled(statsToken, diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index 624216776f42..b97f28da7559 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -329,13 +329,17 @@ 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. + * current value is provided immediately using the provided {@link Executor}. If this + * {@link Consumer} was registered previously, then this is a no op. */ public void addWindowViewsListener(@NonNull Executor executor, @NonNull Consumer<List<View>> consumer) { synchronized (mLock) { + if (mWindowViewsListenerGroup.isConsumerPresent(consumer)) { + return; + } mWindowViewsListenerGroup.addListener(executor, consumer); - mWindowViewsListenerGroup.accept(getWindowViews()); + executor.execute(() -> consumer.accept(getWindowViews())); } } diff --git a/core/java/android/view/inspector/WindowInspector.java b/core/java/android/view/inspector/WindowInspector.java index 3ebca3c9d9b6..f0cc01133e07 100644 --- a/core/java/android/view/inspector/WindowInspector.java +++ b/core/java/android/view/inspector/WindowInspector.java @@ -42,8 +42,9 @@ public final class WindowInspector { } /** - * 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. + * Adds a listener that is notified whenever the value of {@link #getGlobalWindowViews()} + * changes. The current value is provided immediately using the provided {@link Executor}. + * If this {@link Consumer} is already registered, then this method is a no op. * @see #getGlobalWindowViews() */ @FlaggedApi(android.view.flags.Flags.FLAG_ROOT_VIEW_CHANGED_LISTENER) diff --git a/core/java/android/view/translation/ListenerGroup.java b/core/java/android/view/translation/ListenerGroup.java index bf506815f841..5c70805042fa 100644 --- a/core/java/android/view/translation/ListenerGroup.java +++ b/core/java/android/view/translation/ListenerGroup.java @@ -48,7 +48,7 @@ public class ListenerGroup<T> { * is a no op. */ public void addListener(@NonNull Executor executor, @NonNull Consumer<T> consumer) { - if (isContained(consumer)) { + if (isConsumerPresent(consumer)) { return; } mListeners.add(new ListenerWrapper<>(executor, consumer)); @@ -69,7 +69,7 @@ public class ListenerGroup<T> { * Returns {@code true} if the {@link Consumer} is present in the list, {@code false} * otherwise. */ - private boolean isContained(Consumer<T> consumer) { + public boolean isConsumerPresent(Consumer<T> consumer) { return computeIndex(consumer) > -1; } diff --git a/core/java/android/widget/DatePickerCalendarDelegate.java b/core/java/android/widget/DatePickerCalendarDelegate.java index 536b81f77174..4f7eef007a68 100644 --- a/core/java/android/widget/DatePickerCalendarDelegate.java +++ b/core/java/android/widget/DatePickerCalendarDelegate.java @@ -34,6 +34,7 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.DayPickerView.OnDaySelectedListener; import android.widget.YearPickerView.OnYearSelectedListener; @@ -76,10 +77,6 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { private DayPickerView mDayPickerView; private YearPickerView mYearPickerView; - // Accessibility strings. - private String mSelectDay; - private String mSelectYear; - private int mCurrentView = UNINITIALIZED; private final Calendar mTempDate; @@ -118,8 +115,15 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { final ViewGroup header = mContainer.findViewById(R.id.date_picker_header); mHeaderYear = header.findViewById(R.id.date_picker_header_year); mHeaderYear.setOnClickListener(mOnHeaderClickListener); + mHeaderYear.setAccessibilityDelegate( + new ClickActionDelegate(context, R.string.select_year)); + mHeaderYear.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); + mHeaderMonthDay = header.findViewById(R.id.date_picker_header_date); mHeaderMonthDay.setOnClickListener(mOnHeaderClickListener); + mHeaderMonthDay.setAccessibilityDelegate( + new ClickActionDelegate(context, R.string.select_day)); + mHeaderMonthDay.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); // For the sake of backwards compatibility, attempt to extract the text // color from the header month text appearance. If it's set, we'll let @@ -170,10 +174,6 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { mYearPickerView.setYear(mCurrentDate.get(Calendar.YEAR)); mYearPickerView.setOnYearSelectedListener(mOnYearSelectedListener); - // Set up content descriptions. - mSelectDay = res.getString(R.string.select_day); - mSelectYear = res.getString(R.string.select_year); - // Initialize for current locale. This also initializes the date, so no // need to call onDateChanged. onLocaleChanged(mCurrentLocale); @@ -230,6 +230,22 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { return srcRgb | (dstAlpha << 24); } + private static class ClickActionDelegate extends View.AccessibilityDelegate { + private final AccessibilityNodeInfo.AccessibilityAction mClickAction; + + ClickActionDelegate(Context context, int resId) { + mClickAction = new AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfo.ACTION_CLICK, context.getString(resId)); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + + info.addAction(mClickAction); + } + } + /** * Listener called when the user selects a day in the day picker view. */ @@ -310,10 +326,10 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { mYearFormat = DateFormat.getInstanceForSkeleton("y", locale); // Update the header text. - onCurrentDateChanged(false); + onCurrentDateChanged(); } - private void onCurrentDateChanged(boolean announce) { + private void onCurrentDateChanged() { if (mHeaderYear == null) { // Abort, we haven't initialized yet. This method will get called // again later after everything has been set up. @@ -325,11 +341,6 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { final String monthDay = mMonthDayFormat.format(mCurrentDate.getTime()); mHeaderMonthDay.setText(monthDay); - - // TODO: This should use live regions. - if (announce) { - mAnimator.announceForAccessibility(getFormattedCurrentDate()); - } } private void setCurrentView(final int viewIndex) { @@ -343,8 +354,6 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { mAnimator.setDisplayedChild(VIEW_MONTH_DAY); mCurrentView = viewIndex; } - - mAnimator.announceForAccessibility(mSelectDay); break; case VIEW_YEAR: final int year = mCurrentDate.get(Calendar.YEAR); @@ -364,7 +373,6 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { mCurrentView = viewIndex; } - mAnimator.announceForAccessibility(mSelectYear); break; } } @@ -409,7 +417,7 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { mDayPickerView.setDate(mCurrentDate.getTimeInMillis()); mYearPickerView.setYear(year); - onCurrentDateChanged(fromUser); + onCurrentDateChanged(); if (fromUser) { tryVibrate(); @@ -564,7 +572,7 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { mMinDate.setTimeInMillis(ss.getMinDate()); mMaxDate.setTimeInMillis(ss.getMaxDate()); - onCurrentDateChanged(false); + onCurrentDateChanged(); final int currentView = ss.getCurrentView(); setCurrentView(currentView); diff --git a/core/java/android/widget/TimePickerClockDelegate.java b/core/java/android/widget/TimePickerClockDelegate.java index a453c2818566..ebb0f99e988b 100644 --- a/core/java/android/widget/TimePickerClockDelegate.java +++ b/core/java/android/widget/TimePickerClockDelegate.java @@ -121,12 +121,8 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { // Localization data. private boolean mHourFormatShowLeadingZero; - private boolean mHourFormatStartsAtZero; - - // Most recent time announcement values for accessibility. - private CharSequence mLastAnnouncedText; - private boolean mLastAnnouncedIsHour; + private boolean mHourFormatStartsAtZero; public TimePickerClockDelegate(TimePicker delegator, Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(delegator, context); @@ -155,6 +151,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { mHourView.setOnDigitEnteredListener(mDigitEnteredListener); mHourView.setAccessibilityDelegate( new ClickActionDelegate(context, R.string.select_hours)); + mHourView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); mSeparatorView = (TextView) mainView.findViewById(R.id.separator); mMinuteView = (NumericTextView) mainView.findViewById(R.id.minutes); mMinuteView.setOnClickListener(mClickListener); @@ -162,6 +159,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { mMinuteView.setOnDigitEnteredListener(mDigitEnteredListener); mMinuteView.setAccessibilityDelegate( new ClickActionDelegate(context, R.string.select_minutes)); + mMinuteView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); mMinuteView.setRange(0, 59); // Set up AM/PM labels. @@ -435,7 +433,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { private void updateRadialPicker(int index) { mRadialTimePickerView.initialize(mCurrentHour, mCurrentMinute, mIs24Hour); - setCurrentItemShowing(index, false, true); + setCurrentItemShowing(index, false); } private void updateHeaderAmPm() { @@ -786,18 +784,10 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { private void updateHeaderHour(int hourOfDay, boolean announce) { final int localizedHour = getLocalizedHour(hourOfDay); mHourView.setValue(localizedHour); - - if (announce) { - tryAnnounceForAccessibility(mHourView.getText(), true); - } } private void updateHeaderMinute(int minuteOfHour, boolean announce) { mMinuteView.setValue(minuteOfHour); - - if (announce) { - tryAnnounceForAccessibility(mMinuteView.getText(), false); - } } /** @@ -876,31 +866,12 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { return -1; } - private void tryAnnounceForAccessibility(CharSequence text, boolean isHour) { - if (mLastAnnouncedIsHour != isHour || !text.equals(mLastAnnouncedText)) { - // TODO: Find a better solution, potentially live regions? - mDelegator.announceForAccessibility(text); - mLastAnnouncedText = text; - mLastAnnouncedIsHour = isHour; - } - } - /** * Show either Hours or Minutes. */ - private void setCurrentItemShowing(int index, boolean animateCircle, boolean announce) { + private void setCurrentItemShowing(int index, boolean animateCircle) { mRadialTimePickerView.setCurrentItemShowing(index, animateCircle); - if (index == HOUR_INDEX) { - if (announce) { - mDelegator.announceForAccessibility(mSelectHours); - } - } else { - if (announce) { - mDelegator.announceForAccessibility(mSelectMinutes); - } - } - mHourView.setActivated(index == HOUR_INDEX); mMinuteView.setActivated(index == MINUTE_INDEX); } @@ -930,10 +901,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { final boolean isTransition = mAllowAutoAdvance && autoAdvance; setHourInternal(newValue, FROM_RADIAL_PICKER, !isTransition, true); if (isTransition) { - setCurrentItemShowing(MINUTE_INDEX, true, false); - - final int localizedHour = getLocalizedHour(newValue); - mDelegator.announceForAccessibility(localizedHour + ". " + mSelectMinutes); + setCurrentItemShowing(MINUTE_INDEX, true); } break; case RadialTimePickerView.MINUTES: @@ -1030,10 +998,10 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { setAmOrPm(PM); break; case R.id.hours: - setCurrentItemShowing(HOUR_INDEX, true, true); + setCurrentItemShowing(HOUR_INDEX, true); break; case R.id.minutes: - setCurrentItemShowing(MINUTE_INDEX, true, true); + setCurrentItemShowing(MINUTE_INDEX, true); break; default: // Failed to handle this click, don't vibrate. @@ -1058,10 +1026,10 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { setAmOrPm(PM); break; case R.id.hours: - setCurrentItemShowing(HOUR_INDEX, true, true); + setCurrentItemShowing(HOUR_INDEX, true); break; case R.id.minutes: - setCurrentItemShowing(MINUTE_INDEX, true, true); + setCurrentItemShowing(MINUTE_INDEX, true); break; default: // Failed to handle this click, don't vibrate. diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index 703274dd708b..4bd0d97a54b0 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -44,7 +44,7 @@ public enum DesktopModeFlags { // All desktop mode related flags to be overridden by developer option toggle will be added here // go/keep-sorted start DISABLE_DESKTOP_LAUNCH_PARAMS_OUTSIDE_DESKTOP_BUG_FIX( - Flags::disableDesktopLaunchParamsOutsideDesktopBugFix, false), + Flags::disableDesktopLaunchParamsOutsideDesktopBugFix, true), DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE(Flags::disableNonResizableAppSnapResizing, true), ENABLE_ACCESSIBLE_CUSTOM_HEADERS(Flags::enableAccessibleCustomHeaders, true), ENABLE_APP_HEADER_WITH_TASK_DENSITY(Flags::enableAppHeaderWithTaskDensity, true), @@ -103,7 +103,7 @@ public enum DesktopModeFlags { ENABLE_DESKTOP_WINDOWING_TASK_LIMIT(Flags::enableDesktopWindowingTaskLimit, true), ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY(Flags::enableDesktopWindowingWallpaperActivity, true), - ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD(Flags::enableDragResizeSetUpInBgThread, false), + ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD(Flags::enableDragResizeSetUpInBgThread, true), ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX( Flags::enableDragToDesktopIncomingTransitionsBugfix, false), ENABLE_FULLY_IMMERSIVE_IN_DESKTOP(Flags::enableFullyImmersiveInDesktop, true), @@ -115,11 +115,12 @@ public enum DesktopModeFlags { ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS( Flags::enableOpaqueBackgroundForTransparentWindows, true), ENABLE_QUICKSWITCH_DESKTOP_SPLIT_BUGFIX(Flags::enableQuickswitchDesktopSplitBugfix, true), + ENABLE_REQUEST_FULLSCREEN_BUGFIX(Flags::enableRequestFullscreenBugfix, false), ENABLE_RESIZING_METRICS(Flags::enableResizingMetrics, true), ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE( Flags::enableRestoreToPreviousSizeFromDesktopImmersive, true), ENABLE_SHELL_INITIAL_BOUNDS_REGRESSION_BUG_FIX( - Flags::enableShellInitialBoundsRegressionBugFix, false), + Flags::enableShellInitialBoundsRegressionBugFix, true), ENABLE_START_LAUNCH_TRANSITION_FROM_TASKBAR_BUGFIX( Flags::enableStartLaunchTransitionFromTaskbarBugfix, true), ENABLE_TASKBAR_OVERFLOW(Flags::enableTaskbarOverflow, false), diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index e4142a171669..0d87b73a5e03 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -971,6 +971,16 @@ flag { } flag { + name: "enable_request_fullscreen_bugfix" + namespace: "lse_desktop_experience" + description: "Fixes split to fullscreen restoration using the Activity#requestFullscreenMode API" + bug: "402973271" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_dynamic_radius_computation_bugfix" namespace: "lse_desktop_experience" description: "Enables bugfix to compute the corner/shadow radius of desktop windows dynamically with the current window context." @@ -986,3 +996,13 @@ flag { description: "Enables the home to be shown behind the desktop." bug: "375644149" } + +flag { + name: "enable_desktop_ime_bugfix" + namespace: "lse_desktop_experience" + description: "Enables bugfix to handle IME interactions in desktop windowing." + bug: "388570293" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/window/flags/responsible_apis.aconfig b/core/java/android/window/flags/responsible_apis.aconfig index 36219812c002..7039add0b179 100644 --- a/core/java/android/window/flags/responsible_apis.aconfig +++ b/core/java/android/window/flags/responsible_apis.aconfig @@ -88,3 +88,10 @@ flag { description: "Clear the allowlist duration when clearAllowBgActivityStarts is called" bug: "322159724" } + +flag { + name: "bal_additional_logging" + namespace: "responsible_apis" + description: "Enable additional logging." + bug: "403398176" +} diff --git a/core/java/com/android/internal/app/AssistUtils.java b/core/java/com/android/internal/app/AssistUtils.java index 4261a0f14767..dd9cf9d7718e 100644 --- a/core/java/com/android/internal/app/AssistUtils.java +++ b/core/java/com/android/internal/app/AssistUtils.java @@ -61,6 +61,8 @@ public class AssistUtils { public static final int INVOCATION_TYPE_ASSIST_BUTTON = 7; /** value for INVOCATION_TYPE_KEY: long press on nav handle */ public static final int INVOCATION_TYPE_NAV_HANDLE_LONG_PRESS = 8; + /** value for INVOCATION_TYPE_KEY: sysui launcher */ + public static final int INVOCATION_TYPE_LAUNCHER_SYSTEM_SHORTCUT = 9; private final Context mContext; private final IVoiceInteractionManagerService mVoiceInteractionManagerService; diff --git a/core/java/com/android/internal/app/MediaRouteDialogPresenter.java b/core/java/com/android/internal/app/MediaRouteDialogPresenter.java index 5628b7ed9d15..76b289416076 100644 --- a/core/java/com/android/internal/app/MediaRouteDialogPresenter.java +++ b/core/java/com/android/internal/app/MediaRouteDialogPresenter.java @@ -86,10 +86,7 @@ public abstract class MediaRouteDialogPresenter { public static Dialog createDialog(Context context, int routeTypes, View.OnClickListener extendedSettingsClickListener, int theme, boolean showProgressBarWhenEmpty) { - final MediaRouter router = context.getSystemService(MediaRouter.class); - - MediaRouter.RouteInfo route = router.getSelectedRoute(); - if (route.isDefault() || !route.matchesTypes(routeTypes)) { + if (shouldShowChooserDialog(context, routeTypes)) { final MediaRouteChooserDialog d = new MediaRouteChooserDialog(context, theme, showProgressBarWhenEmpty); d.setRouteTypes(routeTypes); @@ -99,4 +96,11 @@ public abstract class MediaRouteDialogPresenter { return new MediaRouteControllerDialog(context, theme); } } + + /** Whether we should show the chooser dialog or the controller dialog.. */ + public static boolean shouldShowChooserDialog(Context context, int routeTypes) { + final MediaRouter router = context.getSystemService(MediaRouter.class); + MediaRouter.RouteInfo route = router.getSelectedRoute(); + return route.isDefault() || !route.matchesTypes(routeTypes); + } } diff --git a/core/java/com/android/internal/statusbar/DisableStates.aidl b/core/java/com/android/internal/statusbar/DisableStates.aidl new file mode 100644 index 000000000000..fd9882f0f7c2 --- /dev/null +++ b/core/java/com/android/internal/statusbar/DisableStates.aidl @@ -0,0 +1,19 @@ +/* + * 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.internal.statusbar; + +parcelable DisableStates; diff --git a/core/java/com/android/internal/statusbar/DisableStates.java b/core/java/com/android/internal/statusbar/DisableStates.java new file mode 100644 index 000000000000..ca2fd6c03558 --- /dev/null +++ b/core/java/com/android/internal/statusbar/DisableStates.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.statusbar; + +import android.app.StatusBarManager.Disable2Flags; +import android.app.StatusBarManager.DisableFlags; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Pair; + +import java.util.HashMap; +import java.util.Map; + +/** + * Holds display ids with their disable flags. + */ +public class DisableStates implements Parcelable { + + /** + * A map of display IDs (integers) with corresponding disable flags. + */ + public Map<Integer, Pair<@DisableFlags Integer, @Disable2Flags Integer>> displaysWithStates; + + /** + * Whether the disable state change should be animated. + */ + public boolean animate; + + public DisableStates( + Map<Integer, Pair<@DisableFlags Integer, @Disable2Flags Integer>> displaysWithStates, + boolean animate) { + this.displaysWithStates = displaysWithStates; + this.animate = animate; + } + + public DisableStates( + Map<Integer, Pair<@DisableFlags Integer, @Disable2Flags Integer>> displaysWithStates) { + this(displaysWithStates, true); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(displaysWithStates.size()); // Write the size of the map + for (Map.Entry<Integer, Pair<Integer, Integer>> entry : displaysWithStates.entrySet()) { + dest.writeInt(entry.getKey()); + dest.writeInt(entry.getValue().first); + dest.writeInt(entry.getValue().second); + } + dest.writeBoolean(animate); + } + + /** + * Used to make this class parcelable. + */ + public static final Parcelable.Creator<DisableStates> CREATOR = new Parcelable.Creator<>() { + @Override + public DisableStates createFromParcel(Parcel source) { + int size = source.readInt(); // Read the size of the map + Map<Integer, Pair<Integer, Integer>> displaysWithStates = new HashMap<>(size); + for (int i = 0; i < size; i++) { + int key = source.readInt(); + int first = source.readInt(); + int second = source.readInt(); + displaysWithStates.put(key, new Pair<>(first, second)); + } + final boolean animate = source.readBoolean(); + return new DisableStates(displaysWithStates, animate); + } + + @Override + public DisableStates[] newArray(int size) { + return new DisableStates[size]; + } + }; +} + diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 5a180d7358dd..ce9b036f2fd7 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -32,6 +32,7 @@ import android.os.UserHandle; import android.view.KeyEvent; import android.service.notification.StatusBarNotification; +import com.android.internal.statusbar.DisableStates; import com.android.internal.statusbar.IAddTileResultCallback; import com.android.internal.statusbar.IUndoMediaTransferCallback; import com.android.internal.statusbar.LetterboxDetails; @@ -44,6 +45,7 @@ oneway interface IStatusBar void setIcon(String slot, in StatusBarIcon icon); void removeIcon(String slot); void disable(int displayId, int state1, int state2); + void disableForAllDisplays(in DisableStates disableStates); void animateExpandNotificationsPanel(); void animateExpandSettingsPanel(String subPanel); void animateCollapsePanels(); diff --git a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java index 8fbd10c25995..d62538b6d1ed 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java +++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java @@ -64,16 +64,16 @@ public class CoreDocument implements Serializable { private static final boolean DEBUG = false; // Semantic version - public static final int MAJOR_VERSION = 1; - public static final int MINOR_VERSION = 0; + public static final int MAJOR_VERSION = 0; + public static final int MINOR_VERSION = 4; public static final int PATCH_VERSION = 0; // Internal version level - public static final int DOCUMENT_API_LEVEL = 5; + public static final int DOCUMENT_API_LEVEL = 4; // We also keep a more fine-grained BUILD number, exposed as // ID_API_LEVEL = DOCUMENT_API_LEVEL + BUILD - static final float BUILD = 0.0f; + static final float BUILD = 0.8f; private static final boolean UPDATE_VARIABLES_BEFORE_LAYOUT = false; diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java index a86b62e2caa3..eb7399afd2b7 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java +++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java @@ -1333,7 +1333,7 @@ public class RemoteComposeBuffer { * @return the nan id of float */ public float reserveFloatVariable() { - int id = mRemoteComposeState.nextId(); + int id = mRemoteComposeState.cacheFloat(0f); return Utils.asNan(id); } @@ -2510,8 +2510,8 @@ public class RemoteComposeBuffer { * Add a debug message * * @param textId text id - * @param value - * @param flags + * @param value value + * @param flags flags */ public void addDebugMessage(int textId, float value, int flags) { DebugMessage.apply(mBuffer, textId, value, flags); diff --git a/core/jni/Android.bp b/core/jni/Android.bp index bfa0aa9638a9..7ed73d7668b9 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -210,7 +210,6 @@ cc_library_shared_for_libandroid_runtime { "android_media_AudioAttributes.cpp", "android_media_AudioProductStrategies.cpp", "android_media_AudioVolumeGroups.cpp", - "android_media_AudioVolumeGroupCallback.cpp", "android_media_DeviceCallback.cpp", "android_media_MediaMetricsJNI.cpp", "android_media_MicrophoneInfo.cpp", @@ -311,6 +310,7 @@ cc_library_shared_for_libandroid_runtime { "audioflinger-aidl-cpp", "audiopolicy-types-aidl-cpp", "spatializer-aidl-cpp", + "volumegroupcallback-aidl-cpp", "av-types-aidl-cpp", "android.hardware.camera.device@3.2", "camera_platform_flags_c_lib", diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index b2b826391e1d..1ff07745e904 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -101,7 +101,6 @@ extern int register_android_media_AudioTrack(JNIEnv *env); extern int register_android_media_AudioAttributes(JNIEnv *env); extern int register_android_media_AudioProductStrategies(JNIEnv *env); extern int register_android_media_AudioVolumeGroups(JNIEnv *env); -extern int register_android_media_AudioVolumeGroupChangeHandler(JNIEnv *env); extern int register_android_media_ImageReader(JNIEnv *env); extern int register_android_media_ImageWriter(JNIEnv *env); extern int register_android_media_MicrophoneInfo(JNIEnv *env); @@ -1660,7 +1659,6 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_media_AudioAttributes), REG_JNI(register_android_media_AudioProductStrategies), REG_JNI(register_android_media_AudioVolumeGroups), - REG_JNI(register_android_media_AudioVolumeGroupChangeHandler), REG_JNI(register_android_media_ImageReader), REG_JNI(register_android_media_ImageWriter), REG_JNI(register_android_media_MediaMetrics), diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index b679688959b1..1bbf811dc373 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -20,16 +20,17 @@ #include <atomic> #define LOG_TAG "AudioSystem-JNI" +#include <android-base/properties.h> #include <android/binder_ibinder_jni.h> #include <android/binder_libbinder.h> #include <android/media/AudioVibratorInfo.h> +#include <android/media/INativeAudioVolumeGroupCallback.h> #include <android/media/INativeSpatializerCallback.h> #include <android/media/ISpatializer.h> #include <android/media/audio/common/AudioConfigBase.h> #include <android_media_audiopolicy.h> #include <android_os_Parcel.h> #include <audiomanager/AudioManager.h> -#include <android-base/properties.h> #include <binder/IBinder.h> #include <jni.h> #include <media/AidlConversion.h> @@ -41,14 +42,14 @@ #include <nativehelper/ScopedLocalRef.h> #include <nativehelper/ScopedPrimitiveArray.h> #include <nativehelper/jni_macros.h> +#include <sys/system_properties.h> #include <system/audio.h> #include <system/audio_policy.h> -#include <sys/system_properties.h> #include <utils/Log.h> +#include <memory> #include <optional> #include <sstream> -#include <memory> #include <vector> #include "android_media_AudioAttributes.h" @@ -59,8 +60,8 @@ #include "android_media_AudioFormat.h" #include "android_media_AudioMixerAttributes.h" #include "android_media_AudioProfile.h" -#include "android_media_MicrophoneInfo.h" #include "android_media_JNIUtils.h" +#include "android_media_MicrophoneInfo.h" #include "android_util_Binder.h" #include "core_jni_helpers.h" @@ -3442,6 +3443,21 @@ static void android_media_AudioSystem_triggerSystemPropertyUpdate(JNIEnv *env, } } +static int android_media_AudioSystem_registerAudioVolumeGroupCallback( + JNIEnv *env, jobject thiz, jobject jIAudioVolumeGroupCallback) { + sp<media::INativeAudioVolumeGroupCallback> nIAudioVolumeGroupCallback = + interface_cast<media::INativeAudioVolumeGroupCallback>( + ibinderForJavaObject(env, jIAudioVolumeGroupCallback)); + return AudioSystem::addAudioVolumeGroupCallback(nIAudioVolumeGroupCallback); +} + +static int android_media_AudioSystem_unregisterAudioVolumeGroupCallback( + JNIEnv *env, jobject thiz, jobject jIAudioVolumeGroupCallback) { + sp<media::INativeAudioVolumeGroupCallback> nIAudioVolumeGroupCallback = + interface_cast<media::INativeAudioVolumeGroupCallback>( + ibinderForJavaObject(env, jIAudioVolumeGroupCallback)); + return AudioSystem::removeAudioVolumeGroupCallback(nIAudioVolumeGroupCallback); +} // ---------------------------------------------------------------------------- @@ -3612,6 +3628,12 @@ static const JNINativeMethod gMethods[] = { MAKE_JNI_NATIVE_METHOD("clearPreferredMixerAttributes", "(Landroid/media/AudioAttributes;II)I", android_media_AudioSystem_clearPreferredMixerAttributes), + MAKE_JNI_NATIVE_METHOD("registerAudioVolumeGroupCallback", + "(Landroid/media/INativeAudioVolumeGroupCallback;)I", + android_media_AudioSystem_registerAudioVolumeGroupCallback), + MAKE_JNI_NATIVE_METHOD("unregisterAudioVolumeGroupCallback", + "(Landroid/media/INativeAudioVolumeGroupCallback;)I", + android_media_AudioSystem_unregisterAudioVolumeGroupCallback), MAKE_AUDIO_SYSTEM_METHOD(supportsBluetoothVariableLatency), MAKE_AUDIO_SYSTEM_METHOD(setBluetoothVariableLatencyEnabled), MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled), diff --git a/core/jni/android_media_AudioVolumeGroupCallback.cpp b/core/jni/android_media_AudioVolumeGroupCallback.cpp deleted file mode 100644 index d130a4bc68fa..000000000000 --- a/core/jni/android_media_AudioVolumeGroupCallback.cpp +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#undef ANDROID_UTILS_REF_BASE_DISABLE_IMPLICIT_CONSTRUCTION // TODO:remove this and fix code - -//#define LOG_NDEBUG 0 - -#define LOG_TAG "AudioVolumeGroupCallback-JNI" - -#include <utils/Log.h> -#include <nativehelper/JNIHelp.h> -#include "core_jni_helpers.h" - -#include "android_media_AudioVolumeGroupCallback.h" - - -// ---------------------------------------------------------------------------- -using namespace android; - -static const char* const kAudioVolumeGroupChangeHandlerClassPathName = - "android/media/audiopolicy/AudioVolumeGroupChangeHandler"; - -static struct { - jfieldID mJniCallback; -} gAudioVolumeGroupChangeHandlerFields; - -static struct { - jmethodID postEventFromNative; -} gAudioVolumeGroupChangeHandlerMethods; - -static Mutex gLock; - -JNIAudioVolumeGroupCallback::JNIAudioVolumeGroupCallback(JNIEnv* env, - jobject thiz, - jobject weak_thiz) -{ - jclass clazz = env->GetObjectClass(thiz); - if (clazz == NULL) { - ALOGE("Can't find class %s", kAudioVolumeGroupChangeHandlerClassPathName); - return; - } - mClass = (jclass)env->NewGlobalRef(clazz); - - // We use a weak reference so the AudioVolumeGroupChangeHandler object can be garbage collected. - // The reference is only used as a proxy for callbacks. - mObject = env->NewGlobalRef(weak_thiz); -} - -JNIAudioVolumeGroupCallback::~JNIAudioVolumeGroupCallback() -{ - // remove global references - JNIEnv *env = AndroidRuntime::getJNIEnv(); - if (env == NULL) { - return; - } - env->DeleteGlobalRef(mObject); - env->DeleteGlobalRef(mClass); -} - -void JNIAudioVolumeGroupCallback::onAudioVolumeGroupChanged(volume_group_t group, int flags) -{ - JNIEnv *env = AndroidRuntime::getJNIEnv(); - if (env == NULL) { - return; - } - ALOGV("%s volume group id %d", __FUNCTION__, group); - env->CallStaticVoidMethod(mClass, - gAudioVolumeGroupChangeHandlerMethods.postEventFromNative, - mObject, - AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED, group, flags, NULL); - if (env->ExceptionCheck()) { - ALOGW("An exception occurred while notifying an event."); - env->ExceptionClear(); - } -} - -void JNIAudioVolumeGroupCallback::onServiceDied() -{ - JNIEnv *env = AndroidRuntime::getJNIEnv(); - if (env == NULL) { - return; - } - env->CallStaticVoidMethod(mClass, - gAudioVolumeGroupChangeHandlerMethods.postEventFromNative, - mObject, - AUDIOVOLUMEGROUP_EVENT_SERVICE_DIED, 0, 0, NULL); - if (env->ExceptionCheck()) { - ALOGW("An exception occurred while notifying an event."); - env->ExceptionClear(); - } -} - -static -sp<JNIAudioVolumeGroupCallback> setJniCallback(JNIEnv* env, - jobject thiz, - const sp<JNIAudioVolumeGroupCallback>& callback) -{ - Mutex::Autolock l(gLock); - sp<JNIAudioVolumeGroupCallback> old = (JNIAudioVolumeGroupCallback*)env->GetLongField( - thiz, gAudioVolumeGroupChangeHandlerFields.mJniCallback); - if (callback.get()) { - callback->incStrong((void*)setJniCallback); - } - if (old != 0) { - old->decStrong((void*)setJniCallback); - } - env->SetLongField(thiz, gAudioVolumeGroupChangeHandlerFields.mJniCallback, - (jlong)callback.get()); - return old; -} - -static void -android_media_AudioVolumeGroupChangeHandler_eventHandlerSetup(JNIEnv *env, - jobject thiz, - jobject weak_this) -{ - ALOGV("%s", __FUNCTION__); - sp<JNIAudioVolumeGroupCallback> callback = - new JNIAudioVolumeGroupCallback(env, thiz, weak_this); - - if (AudioSystem::addAudioVolumeGroupCallback(callback) == NO_ERROR) { - setJniCallback(env, thiz, callback); - } -} - -static void -android_media_AudioVolumeGroupChangeHandler_eventHandlerFinalize(JNIEnv *env, jobject thiz) -{ - ALOGV("%s", __FUNCTION__); - sp<JNIAudioVolumeGroupCallback> callback = setJniCallback(env, thiz, 0); - if (callback != 0) { - AudioSystem::removeAudioVolumeGroupCallback(callback); - } -} - -/* - * JNI registration. - */ -static const JNINativeMethod gMethods[] = { - {"native_setup", "(Ljava/lang/Object;)V", - (void *)android_media_AudioVolumeGroupChangeHandler_eventHandlerSetup}, - {"native_finalize", "()V", - (void *)android_media_AudioVolumeGroupChangeHandler_eventHandlerFinalize}, -}; - -int register_android_media_AudioVolumeGroupChangeHandler(JNIEnv *env) -{ - jclass audioVolumeGroupChangeHandlerClass = - FindClassOrDie(env, kAudioVolumeGroupChangeHandlerClassPathName); - gAudioVolumeGroupChangeHandlerMethods.postEventFromNative = - GetStaticMethodIDOrDie(env, audioVolumeGroupChangeHandlerClass, "postEventFromNative", - "(Ljava/lang/Object;IIILjava/lang/Object;)V"); - - gAudioVolumeGroupChangeHandlerFields.mJniCallback = - GetFieldIDOrDie(env, audioVolumeGroupChangeHandlerClass, "mJniCallback", "J"); - - env->DeleteLocalRef(audioVolumeGroupChangeHandlerClass); - - return RegisterMethodsOrDie(env, - kAudioVolumeGroupChangeHandlerClassPathName, - gMethods, - NELEM(gMethods)); -} - diff --git a/core/jni/android_media_AudioVolumeGroupCallback.h b/core/jni/android_media_AudioVolumeGroupCallback.h deleted file mode 100644 index de06549621b9..000000000000 --- a/core/jni/android_media_AudioVolumeGroupCallback.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include <system/audio.h> -#include <media/AudioSystem.h> - -namespace android { - -// keep in sync with AudioManager.AudioVolumeGroupChangeHandler.java -#define AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED 1000 -#define AUDIOVOLUMEGROUP_EVENT_SERVICE_DIED 1001 - -class JNIAudioVolumeGroupCallback: public AudioSystem::AudioVolumeGroupCallback -{ -public: - JNIAudioVolumeGroupCallback(JNIEnv* env, jobject thiz, jobject weak_thiz); - ~JNIAudioVolumeGroupCallback(); - - void onAudioVolumeGroupChanged(volume_group_t group, int flags) override; - void onServiceDied() override; - -private: - void sendEvent(int event); - - jclass mClass; /**< Reference to AudioVolumeGroupChangeHandler class. */ - jobject mObject; /**< Weak ref to AudioVolumeGroupChangeHandler object to call on. */ -}; - -} // namespace android 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 57c89b91cff7..201f46025286 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_base.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml @@ -74,6 +74,7 @@ android:id="@+id/notification_headerless_view_column" android:layout_width="0px" android:layout_height="wrap_content" + android:layout_gravity="center_vertical" android:layout_weight="1" android:layout_marginVertical="@dimen/notification_2025_reduced_margin" android:orientation="vertical" 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 de82f9feb512..e599de12097a 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_media.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml @@ -76,6 +76,7 @@ android:id="@+id/notification_headerless_view_column" android:layout_width="0px" android:layout_height="wrap_content" + android:layout_gravity="center_vertical" android:layout_weight="1" android:layout_marginVertical="@dimen/notification_2025_reduced_margin" android:orientation="vertical" diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index e47adc90fc7a..1a74fe6719e3 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -1219,6 +1219,8 @@ 6 - Lock if keyguard enabled or go to sleep (doze) 7 - Dream if possible or go to sleep (doze) 8 - Go to glanceable hub or dream if possible, or sleep if neither is available (doze) + 9 - Go to dream if device is not dreaming, stop dream if device is dreaming, or sleep if + neither is available (doze) --> <integer name="config_shortPressOnPowerBehavior">1</integer> diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index ef6b9188532e..849ca2882889 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -86,7 +86,7 @@ CarrierConfigManager#KEY_AUTO_DATA_SWITCH_RAT_SIGNAL_SCORE_STRING_ARRAY. If 0, the device always switch to the higher score SIM. If < 0, the network type and signal strength based auto switch is disabled. --> - <integer name="auto_data_switch_score_tolerance">4000</integer> + <integer name="auto_data_switch_score_tolerance">7000</integer> <java-symbol type="integer" name="auto_data_switch_score_tolerance" /> <!-- Boolean indicating whether the Iwlan data service supports persistence of iwlan ipsec @@ -261,7 +261,9 @@ to identify providers that should be ignored if the carrier config carrier_supported_satellite_services_per_provider_bundle does not support them. --> - <string-array name="config_satellite_providers" translatable="false"></string-array> + <string-array name="config_satellite_providers" translatable="false"> + <item>"310830"</item> + </string-array> <java-symbol type="array" name="config_satellite_providers" /> <!-- The identifier of the satellite's SIM profile. The identifier is composed of MCC and MNC diff --git a/core/tests/FileSystemUtilsTest/OWNERS b/core/tests/FileSystemUtilsTest/OWNERS new file mode 100644 index 000000000000..74eeacfeb973 --- /dev/null +++ b/core/tests/FileSystemUtilsTest/OWNERS @@ -0,0 +1,2 @@ +waghpawan@google.com +kaleshsingh@google.com diff --git a/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java b/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java index 208d74e49afe..dbfd3e8ccdaa 100644 --- a/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java +++ b/core/tests/FileSystemUtilsTest/src/com/android/internal/content/FileSystemUtilsTest.java @@ -38,6 +38,8 @@ public class FileSystemUtilsTest extends BaseHostJUnit4Test { private static final String PAGE_SIZE_COMPAT_ENABLED_BY_PLATFORM = "app_with_4kb_elf_no_override.apk"; + private static final int DEVICE_WAIT_TIMEOUT = 120000; + @Test @AppModeFull public void runPunchedApp_embeddedNativeLibs() throws DeviceNotAvailableException { @@ -98,8 +100,20 @@ public class FileSystemUtilsTest extends BaseHostJUnit4Test { @AppModeFull public void runAppWith4KbLib_compatByAlignmentChecks() throws DeviceNotAvailableException, TargetSetupError { + // make sure that device is available for UI test + prepareDevice(); // This test is expected to fail since compat is disabled in manifest runPageSizeCompatTest(PAGE_SIZE_COMPAT_ENABLED_BY_PLATFORM, "testPageSizeCompat_compatByAlignmentChecks"); } + + private void prepareDevice() throws DeviceNotAvailableException { + // Verify that device is online before running test and enable root + getDevice().waitForDeviceAvailable(DEVICE_WAIT_TIMEOUT); + getDevice().enableAdbRoot(); + getDevice().waitForDeviceAvailable(DEVICE_WAIT_TIMEOUT); + + getDevice().executeShellCommand("input keyevent KEYCODE_WAKEUP"); + getDevice().executeShellCommand("wm dismiss-keyguard"); + } } diff --git a/core/tests/coretests/src/android/content/IntentTest.java b/core/tests/coretests/src/android/content/IntentTest.java index fa1948d9786c..1dbe7f5f245b 100644 --- a/core/tests/coretests/src/android/content/IntentTest.java +++ b/core/tests/coretests/src/android/content/IntentTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import android.net.Uri; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; @@ -32,14 +33,21 @@ import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.security.Flags; import android.util.ArraySet; +import android.util.Xml; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.internal.util.XmlUtils; +import com.android.modules.utils.TypedXmlPullParser; +import com.android.modules.utils.TypedXmlSerializer; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -277,4 +285,40 @@ public class IntentTest { assertThat(b2.getBundle("bundle").getClassLoader()).isEqualTo(cl); } + @Test + @RequiresFlagsEnabled(android.content.flags.Flags.FLAG_INTENT_SAVE_TO_XML_PACKAGE) + public void testSaveToXmlAndRestore() throws Exception { + // Create an intent and set fields. + Intent original = new Intent(); + original.setAction(Intent.ACTION_MAIN); + original.setComponent(ComponentName.createRelative("com.intent.test", "IntentTest")); + original.setData(Uri.parse("content://path/to/file.txt")); + original.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + original.setIdentifier("unique_identifier"); + original.setPackage("com.intent.test"); + original.addCategory(Intent.CATEGORY_LAUNCHER); + original.putExtra("Name", "Some really important data"); + + String tag = "intent"; + + // Write to xml. + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + TypedXmlSerializer serializer = Xml.resolveSerializer(byteArrayOutputStream); + serializer.startDocument(null, true); + serializer.startTag(null, tag); + original.saveToXml(serializer); + serializer.endTag(null, tag); + serializer.endDocument(); + + // Restore from xml. + ByteArrayInputStream byteArrayInputStream = + new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); + TypedXmlPullParser parser = Xml.resolvePullParser(byteArrayInputStream); + XmlUtils.beginDocument(parser, tag); + Intent restored = Intent.restoreFromXml(parser); + + // Verify that the restored intent passed filterEquals on the original. + assertTrue(original.filterEquals(restored)); + } + } diff --git a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheUnitTest.java b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheUnitTest.java index 8349659517c5..b63fcdc8362f 100644 --- a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheUnitTest.java +++ b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheUnitTest.java @@ -68,6 +68,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; /** * Unit tests for {@link android.content.pm.RegisteredServicesCache} @@ -84,8 +85,8 @@ public class RegisteredServicesCacheUnitTest { @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); - private final ResolveInfo mResolveInfo1 = new ResolveInfo(); - private final ResolveInfo mResolveInfo2 = new ResolveInfo(); + private final TestResolveInfo mResolveInfo1 = new TestResolveInfo(); + private final TestResolveInfo mResolveInfo2 = new TestResolveInfo(); private final TestServiceType mTestServiceType1 = new TestServiceType("t1", "value1"); private final TestServiceType mTestServiceType2 = new TestServiceType("t2", "value2"); @Mock @@ -195,13 +196,13 @@ public class RegisteredServicesCacheUnitTest { reset(testServicesCache); - testServicesCache.clearServicesForQuerying(); int u1uid = UserHandle.getUid(U1, UID1); assertThat(u1uid).isNotEqualTo(UID1); final RegisteredServicesCache.ServiceInfo<TestServiceType> serviceInfo2 = newServiceInfo( mTestServiceType1, u1uid, mResolveInfo1.serviceInfo.getComponentName(), 1000L /* lastUpdateTime */); + mResolveInfo1.setResolveInfoId(U1); testServicesCache.addServiceForQuerying(U1, mResolveInfo1, serviceInfo2); testServicesCache.getAllServices(U1); @@ -286,7 +287,7 @@ public class RegisteredServicesCacheUnitTest { } @Test - public void testClearServiceInfoCachesAfterTimeout() throws Exception { + public void testClearServiceInfoCachesForSingleUserAfterTimeout() throws Exception { PackageInfo packageInfo1 = createPackageInfo(1000L /* lastUpdateTime */); when(mMockPackageManager.getPackageInfoAsUser(eq(mResolveInfo1.serviceInfo.packageName), anyInt(), eq(U0))).thenReturn(packageInfo1); @@ -316,6 +317,58 @@ public class RegisteredServicesCacheUnitTest { verify(testServicesCache, times(1)).parseServiceInfo(eq(mResolveInfo1), eq(1000L)); } + @Test + public void testClearServiceInfoCachesForMultiUserAfterTimeout() throws Exception { + PackageInfo packageInfo1 = createPackageInfo(1000L /* lastUpdateTime */); + when(mMockPackageManager.getPackageInfoAsUser(eq(mResolveInfo1.serviceInfo.packageName), + anyInt(), eq(U0))).thenReturn(packageInfo1); + PackageInfo packageInfo2 = createPackageInfo(2000L /* lastUpdateTime */); + when(mMockPackageManager.getPackageInfoAsUser(eq(mResolveInfo2.serviceInfo.packageName), + anyInt(), eq(U1))).thenReturn(packageInfo2); + + TestRegisteredServicesCache testServicesCache = spy( + new TestRegisteredServicesCache(mMockInjector, null /* serializerAndParser */)); + final RegisteredServicesCache.ServiceInfo<TestServiceType> serviceInfo1 = newServiceInfo( + mTestServiceType1, UID1, mResolveInfo1.serviceInfo.getComponentName(), + 1000L /* lastUpdateTime */); + testServicesCache.addServiceForQuerying(U0, mResolveInfo1, serviceInfo1); + + int u1uid = UserHandle.getUid(U1, UID1); + final RegisteredServicesCache.ServiceInfo<TestServiceType> serviceInfo2 = newServiceInfo( + mTestServiceType2, u1uid, mResolveInfo2.serviceInfo.getComponentName(), + 2000L /* lastUpdateTime */); + testServicesCache.addServiceForQuerying(U1, mResolveInfo2, serviceInfo2); + + // Don't invoke run on the Runnable for U0 user, and it will not clear the service info of + // U0 user. Invoke run on the Runnable for U1 user, and it will just clear the service info + // of U1 user. + doAnswer(invocation -> { + Message message = invocation.getArgument(0); + if (!message.obj.equals(Integer.valueOf(U0))) { + message.getCallback().run(); + } + return true; + }).when(mMockBackgroundHandler).sendMessageAtTime(any(Message.class), anyLong()); + + // It will generate the service info of U0 user into cache. + testServicesCache.getAllServices(U0); + verify(testServicesCache, times(1)).parseServiceInfo(eq(mResolveInfo1), eq(1000L)); + // It will generate the service info of U1 user into cache. + testServicesCache.getAllServices(U1); + verify(testServicesCache, times(1)).parseServiceInfo(eq(mResolveInfo2), eq(2000L)); + verify(mMockBackgroundHandler, times(2)).sendMessageAtTime(any(Message.class), anyLong()); + + reset(testServicesCache); + + testServicesCache.invalidateCache(U0); + testServicesCache.getAllServices(U0); + verify(testServicesCache, never()).parseServiceInfo(eq(mResolveInfo1), eq(1000L)); + + testServicesCache.invalidateCache(U1); + testServicesCache.getAllServices(U1); + verify(testServicesCache, times(1)).parseServiceInfo(eq(mResolveInfo2), eq(2000L)); + } + private static RegisteredServicesCache.ServiceInfo<TestServiceType> newServiceInfo( TestServiceType type, int uid, ComponentName componentName, long lastUpdateTime) { final ComponentInfo info = new ComponentInfo(); @@ -324,7 +377,7 @@ public class RegisteredServicesCacheUnitTest { return new RegisteredServicesCache.ServiceInfo<>(type, info, componentName, lastUpdateTime); } - private void addServiceInfoIntoResolveInfo(ResolveInfo resolveInfo, String packageName, + private void addServiceInfoIntoResolveInfo(TestResolveInfo resolveInfo, String packageName, String serviceName) { final ServiceInfo serviceInfo = new ServiceInfo(); serviceInfo.packageName = packageName; @@ -345,7 +398,7 @@ public class RegisteredServicesCacheUnitTest { static final String SERVICE_INTERFACE = "RegisteredServicesCacheUnitTest"; static final String SERVICE_META_DATA = "RegisteredServicesCacheUnitTest"; static final String ATTRIBUTES_NAME = "test"; - private SparseArray<Map<ResolveInfo, ServiceInfo<TestServiceType>>> mServices = + private SparseArray<Map<TestResolveInfo, ServiceInfo<TestServiceType>>> mServices = new SparseArray<>(); public TestRegisteredServicesCache(Injector<TestServiceType> injector, @@ -362,14 +415,14 @@ public class RegisteredServicesCacheUnitTest { @Override protected List<ResolveInfo> queryIntentServices(int userId) { - Map<ResolveInfo, ServiceInfo<TestServiceType>> map = mServices.get(userId, - new HashMap<ResolveInfo, ServiceInfo<TestServiceType>>()); + Map<TestResolveInfo, ServiceInfo<TestServiceType>> map = mServices.get(userId, + new HashMap<TestResolveInfo, ServiceInfo<TestServiceType>>()); return new ArrayList<>(map.keySet()); } - void addServiceForQuerying(int userId, ResolveInfo resolveInfo, + void addServiceForQuerying(int userId, TestResolveInfo resolveInfo, ServiceInfo<TestServiceType> serviceInfo) { - Map<ResolveInfo, ServiceInfo<TestServiceType>> map = mServices.get(userId); + Map<TestResolveInfo, ServiceInfo<TestServiceType>> map = mServices.get(userId); if (map == null) { map = new HashMap<>(); mServices.put(userId, map); @@ -377,16 +430,12 @@ public class RegisteredServicesCacheUnitTest { map.put(resolveInfo, serviceInfo); } - void clearServicesForQuerying() { - mServices.clear(); - } - @Override protected ServiceInfo<TestServiceType> parseServiceInfo(ResolveInfo resolveInfo, long lastUpdateTime) throws XmlPullParserException, IOException { int size = mServices.size(); for (int i = 0; i < size; i++) { - Map<ResolveInfo, ServiceInfo<TestServiceType>> map = mServices.valueAt(i); + Map<TestResolveInfo, ServiceInfo<TestServiceType>> map = mServices.valueAt(i); ServiceInfo<TestServiceType> serviceInfo = map.get(resolveInfo); if (serviceInfo != null) { return serviceInfo; @@ -400,4 +449,20 @@ public class RegisteredServicesCacheUnitTest { super.onUserRemoved(userId); } } + + /** + * Create different hash code with the same {@link android.content.pm.ResolveInfo} for testing. + */ + public static class TestResolveInfo extends ResolveInfo { + int mResolveInfoId = 0; + + @Override + public int hashCode() { + return Objects.hash(mResolveInfoId, serviceInfo); + } + + public void setResolveInfoId(int resolveInfoId) { + mResolveInfoId = resolveInfoId; + } + } } diff --git a/core/tests/coretests/src/com/android/internal/app/MediaRouteDialogPresenterTest.kt b/core/tests/coretests/src/com/android/internal/app/MediaRouteDialogPresenterTest.kt new file mode 100644 index 000000000000..e80d3a6e625f --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/app/MediaRouteDialogPresenterTest.kt @@ -0,0 +1,92 @@ +/* + * 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.internal.app + +import android.content.Context +import android.media.MediaRouter +import android.testing.TestableLooper.RunWithLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub + +@SmallTest +@RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidJUnit4::class) +class MediaRouteDialogPresenterTest { + private var selectedRoute: MediaRouter.RouteInfo = mock() + private var mediaRouter: MediaRouter = mock<MediaRouter> { + on { selectedRoute } doReturn selectedRoute + } + private var context: Context = mock<Context> { + on { getSystemServiceName(MediaRouter::class.java) } doReturn Context.MEDIA_ROUTER_SERVICE + on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter + } + + @Test + fun shouldShowChooserDialog_routeNotDefault_returnsFalse() { + selectedRoute.stub { + on { isDefault } doReturn false + on { matchesTypes(anyInt()) } doReturn true + } + + assertThat(MediaRouteDialogPresenter.shouldShowChooserDialog( + context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)) + .isEqualTo(false) + } + + @Test + fun shouldShowChooserDialog_routeDefault_returnsTrue() { + selectedRoute.stub { + on { isDefault } doReturn true + on { matchesTypes(anyInt()) } doReturn true + } + + assertThat(MediaRouteDialogPresenter.shouldShowChooserDialog( + context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)) + .isEqualTo(true) + } + + @Test + fun shouldShowChooserDialog_routeNotMatch_returnsTrue() { + selectedRoute.stub { + on { isDefault } doReturn false + on { matchesTypes(anyInt()) } doReturn false + } + + assertThat(MediaRouteDialogPresenter.shouldShowChooserDialog( + context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)) + .isEqualTo(true) + } + + @Test + fun shouldShowChooserDialog_routeDefaultAndNotMatch_returnsTrue() { + selectedRoute.stub { + on { isDefault } doReturn true + on { matchesTypes(anyInt()) } doReturn false + } + + assertThat(MediaRouteDialogPresenter.shouldShowChooserDialog( + context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)) + .isEqualTo(true) + } +}
\ No newline at end of file diff --git a/core/tests/coretests/src/com/android/internal/statusbar/DisableStatesTest.java b/core/tests/coretests/src/com/android/internal/statusbar/DisableStatesTest.java new file mode 100644 index 000000000000..5b82696b81c3 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/statusbar/DisableStatesTest.java @@ -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.internal.statusbar; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import android.os.Parcel; +import android.util.Pair; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.HashMap; +import java.util.Map; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DisableStatesTest { + + @Test + public void testParcelable() { + Map<Integer, Pair<Integer, Integer>> displaysWithStates = new HashMap<>(); + displaysWithStates.put(1, new Pair<>(10, 20)); + displaysWithStates.put(2, new Pair<>(30, 40)); + boolean animate = true; + DisableStates original = new DisableStates(displaysWithStates, animate); + + Parcel parcel = Parcel.obtain(); + original.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + DisableStates restored = DisableStates.CREATOR.createFromParcel(parcel); + + assertNotNull(restored); + assertEquals(original.displaysWithStates.size(), restored.displaysWithStates.size()); + for (Map.Entry<Integer, Pair<Integer, Integer>> entry : + original.displaysWithStates.entrySet()) { + int displayId = entry.getKey(); + Pair<Integer, Integer> originalDisplayStates = entry.getValue(); + Pair<Integer, Integer> restoredDisplayStates = restored.displaysWithStates.get( + displayId); + assertEquals(originalDisplayStates.first, restoredDisplayStates.first); + assertEquals(originalDisplayStates.second, restoredDisplayStates.second); + } + assertEquals(original.animate, restored.animate); + } +} diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java index d1aca34c7b8d..39cd4a89aae6 100644 --- a/graphics/java/android/graphics/Typeface.java +++ b/graphics/java/android/graphics/Typeface.java @@ -80,6 +80,7 @@ import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.BiConsumer; /** * The Typeface class specifies the typeface and intrinsic style of a font. @@ -1550,14 +1551,21 @@ public class Typeface { setDefault(defaults.get(0)); ArrayList<Typeface> oldGenerics = new ArrayList<>(); - oldGenerics.add(sSystemFontMap.get("sans-serif")); - sSystemFontMap.put("sans-serif", genericFamilies.get(0)); + BiConsumer<Typeface, String> swapTypeface = (typeface, key) -> { + oldGenerics.add(sSystemFontMap.get(key)); + sSystemFontMap.put(key, typeface); + }; - oldGenerics.add(sSystemFontMap.get("serif")); - sSystemFontMap.put("serif", genericFamilies.get(1)); + Typeface sansSerif = genericFamilies.get(0); + swapTypeface.accept(sansSerif, "sans-serif"); + swapTypeface.accept(Typeface.create(sansSerif, 100, false), "sans-serif-thin"); + swapTypeface.accept(Typeface.create(sansSerif, 300, false), "sans-serif-light"); + swapTypeface.accept(Typeface.create(sansSerif, 500, false), "sans-serif-medium"); + swapTypeface.accept(Typeface.create(sansSerif, 700, false), "sans-serif-bold"); + swapTypeface.accept(Typeface.create(sansSerif, 900, false), "sans-serif-black"); - oldGenerics.add(sSystemFontMap.get("monospace")); - sSystemFontMap.put("monospace", genericFamilies.get(2)); + swapTypeface.accept(genericFamilies.get(1), "serif"); + swapTypeface.accept(genericFamilies.get(2), "monospace"); return new Pair<>(oldDefaults, oldGenerics); } diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_restart.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_restart.xml new file mode 100644 index 000000000000..d407884d3fcf --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_restart.xml @@ -0,0 +1,28 @@ +<?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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="20dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#1C1C14" + android:pathData="M6,13c0,-1.65 0.67,-3.15 1.76,-4.24L6.34,7.34C4.9,8.79 4,10.79 4,13c0,4.08 3.05,7.44 7,7.93v-2.02C8.17,18.43 6,15.97 6,13z"/> + <path + android:fillColor="#1C1C14" + android:pathData="M20,13c0,-4.42 -3.58,-8 -8,-8c-0.06,0 -0.12,0.01 -0.18,0.01v0l1.09,-1.09L11.5,2.5L8,6l3.5,3.5l1.41,-1.41l-1.08,-1.08C11.89,7.01 11.95,7 12,7c3.31,0 6,2.69 6,6c0,2.97 -2.17,5.43 -5,5.91v2.02C16.95,20.44 20,17.08 20,13z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml index bfaa40771894..30acf1ac6eda 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml @@ -163,6 +163,13 @@ android:text="@string/change_aspect_ratio_text" android:src="@drawable/desktop_mode_ic_handle_menu_change_aspect_ratio" style="@style/DesktopModeHandleMenuActionButton"/> + + <com.android.wm.shell.windowdecor.HandleMenuActionButton + android:id="@+id/handle_menu_restart_button" + android:contentDescription="@string/handle_menu_restart_text" + android:text="@string/handle_menu_restart_text" + android:src="@drawable/desktop_mode_ic_handle_menu_restart" + style="@style/DesktopModeHandleMenuActionButton"/> </LinearLayout> <LinearLayout @@ -186,14 +193,13 @@ <ImageButton android:id="@+id/open_by_default_button" - android:layout_width="20dp" - android:layout_height="20dp" android:layout_gravity="end|center_vertical" - android:layout_marginStart="8dp" - android:layout_marginEnd="16dp" + android:paddingStart="12dp" + android:paddingEnd="16dp" android:contentDescription="@string/open_by_default_settings_text" android:src="@drawable/desktop_mode_ic_handle_menu_open_by_default_settings" - android:tint="@androidprv:color/materialColorOnSurface"/> + android:tint="@androidprv:color/materialColorOnSurface" + style="@style/DesktopModeHandleMenuWindowingButton"/> </LinearLayout> </LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_action_button.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_action_button.xml index 35e7de0e7c1e..0e5843f3e592 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_action_button.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_action_button.xml @@ -14,18 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:id="@+id/action_button" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:gravity="start|center_vertical" - android:paddingHorizontal="16dp" - android:importantForAccessibility="yes" - android:orientation="horizontal" - android:background="?android:attr/selectableItemBackground"> - +<merge xmlns:android="http://schemas.android.com/apk/res/android"> <ImageView android:id="@+id/image" android:importantForAccessibility="no" @@ -35,4 +24,4 @@ android:id="@+id/label" android:importantForAccessibility="no" style="@style/DesktopModeHandleMenuActionButtonTextView"/> -</LinearLayout> +</merge> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index e1bf6638a9b2..733f3bb8d6d0 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -294,6 +294,8 @@ <dimen name="bubble_bar_expanded_view_drop_target_padding_top">60dp</dimen> <dimen name="bubble_bar_expanded_view_drop_target_padding_bottom">24dp</dimen> <dimen name="bubble_bar_expanded_view_drop_target_padding_horizontal">48dp</dimen> + <dimen name="bubble_bar_drop_target_width">84dp</dimen> + <dimen name="bubble_bar_drop_target_height">48dp</dimen> <!-- Width of the box around bottom center of the screen where drag only leads to dismiss --> <dimen name="bubble_bar_dismiss_zone_width">192dp</dimen> <!-- Height of the box around bottom center of the screen where drag only leads to dismiss --> @@ -532,10 +534,10 @@ pill elevation. --> <dimen name="desktop_mode_handle_menu_width">218dp</dimen> - <!-- The maximum height of the handle menu in desktop mode. Three pills at 52dp each plus - additional actions pill 208dp plus 2dp spacing between them plus 4dp top padding - plus 2dp bottom padding: 52*3 + 52*4 + (4-1)*2 + 4 + 2 = 376 --> - <dimen name="desktop_mode_handle_menu_height">376dp</dimen> + <!-- The maximum height of the handle menu in desktop mode. Three pills at 52dp each, + additional actions pill 260dp, plus 2dp spacing between them plus 4dp top padding. + 52*3 + 52*5 + (5-1)*2 + 4 = 428 --> + <dimen name="desktop_mode_handle_menu_height">428dp</dimen> <!-- The elevation set on the handle menu pills. --> <dimen name="desktop_mode_handle_menu_pill_elevation">1dp</dimen> @@ -564,6 +566,9 @@ <!-- The height of the handle menu's "Change aspect ratio" pill in desktop mode. --> <dimen name="desktop_mode_handle_menu_change_aspect_ratio_height">52dp</dimen> + <!-- The height of the handle menu's "Optimize View" pill in desktop mode. --> + <dimen name="desktop_mode_handle_menu_restart_button_height">52dp</dimen> + <!-- The margin between pills of the handle menu in desktop mode. --> <dimen name="desktop_mode_handle_menu_pill_spacing_margin">2dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 5ef83826840b..1fd4704f7814 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -314,6 +314,8 @@ <string name="manage_windows_text">Manage Windows</string> <!-- Accessibility text for the handle menu change aspect ratio button [CHAR LIMIT=NONE] --> <string name="change_aspect_ratio_text">Change aspect ratio</string> + <!-- Accessibility text for the handle menu restart button [CHAR LIMIT=NONE] --> + <string name="handle_menu_restart_text">Optimize View</string> <!-- Accessibility text for the handle menu close button [CHAR LIMIT=NONE] --> <string name="close_text">Close</string> <!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] --> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 5f1db83d7acb..08cda7b94a78 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -45,7 +45,13 @@ <item name="android:layout_height">52dp</item> <item name="android:textColor">@androidprv:color/materialColorOnSurface</item> <item name="android:drawableTint">@androidprv:color/materialColorOnSurface</item> - <item name="android:importantForAccessibility">no</item> + <item name="android:importantForAccessibility">yes</item> + <item name="android:gravity">start|center_vertical</item> + <item name="android:paddingHorizontal">16dp</item> + <item name="android:clickable">true</item> + <item name="android:focusable">true</item> + <item name="android:orientation">horizontal</item> + <item name="android:background">?android:attr/selectableItemBackground</item> </style> <style name="DesktopModeHandleMenuActionButtonImage"> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt index 9bee11a92430..84e0fbe96de2 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt @@ -26,4 +26,9 @@ interface BubbleDropTargetBoundsProvider { * Get bubble bar expanded view visual drop target bounds on screen */ fun getBubbleBarExpandedViewDropTargetBounds(onLeft: Boolean): Rect + + /** + * Get the bar visual drop target bounds on screen + */ + fun getBarDropTargetBounds(onLeft: Boolean): Rect }
\ No newline at end of file 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 ed5e0c608675..e5a4cd034e72 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 @@ -278,6 +278,9 @@ public class DesktopModeStatus { if (!canEnterDesktopMode(context)) { return false; } + if (!enforceDeviceRestrictions()) { + return true; + } if (display.getType() == Display.TYPE_INTERNAL) { return canInternalDisplayHostDesktops(context); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 5d59af940da0..81cf031994a2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -508,7 +508,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } mShouldStartOnNextMoveEvent = false; } else { - mShouldStartOnNextMoveEvent = true; + if (predictiveBackDelayWmTransition()) { + onGestureStarted(touchX, touchY, swipeEdge); + } else { + mShouldStartOnNextMoveEvent = true; + } } } } else if (keyAction == MotionEvent.ACTION_MOVE) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index 03d6b0a8075d..0b45b086e13c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -106,6 +106,8 @@ public class BubblePositioner implements BubbleDropTargetBoundsProvider { private int mBarExpViewDropTargetPaddingTop; private int mBarExpViewDropTargetPaddingBottom; private int mBarExpViewDropTargetPaddingHorizontal; + private int mBarDropTargetWidth; + private int mBarDropTargetHeight; private PointF mRestingStackPosition; @@ -181,6 +183,8 @@ public class BubblePositioner implements BubbleDropTargetBoundsProvider { R.dimen.bubble_bar_expanded_view_drop_target_padding_bottom); mBarExpViewDropTargetPaddingHorizontal = res.getDimensionPixelSize( R.dimen.bubble_bar_expanded_view_drop_target_padding_horizontal); + mBarDropTargetWidth = res.getDimensionPixelSize(R.dimen.bubble_bar_drop_target_width); + mBarDropTargetHeight = res.getDimensionPixelSize(R.dimen.bubble_bar_drop_target_height); if (mShowingInBubbleBar) { mExpandedViewLargeScreenWidth = mExpandedViewBubbleBarWidth; @@ -1003,4 +1007,20 @@ public class BubblePositioner implements BubbleDropTargetBoundsProvider { ); return bounds; } + + @NonNull + @Override + public Rect getBarDropTargetBounds(boolean onLeft) { + Rect bounds = getBubbleBarExpandedViewDropTargetBounds(onLeft); + bounds.top = getBubbleBarTopOnScreen(); + bounds.bottom = bounds.top + mBarDropTargetHeight; + if (onLeft) { + // Keep the left edge from expanded view + bounds.right = bounds.left + mBarDropTargetWidth; + } else { + // Keep the right edge from expanded view + bounds.left = bounds.right - mBarDropTargetWidth; + } + return bounds; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java index 728975e8ef9f..6f7d7a486453 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java @@ -649,7 +649,7 @@ public class BubbleTransitions { @Override public void continueCollapse() { mBubble.cleanupTaskView(); - if (mTaskLeash == null || !mTaskLeash.isValid()) return; + if (mTaskLeash == null || !mTaskLeash.isValid() || !mRootLeash.isValid()) return; SurfaceControl.Transaction t = new SurfaceControl.Transaction(); t.reparent(mTaskLeash, mRootLeash); t.apply(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java index 04e8d8dee520..5d603d6c087d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java @@ -221,8 +221,11 @@ public class PipBoundsAlgorithm { + " than destination(%s)", sourceRectHint, destinationBounds); return false; } - if (!PictureInPictureParams.isSameAspectRatio(sourceRectHint, - new Rational(destinationBounds.width(), destinationBounds.height()))) { + // We use the aspect ratio of source rect hint to check against destination bounds + // here to avoid upscaling error. + final Rational srcAspectRatio = new Rational( + sourceRectHint.width(), sourceRectHint.height()); + if (!PictureInPictureParams.isSameAspectRatio(destinationBounds, srcAspectRatio)) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "isSourceRectHintValidForEnterPip=false, hint(%s) does not match" + " destination(%s) aspect ratio", sourceRectHint, destinationBounds); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java deleted file mode 100644 index 1128fb2259b2..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.common.pip; - -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; - -import android.window.DesktopExperienceFlags; -import android.window.DesktopModeFlags; -import android.window.DisplayAreaInfo; - -import com.android.wm.shell.Flags; -import com.android.wm.shell.RootTaskDisplayAreaOrganizer; -import com.android.wm.shell.desktopmode.DesktopUserRepositories; -import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler; - -import java.util.Optional; - -/** Helper class for PiP on Desktop Mode. */ -public class PipDesktopState { - private final PipDisplayLayoutState mPipDisplayLayoutState; - private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional; - private final Optional<DragToDesktopTransitionHandler> mDragToDesktopTransitionHandlerOptional; - private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; - - public PipDesktopState(PipDisplayLayoutState pipDisplayLayoutState, - Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, - Optional<DragToDesktopTransitionHandler> dragToDesktopTransitionHandlerOptional, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { - mPipDisplayLayoutState = pipDisplayLayoutState; - mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional; - mDragToDesktopTransitionHandlerOptional = dragToDesktopTransitionHandlerOptional; - mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; - } - - /** - * Returns whether PiP in Desktop Windowing is enabled by checking the following: - * - PiP in Desktop Windowing flag is enabled - * - DesktopUserRepositories is injected - * - DragToDesktopTransitionHandler is injected - */ - public boolean isDesktopWindowingPipEnabled() { - return DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue() - && mDesktopUserRepositoriesOptional.isPresent() - && mDragToDesktopTransitionHandlerOptional.isPresent(); - } - - /** - * Returns whether PiP in Connected Displays is enabled by checking the following: - * - PiP in Connected Displays flag is enabled - * - PiP2 flag is enabled - */ - public boolean isConnectedDisplaysPipEnabled() { - return DesktopExperienceFlags.ENABLE_CONNECTED_DISPLAYS_PIP.isTrue() && Flags.enablePip2(); - } - - /** Returns whether the display with the PiP task is in freeform windowing mode. */ - private boolean isDisplayInFreeform() { - final DisplayAreaInfo tdaInfo = mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo( - mPipDisplayLayoutState.getDisplayId()); - if (tdaInfo != null) { - return tdaInfo.configuration.windowConfiguration.getWindowingMode() - == WINDOWING_MODE_FREEFORM; - } - return false; - } - - /** Returns whether PiP is active in a display that is in active Desktop Mode session. */ - public boolean isPipInDesktopMode() { - // Early return if PiP in Desktop Windowing is not supported. - if (!isDesktopWindowingPipEnabled()) { - return false; - } - final int displayId = mPipDisplayLayoutState.getDisplayId(); - return mDesktopUserRepositoriesOptional.get().getCurrent().isAnyDeskActive(displayId); - } - - /** - * The windowing mode to restore to when resizing out of PIP direction. - * Defaults to undefined and can be overridden to restore to an alternate windowing mode. - */ - public int getOutPipWindowingMode() { - // If we are exiting PiP while the device is in Desktop mode (the task should expand to - // freeform windowing mode): - // 1) If the display windowing mode is freeform, set windowing mode to UNDEFINED so it will - // resolve the windowing mode to the display's windowing mode. - // 2) If the display windowing mode is not FREEFORM, set windowing mode to FREEFORM. - if (isPipInDesktopMode()) { - if (isDisplayInFreeform()) { - return WINDOWING_MODE_UNDEFINED; - } else { - return WINDOWING_MODE_FREEFORM; - } - } - - // By default, or if the task is going to fullscreen, reset the windowing mode to undefined. - return WINDOWING_MODE_UNDEFINED; - } - - /** Returns whether there is a drag-to-desktop transition in progress. */ - public boolean isDragToDesktopInProgress() { - // Early return if PiP in Desktop Windowing is not supported. - if (!isDesktopWindowingPipEnabled()) { - return false; - } - return mDragToDesktopTransitionHandlerOptional.get().getInProgress(); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.kt new file mode 100644 index 000000000000..55bde8906b63 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.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.wm.shell.common.pip + +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.window.DesktopExperienceFlags +import android.window.DesktopModeFlags +import com.android.wm.shell.Flags +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.desktopmode.DesktopUserRepositories +import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler +import java.util.Optional + +/** Helper class for PiP on Desktop Mode. */ +class PipDesktopState( + private val pipDisplayLayoutState: PipDisplayLayoutState, + private val desktopUserRepositoriesOptional: Optional<DesktopUserRepositories>, + private val dragToDesktopTransitionHandlerOptional: Optional<DragToDesktopTransitionHandler>, + private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer +) { + /** + * Returns whether PiP in Desktop Windowing is enabled by checking the following: + * - PiP in Desktop Windowing flag is enabled + * - DesktopUserRepositories is present + * - DragToDesktopTransitionHandler is present + */ + fun isDesktopWindowingPipEnabled(): Boolean = + DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue && + desktopUserRepositoriesOptional.isPresent && + dragToDesktopTransitionHandlerOptional.isPresent + + /** + * Returns whether PiP in Connected Displays is enabled by checking the following: + * - PiP in Connected Displays flag is enabled + * - PiP2 flag is enabled + */ + fun isConnectedDisplaysPipEnabled(): Boolean = + DesktopExperienceFlags.ENABLE_CONNECTED_DISPLAYS_PIP.isTrue && Flags.enablePip2() + + /** Returns whether the display with the PiP task is in freeform windowing mode. */ + private fun isDisplayInFreeform(): Boolean { + val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo( + pipDisplayLayoutState.displayId + ) + + return tdaInfo?.configuration?.windowConfiguration?.windowingMode == WINDOWING_MODE_FREEFORM + } + + /** Returns whether PiP is active in a display that is in active Desktop Mode session. */ + fun isPipInDesktopMode(): Boolean { + if (!isDesktopWindowingPipEnabled()) { + return false + } + + val displayId = pipDisplayLayoutState.displayId + return desktopUserRepositoriesOptional.get().current.isAnyDeskActive(displayId) + } + + /** Returns the windowing mode to restore to when resizing out of PIP direction. */ + // TODO(b/403345629): Update this for Multi-Desktop. + fun getOutPipWindowingMode(): Int { + // If we are exiting PiP while the device is in Desktop mode, the task should expand to + // freeform windowing mode. + // 1) If the display windowing mode is freeform, set windowing mode to UNDEFINED so it will + // resolve the windowing mode to the display's windowing mode. + // 2) If the display windowing mode is not FREEFORM, set windowing mode to FREEFORM. + if (isPipInDesktopMode()) { + return if (isDisplayInFreeform()) { + WINDOWING_MODE_UNDEFINED + } else { + WINDOWING_MODE_FREEFORM + } + } + + // By default, or if the task is going to fullscreen, reset the windowing mode to undefined. + return WINDOWING_MODE_UNDEFINED + } + + /** Returns whether there is a drag-to-desktop transition in progress. */ + fun isDragToDesktopInProgress(): Boolean = + isDesktopWindowingPipEnabled() && dragToDesktopTransitionHandlerOptional.get().inProgress +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java index 9fa162164e0e..d9a66e1d64b2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java @@ -57,6 +57,11 @@ public class FlexParallaxSpec implements ParallaxSpec { * @return 0f = no dim applied. 1f = full black. */ public float getDimValue(int position, DividerSnapAlgorithm snapAlgorithm) { + // On tablets, apps don't go offscreen, so only dim for dismissal. + if (!snapAlgorithm.areOffscreenRatiosSupported()) { + return ParallaxSpec.super.getDimValue(position, snapAlgorithm); + } + int startDismissPos = snapAlgorithm.getDismissStartTarget().getPosition(); int firstTargetPos = snapAlgorithm.getFirstSplitTarget().getPosition(); int middleTargetPos = snapAlgorithm.getMiddleTarget().getPosition(); 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 4413c8715c0d..d5f4a3885dbb 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 @@ -112,6 +112,12 @@ public class CompatUIController implements OnDisplaysChangedListener, new SparseArray<>(0); /** + * {@link SparseArray} that maps task ids to {@link CompatUIInfo}. + */ + private final SparseArray<CompatUIInfo> mTaskIdToCompatUIInfoMap = + new SparseArray<>(0); + + /** * {@link Set} of task ids for which we need to display a restart confirmation dialog */ private Set<Integer> mSetOfTaskIdsShowingRestartDialog = new HashSet<>(); @@ -261,7 +267,11 @@ public class CompatUIController implements OnDisplaysChangedListener, private void handleDisplayCompatShowRestartDialog( CompatUIRequests.DisplayCompatShowRestartDialog request) { - onRestartButtonClicked(new Pair<>(request.getTaskInfo(), request.getTaskListener())); + final CompatUIInfo compatUIInfo = mTaskIdToCompatUIInfoMap.get(request.getTaskId()); + if (compatUIInfo == null) { + return; + } + onRestartButtonClicked(new Pair<>(compatUIInfo.getTaskInfo(), compatUIInfo.getListener())); } /** @@ -273,6 +283,11 @@ public class CompatUIController implements OnDisplaysChangedListener, public void onCompatInfoChanged(@NonNull CompatUIInfo compatUIInfo) { final TaskInfo taskInfo = compatUIInfo.getTaskInfo(); final ShellTaskOrganizer.TaskListener taskListener = compatUIInfo.getListener(); + if (taskListener == null) { + mTaskIdToCompatUIInfoMap.delete(taskInfo.taskId); + } else { + mTaskIdToCompatUIInfoMap.put(taskInfo.taskId, compatUIInfo); + } final boolean isInDisplayCompatMode = taskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove(); if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat() 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 index da4fc99491dc..b7af596ee0ae 100644 --- 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 @@ -16,8 +16,6 @@ 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 @@ -27,7 +25,6 @@ internal const val DISPLAY_COMPAT_SHOW_RESTART_DIALOG = 0 */ 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) : + data class DisplayCompatShowRestartDialog(val taskId: Int) : CompatUIRequests(DISPLAY_COMPAT_SHOW_RESTART_DIALOG) } 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 2cf671b0f446..0edab006cb66 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 @@ -79,6 +79,7 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.UserProfileContexts; import com.android.wm.shell.common.split.SplitState; +import com.android.wm.shell.compatui.api.CompatUIHandler; import com.android.wm.shell.compatui.letterbox.LetterboxCommandHandler; import com.android.wm.shell.compatui.letterbox.LetterboxTransitionObserver; import com.android.wm.shell.crashhandling.ShellCrashHandler; @@ -1044,7 +1045,8 @@ public abstract class WMShellModule { RecentsTransitionHandler recentsTransitionHandler, DesktopModeCompatPolicy desktopModeCompatPolicy, DesktopTilingDecorViewModel desktopTilingDecorViewModel, - MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController + MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController, + Optional<CompatUIHandler> compatUI ) { if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) { return Optional.empty(); @@ -1062,7 +1064,7 @@ public abstract class WMShellModule { activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy, desktopTilingDecorViewModel, - multiDisplayDragMoveIndicatorController)); + multiDisplayDragMoveIndicatorController, compatUI.orElse(null))); } @WMSingleton @@ -1213,7 +1215,8 @@ public abstract class WMShellModule { ShellTaskOrganizer shellTaskOrganizer, TaskStackListenerImpl taskStackListener, ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, - @DynamicOverride DesktopUserRepositories desktopUserRepositories) { + @DynamicOverride DesktopUserRepositories desktopUserRepositories, + DisplayController displayController) { if (DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.of( new DesktopActivityOrientationChangeHandler( @@ -1222,7 +1225,8 @@ public abstract class WMShellModule { shellTaskOrganizer, taskStackListener, toggleResizeDesktopTaskTransitionHandler, - desktopUserRepositories)); + desktopUserRepositories, + displayController)); } return Optional.empty(); } @@ -1341,7 +1345,9 @@ public abstract class WMShellModule { Context context, ShellInit shellInit, @ShellMainThread CoroutineScope mainScope, + ShellController shellController, DisplayController displayController, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, Optional<DesktopUserRepositories> desktopUserRepositories, Optional<DesktopTasksController> desktopTasksController, Optional<DesktopDisplayModeController> desktopDisplayModeController, @@ -1355,7 +1361,9 @@ public abstract class WMShellModule { context, shellInit, mainScope, + shellController, displayController, + rootTaskDisplayAreaOrganizer, desktopRepositoryInitializer, desktopUserRepositories.get(), desktopTasksController.get(), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt index b8f4bb8d8323..39ce5d9023a6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt @@ -23,10 +23,10 @@ import android.content.pm.ActivityInfo.ScreenOrientation import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.content.res.Configuration.ORIENTATION_PORTRAIT import android.graphics.Rect -import android.util.Size import android.window.WindowContainerTransaction import com.android.window.flags.Flags import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.TaskStackListenerCallback import com.android.wm.shell.common.TaskStackListenerImpl import com.android.wm.shell.shared.desktopmode.DesktopModeStatus @@ -40,6 +40,7 @@ class DesktopActivityOrientationChangeHandler( private val taskStackListener: TaskStackListenerImpl, private val resizeHandler: ToggleResizeDesktopTaskTransitionHandler, private val desktopUserRepositories: DesktopUserRepositories, + private val displayController: DisplayController, ) { init { @@ -101,12 +102,24 @@ class DesktopActivityOrientationChangeHandler( orientation == ORIENTATION_LANDSCAPE && ActivityInfo.isFixedOrientationPortrait(requestedOrientation) ) { + val displayLayout = displayController.getDisplayLayout(task.displayId) ?: return + val captionInsets = + task.configuration.windowConfiguration.appBounds?.let { + it.top - task.configuration.windowConfiguration.bounds.top + } ?: 0 + val newOrientationBounds = + calculateInitialBounds( + displayLayout = displayLayout, + taskInfo = task, + captionInsets = captionInsets, + requestedScreenOrientation = requestedOrientation, + ) - val finalSize = Size(taskHeight, taskWidth) // Use the center x as the resizing anchor point. - val left = taskBounds.centerX() - finalSize.width / 2 - val right = left + finalSize.width - val finalBounds = Rect(left, taskBounds.top, right, taskBounds.top + finalSize.height) + val left = taskBounds.centerX() - newOrientationBounds.width() / 2 + val right = left + newOrientationBounds.width() + val finalBounds = + Rect(left, taskBounds.top, right, taskBounds.top + newOrientationBounds.height()) val wct = WindowContainerTransaction().setBounds(task.token, finalBounds) resizeHandler.startTransition(wct) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt index 683b74392fa6..3b98f8123b46 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt @@ -17,16 +17,20 @@ package com.android.wm.shell.desktopmode import android.content.Context +import android.view.Display import android.view.Display.DEFAULT_DISPLAY import android.window.DesktopExperienceFlags import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.sysui.UserChangeListener import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel import kotlinx.coroutines.launch @@ -36,7 +40,9 @@ class DesktopDisplayEventHandler( private val context: Context, shellInit: ShellInit, private val mainScope: CoroutineScope, + private val shellController: ShellController, private val displayController: DisplayController, + private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, private val desktopRepositoryInitializer: DesktopRepositoryInitializer, private val desktopUserRepositories: DesktopUserRepositories, private val desktopTasksController: DesktopTasksController, @@ -53,8 +59,17 @@ class DesktopDisplayEventHandler( private fun onInit() { displayController.addDisplayWindowListener(this) - if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { desktopTasksController.onDeskRemovedListener = this + + shellController.addUserChangeListener( + object : UserChangeListener { + override fun onUserChanged(newUserId: Int, userContext: Context) { + val displayIds = rootTaskDisplayAreaOrganizer.displayIds + createDefaultDesksIfNeeded(displayIds.toSet()) + } + } + ) } } @@ -63,23 +78,7 @@ class DesktopDisplayEventHandler( desktopDisplayModeController.refreshDisplayWindowingMode() } - if (!supportsDesks(displayId)) { - logV("Display #$displayId does not support desks") - return - } - - mainScope.launch { - desktopRepositoryInitializer.isInitialized.collect { initialized -> - if (!initialized) return@collect - if (desktopRepository.getNumberOfDesks(displayId) == 0) { - logV("Creating new desk in new display#$displayId") - // TODO: b/393978539 - consider activating the desk on creation when - // applicable, such as for connected displays. - desktopTasksController.createDesk(displayId) - } - cancel() - } - } + createDefaultDesksIfNeeded(displayIds = setOf(displayId)) } override fun onDisplayRemoved(displayId: Int) { @@ -93,8 +92,34 @@ class DesktopDisplayEventHandler( override fun onDeskRemoved(lastDisplayId: Int, deskId: Int) { val remainingDesks = desktopRepository.getNumberOfDesks(lastDisplayId) if (remainingDesks == 0) { - logV("All desks removed from display#$lastDisplayId, creating empty desk") - desktopTasksController.createDesk(lastDisplayId) + logV("All desks removed from display#$lastDisplayId") + createDefaultDesksIfNeeded(setOf(lastDisplayId)) + } + } + + private fun createDefaultDesksIfNeeded(displayIds: Set<Int>) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return + logV("createDefaultDesksIfNeeded displays=%s", displayIds) + mainScope.launch { + desktopRepositoryInitializer.isInitialized.collect { initialized -> + if (!initialized) return@collect + displayIds + .filter { displayId -> displayId != Display.INVALID_DISPLAY } + .filter { displayId -> supportsDesks(displayId) } + .filter { displayId -> desktopRepository.getNumberOfDesks(displayId) == 0 } + .also { displaysNeedingDesk -> + logV( + "createDefaultDesksIfNeeded creating default desks in displays=%s", + displaysNeedingDesk, + ) + } + .forEach { displayId -> + // TODO: b/393978539 - consider activating the desk on creation when + // applicable, such as for connected displays. + desktopTasksController.createDesk(displayId) + } + cancel() + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt index 1ea545f3ab67..19507c17bc95 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt @@ -23,10 +23,7 @@ import android.hardware.input.InputManager import android.hardware.input.InputManager.KeyGestureEventHandler import android.hardware.input.KeyGestureEvent import android.os.IBinder -import android.window.DesktopModeFlags -import com.android.hardware.input.Flags.manageKeyGestures import com.android.internal.protolog.ProtoLog -import com.android.window.flags.Flags.enableMoveToNextDisplayShortcut import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.ShellExecutor @@ -51,16 +48,20 @@ class DesktopModeKeyGestureHandler( ) : KeyGestureEventHandler { init { - inputManager.registerKeyGestureEventHandler(this) + if (desktopTasksController.isPresent && desktopModeWindowDecorViewModel.isPresent) { + val supportedGestures = + listOf( + KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY, + KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW, + KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW, + KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW, + ) + inputManager.registerKeyGestureEventHandler(supportedGestures, this) + } } - override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?): Boolean { - if ( - !desktopTasksController.isPresent || - !desktopModeWindowDecorViewModel.isPresent - ) { - return false - } + override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?) { when (event.keyGestureType) { KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY -> { logV("Key gesture MOVE_TO_NEXT_DISPLAY is handled") @@ -69,7 +70,6 @@ class DesktopModeKeyGestureHandler( desktopTasksController.get().moveToNextDisplay(it.taskId) } } - return true } KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW -> { logV("Key gesture SNAP_LEFT_FREEFORM_WINDOW is handled") @@ -85,7 +85,6 @@ class DesktopModeKeyGestureHandler( ) } } - return true } KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW -> { logV("Key gesture SNAP_RIGHT_FREEFORM_WINDOW is handled") @@ -101,7 +100,6 @@ class DesktopModeKeyGestureHandler( ) } } - return true } KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW -> { logV("Key gesture TOGGLE_MAXIMIZE_FREEFORM_WINDOW is handled") @@ -120,7 +118,6 @@ class DesktopModeKeyGestureHandler( ) } } - return true } KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW -> { logV("Key gesture MINIMIZE_FREEFORM_WINDOW is handled") @@ -129,9 +126,7 @@ class DesktopModeKeyGestureHandler( desktopTasksController.get().minimizeTask(it, MinimizeReason.KEY_GESTURE) } } - return true } - else -> return false } } 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 a8b0bafee724..3c44fe8061aa 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 @@ -69,6 +69,7 @@ fun calculateInitialBounds( taskInfo: RunningTaskInfo, scale: Float = DESKTOP_MODE_INITIAL_BOUNDS_SCALE, captionInsets: Int = 0, + requestedScreenOrientation: Int? = null, ): Rect { val screenBounds = Rect(0, 0, displayLayout.width(), displayLayout.height()) val appAspectRatio = calculateAspectRatio(taskInfo) @@ -85,12 +86,13 @@ fun calculateInitialBounds( } val topActivityInfo = taskInfo.topActivityInfo ?: return positionInScreen(idealSize, stableBounds) + val screenOrientation = requestedScreenOrientation ?: topActivityInfo.screenOrientation val initialSize: Size = when (taskInfo.configuration.orientation) { ORIENTATION_LANDSCAPE -> { if (taskInfo.canChangeAspectRatio) { - if (isFixedOrientationPortrait(topActivityInfo.screenOrientation)) { + if (isFixedOrientationPortrait(screenOrientation)) { // For portrait resizeable activities, respect apps fullscreen width but // apply ideal size height. Size( @@ -104,14 +106,20 @@ fun calculateInitialBounds( } else { // If activity is unresizeable, regardless of orientation, calculate maximum // size (within the ideal size) maintaining original aspect ratio. - maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio, captionInsets) + maximizeSizeGivenAspectRatio( + taskInfo, + idealSize, + appAspectRatio, + captionInsets, + screenOrientation, + ) } } ORIENTATION_PORTRAIT -> { val customPortraitWidthForLandscapeApp = screenBounds.width() - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2) if (taskInfo.canChangeAspectRatio) { - if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) { + if (isFixedOrientationLandscape(screenOrientation)) { // For landscape resizeable activities, respect apps fullscreen height and // apply custom app width. Size( @@ -123,7 +131,7 @@ fun calculateInitialBounds( idealSize } } else { - if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) { + if (isFixedOrientationLandscape(screenOrientation)) { // For landscape unresizeable activities, apply custom app width to ideal // size and calculate maximum size with this area while maintaining original // aspect ratio. @@ -132,6 +140,7 @@ fun calculateInitialBounds( Size(customPortraitWidthForLandscapeApp, idealSize.height), appAspectRatio, captionInsets, + screenOrientation, ) } else { // For portrait unresizeable activities, calculate maximum size (within the @@ -141,6 +150,7 @@ fun calculateInitialBounds( idealSize, appAspectRatio, captionInsets, + screenOrientation, ) } } @@ -190,13 +200,16 @@ fun maximizeSizeGivenAspectRatio( targetArea: Size, aspectRatio: Float, captionInsets: Int = 0, + requestedScreenOrientation: Int? = null, ): Size { val targetHeight = targetArea.height - captionInsets val targetWidth = targetArea.width val finalHeight: Int val finalWidth: Int // Get orientation either through top activity or task's orientation - if (taskInfo.hasPortraitTopActivity()) { + val screenOrientation = + requestedScreenOrientation ?: taskInfo.topActivityInfo?.screenOrientation + if (taskInfo.hasPortraitTopActivity(screenOrientation)) { val tempWidth = ceil(targetHeight / aspectRatio).toInt() if (tempWidth <= targetWidth) { finalHeight = targetHeight @@ -354,9 +367,8 @@ fun centerInArea(desiredSize: Size, areaBounds: Rect, leftStart: Int, topStart: return Rect(newLeft, newTop, newRight, newBottom) } -private fun TaskInfo.hasPortraitTopActivity(): Boolean { - val topActivityScreenOrientation = - topActivityInfo?.screenOrientation ?: SCREEN_ORIENTATION_UNSPECIFIED +private fun TaskInfo.hasPortraitTopActivity(screenOrientation: Int?): Boolean { + val topActivityScreenOrientation = screenOrientation ?: SCREEN_ORIENTATION_UNSPECIFIED val appBounds = configuration.windowConfiguration.appBounds return when { 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 093e8ef8bc0e..5849d4af4e7e 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 @@ -465,6 +465,30 @@ class DesktopTasksController( return isFreeformDisplay } + /** Called when the recents transition that started while in desktop is finishing. */ + fun onRecentsInDesktopAnimationFinishing( + transition: IBinder, + finishWct: WindowContainerTransaction, + returnToApp: Boolean, + ) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return + logV("onRecentsInDesktopAnimationFinishing returnToApp=%b", returnToApp) + if (returnToApp) return + // Home/Recents only exists in the default display. + val activeDesk = taskRepository.getActiveDeskId(DEFAULT_DISPLAY) ?: return + // Not going back to the active desk, deactivate it. + val runOnTransitStart = + performDesktopExitCleanUp( + wct = finishWct, + deskId = activeDesk, + displayId = DEFAULT_DISPLAY, + willExitDesktop = true, + shouldEndUpAtHome = true, + fromRecentsTransition = true, + ) + runOnTransitStart?.invoke(transition) + } + /** Creates a new desk in the given display. */ fun createDesk(displayId: Int) { if (displayId == Display.INVALID_DISPLAY) { @@ -835,20 +859,22 @@ class DesktopTasksController( val requestRes = transitions.dispatchRequest(Binder(), requestInfo, /* skip= */ null) wct.merge(requestRes.second, true) - desktopPipTransitionObserver.get().addPendingPipTransition( - DesktopPipTransitionObserver.PendingPipTransition( - token = freeformTaskTransitionStarter.startPipTransition(wct), - taskId = taskInfo.taskId, - onSuccess = { - onDesktopTaskEnteredPip( - taskId = taskId, - deskId = deskId, - displayId = taskInfo.displayId, - taskIsLastVisibleTaskBeforePip = isLastTask, - ) - }, + desktopPipTransitionObserver + .get() + .addPendingPipTransition( + DesktopPipTransitionObserver.PendingPipTransition( + token = freeformTaskTransitionStarter.startPipTransition(wct), + taskId = taskInfo.taskId, + onSuccess = { + onDesktopTaskEnteredPip( + taskId = taskId, + deskId = deskId, + displayId = taskInfo.displayId, + taskIsLastVisibleTaskBeforePip = isLastTask, + ) + }, + ) ) - ) } else { snapEventHandler.removeTaskIfTiled(displayId, taskId) val willExitDesktop = willExitDesktop(taskId, displayId, forceExitDesktop = false) @@ -1208,6 +1234,7 @@ class DesktopTasksController( pendingIntentBackgroundActivityStartMode = ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS launchBounds = bounds + launchDisplayId = displayId if (DesktopModeFlags.ENABLE_SHELL_INITIAL_BOUNDS_REGRESSION_BUG_FIX.isTrue) { // Sets launch bounds size as flexible so core can recalculate. flexibleLaunchSize = true @@ -1937,16 +1964,23 @@ class DesktopTasksController( displayId: Int, willExitDesktop: Boolean, shouldEndUpAtHome: Boolean = true, + fromRecentsTransition: Boolean = false, ): RunOnTransitStart? { if (!willExitDesktop) return null desktopModeEnterExitTransitionListener?.onExitDesktopModeTransitionStarted( FULLSCREEN_ANIMATION_DURATION ) - removeWallpaperActivity(wct, displayId) - if (shouldEndUpAtHome) { - // If the transition should end up with user going to home, launch home with a pending - // intent. - addLaunchHomePendingIntent(wct, displayId) + // No need to clean up the wallpaper / reorder home when coming from a recents transition. + if ( + !fromRecentsTransition || + !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue + ) { + removeWallpaperActivity(wct, displayId) + if (shouldEndUpAtHome) { + // If the transition should end up with user going to home, launch home with a + // pending intent. + addLaunchHomePendingIntent(wct, displayId) + } } return prepareDeskDeactivationIfNeeded(wct, deskId) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt index 23562388b3e5..5e4122ba14ec 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode import android.animation.Animator import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet import android.animation.RectEvaluator import android.animation.ValueAnimator import android.app.ActivityManager @@ -32,6 +33,8 @@ import android.view.View import android.view.WindowManager import android.view.WindowlessWindowManager import android.view.animation.DecelerateInterpolator +import android.widget.FrameLayout +import androidx.core.animation.doOnEnd import com.android.internal.annotations.VisibleForTesting import com.android.window.flags.Flags import com.android.wm.shell.R @@ -42,6 +45,7 @@ import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType import com.android.wm.shell.shared.annotations.ShellDesktopThread import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider import com.android.wm.shell.windowdecor.WindowDecoration.SurfaceControlViewHostFactory import com.android.wm.shell.windowdecor.tiling.SnapEventHandler @@ -64,6 +68,8 @@ constructor( private val snapEventHandler: SnapEventHandler, ) { @VisibleForTesting var indicatorView: View? = null + // Optional extra indicator showing the outline of the bubble bar + private var barIndicatorView: View? = null private var indicatorViewHost: SurfaceControlViewHost? = null // Below variables and the SyncTransactionQueue are the only variables that should // be accessed from shell main thread. Everything else should be used exclusively @@ -93,7 +99,12 @@ constructor( screenWidth = metrics.widthPixels screenHeight = metrics.heightPixels } - indicatorView = View(context) + indicatorView = + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + FrameLayout(context) + } else { + View(context) + } val leash = indicatorBuilder .setName("Desktop Mode Visual Indicator") @@ -183,23 +194,50 @@ constructor( ) } else { val animStartType = IndicatorType.valueOf(currentType.name) - val animator = - indicatorView?.let { - VisualIndicatorAnimator.animateIndicatorType( - it, - layout, - animStartType, - newType, - bubbleBoundsProvider, - taskInfo.displayId, - snapEventHandler, - ) - } ?: return@execute + val indicator = indicatorView ?: return@execute + var animator: Animator = + VisualIndicatorAnimator.animateIndicatorType( + indicator, + layout, + animStartType, + newType, + bubbleBoundsProvider, + taskInfo.displayId, + snapEventHandler, + ) + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + if (currentType.isBubbleType() || newType.isBubbleType()) { + animator = addBarIndicatorAnimation(animator, currentType, newType) + } + } animator.start() } } } + private fun addBarIndicatorAnimation( + visualIndicatorAnimator: Animator, + currentType: IndicatorType, + newType: IndicatorType, + ): Animator { + if (newType.isBubbleType()) { + getOrCreateBubbleBarIndicator(newType)?.let { bar -> + return AnimatorSet().apply { + playTogether(visualIndicatorAnimator, fadeBarIndicatorIn(bar)) + } + } + } + if (currentType.isBubbleType()) { + barIndicatorView?.let { bar -> + barIndicatorView = null + return AnimatorSet().apply { + playTogether(visualIndicatorAnimator, fadeBarIndicatorOut(bar)) + } + } + } + return visualIndicatorAnimator + } + /** * Fade indicator in as provided type. * @@ -223,17 +261,20 @@ constructor( snapEventHandler: SnapEventHandler, ) { desktopExecutor.assertCurrentThread() - indicatorView?.let { - it.setBackgroundResource(R.drawable.desktop_windowing_transition_background) - val animator = + indicatorView?.let { indicator -> + indicator.setBackgroundResource(R.drawable.desktop_windowing_transition_background) + var animator: Animator = VisualIndicatorAnimator.fadeBoundsIn( - it, + indicator, type, layout, bubbleBoundsProvider, displayId, snapEventHandler, ) + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + animator = addBarIndicatorAnimation(animator, IndicatorType.NO_INDICATOR, type) + } animator.start() } } @@ -259,7 +300,7 @@ constructor( desktopExecutor.execute { indicatorView?.let { val animStartType = IndicatorType.valueOf(currentType.name) - val animator = + var animator: Animator = VisualIndicatorAnimator.fadeBoundsOut( it, animStartType, @@ -268,6 +309,10 @@ constructor( displayId, snapEventHandler, ) + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + animator = + addBarIndicatorAnimation(animator, currentType, IndicatorType.NO_INDICATOR) + } animator.addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { @@ -302,6 +347,38 @@ constructor( isReleased = true } + private fun getOrCreateBubbleBarIndicator(type: IndicatorType): View? { + val container = indicatorView as? FrameLayout ?: return null + val onLeft = type == IndicatorType.TO_BUBBLE_LEFT_INDICATOR + val bounds = bubbleBoundsProvider?.getBarDropTargetBounds(onLeft) ?: return null + val lp = FrameLayout.LayoutParams(bounds.width(), bounds.height()) + lp.leftMargin = bounds.left + lp.topMargin = bounds.top + if (barIndicatorView == null) { + val indicator = View(container.context) + indicator.setBackgroundResource(R.drawable.desktop_windowing_transition_background) + container.addView(indicator, lp) + barIndicatorView = indicator + } else { + barIndicatorView?.layoutParams = lp + } + return barIndicatorView + } + + private fun fadeBarIndicatorIn(barIndicator: View): Animator { + // Use layout bounds as the end bounds in case the view has not been laid out yet + val lp = barIndicator.layoutParams + val endBounds = Rect(0, 0, lp.width, lp.height) + return VisualIndicatorAnimator.fadeBoundsIn(barIndicator, endBounds) + } + + private fun fadeBarIndicatorOut(barIndicator: View): Animator { + val startBounds = Rect(0, 0, barIndicator.width, barIndicator.height) + val barAnimator = VisualIndicatorAnimator.fadeBoundsOut(barIndicator, startBounds) + barAnimator.doOnEnd { (indicatorView as? FrameLayout)?.removeView(barIndicator) } + return barAnimator + } + /** * Animator for Desktop Mode transitions which supports bounds and alpha animation. Functions * should only be called from the desktop executor. @@ -383,9 +460,13 @@ constructor( displayId, snapEventHandler, ) + return fadeBoundsIn(view, endBounds) + } + + @ShellDesktopThread + fun fadeBoundsIn(view: View, endBounds: Rect): VisualIndicatorAnimator { val startBounds = getMinBounds(endBounds) view.background.bounds = startBounds - val animator = VisualIndicatorAnimator(view, startBounds, endBounds) animator.interpolator = DecelerateInterpolator() setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_FADE_IN_ANIM) @@ -409,6 +490,11 @@ constructor( displayId, snapEventHandler, ) + return fadeBoundsOut(view, startBounds) + } + + @ShellDesktopThread + fun fadeBoundsOut(view: View, startBounds: Rect): VisualIndicatorAnimator { val endBounds = getMinBounds(startBounds) view.background.bounds = startBounds val animator = VisualIndicatorAnimator(view, startBounds, endBounds) @@ -571,4 +657,9 @@ constructor( } } } + + private fun IndicatorType.isBubbleType(): Boolean { + return this == IndicatorType.TO_BUBBLE_LEFT_INDICATOR || + this == IndicatorType.TO_BUBBLE_RIGHT_INDICATOR + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt index 9dec96933ee5..454419c805c1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt @@ -18,9 +18,12 @@ package com.android.wm.shell.desktopmode.multidesks import android.os.IBinder /** Represents shell-started transitions involving desks. */ -sealed class DeskTransition { +sealed interface DeskTransition { /** The transition token. */ - abstract val token: IBinder + val token: IBinder + + /** Returns a copy of this desk transition with a new transition token. */ + fun copyWithToken(token: IBinder): DeskTransition /** A transition to remove a desk and its tasks from a display. */ data class RemoveDesk( @@ -29,11 +32,15 @@ sealed class DeskTransition { val deskId: Int, val tasks: Set<Int>, val onDeskRemovedListener: OnDeskRemovedListener?, - ) : DeskTransition() + ) : DeskTransition { + override fun copyWithToken(token: IBinder): DeskTransition = copy(token) + } /** A transition to activate a desk in its display. */ data class ActivateDesk(override val token: IBinder, val displayId: Int, val deskId: Int) : - DeskTransition() + DeskTransition { + override fun copyWithToken(token: IBinder): DeskTransition = copy(token) + } /** A transition to activate a desk by moving an outside task to it. */ data class ActiveDeskWithTask( @@ -41,8 +48,12 @@ sealed class DeskTransition { val displayId: Int, val deskId: Int, val enterTaskId: Int, - ) : DeskTransition() + ) : DeskTransition { + override fun copyWithToken(token: IBinder): DeskTransition = copy(token) + } /** A transition to deactivate a desk. */ - data class DeactivateDesk(override val token: IBinder, val deskId: Int) : DeskTransition() + data class DeactivateDesk(override val token: IBinder, val deskId: Int) : DeskTransition { + override fun copyWithToken(token: IBinder): DeskTransition = copy(token) + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt index b521b2e8c942..588b5c350330 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt @@ -39,6 +39,7 @@ class DesksTransitionObserver( val transitions = deskTransitions[transition.token] ?: mutableSetOf() transitions += transition deskTransitions[transition.token] = transitions + logD("Added pending desk transition: %s", transition) } /** @@ -51,6 +52,43 @@ class DesksTransitionObserver( deskTransitions.forEach { deskTransition -> handleDeskTransition(info, deskTransition) } } + /** + * Called when a transition is merged with another transition, which may include transitions not + * tracked by this observer. + */ + fun onTransitionMerged(merged: IBinder, playing: IBinder) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return + val transitions = deskTransitions.remove(merged) ?: return + deskTransitions[playing] = + transitions + .map { deskTransition -> deskTransition.copyWithToken(token = playing) } + .toMutableSet() + } + + /** + * Called when any transition finishes, which may include transitions not tracked by this + * observer. + * + * Most [DeskTransition]s are not handled here because [onTransitionReady] handles them and + * removes them from the map. However, there can be cases where the transition was added after + * [onTransitionReady] had already been called and they need to be handled here, such as the + * swipe-to-home recents transition when there is no book-end transition. + */ + fun onTransitionFinished(transition: IBinder) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return + val deskTransitions = deskTransitions.remove(transition) ?: return + deskTransitions.forEach { deskTransition -> + if (deskTransition is DeskTransition.DeactivateDesk) { + handleDeactivateDeskTransition(null, deskTransition) + } else { + logW( + "Unexpected desk transition finished without being handled: %s", + deskTransition, + ) + } + } + } + private fun handleDeskTransition(info: TransitionInfo, deskTransition: DeskTransition) { logD("Desk transition ready: %s", deskTransition) val desktopRepository = desktopUserRepositories.current @@ -102,41 +140,54 @@ class DesksTransitionObserver( ) } } - is DeskTransition.DeactivateDesk -> { - var visibleDeactivation = false - for (change in info.changes) { - val isDeskChange = desksOrganizer.isDeskChange(change, deskTransition.deskId) - if (isDeskChange) { - visibleDeactivation = true - continue - } - val taskId = change.taskInfo?.taskId ?: continue - val removedFromDesk = - desktopRepository.getDeskIdForTask(taskId) == deskTransition.deskId && - desksOrganizer.getDeskAtEnd(change) == null - if (removedFromDesk) { - desktopRepository.removeTaskFromDesk( - deskId = deskTransition.deskId, - taskId = taskId, - ) - } - } - // Always deactivate even if there's no change that confirms the desk was - // deactivated. Some interactions, such as the desk deactivating because it's - // occluded by a fullscreen task result in a transition change, but others, such - // as transitioning from an empty desk to home may not. - if (!visibleDeactivation) { - logD("Deactivating desk without transition change") - } - desktopRepository.setDeskInactive(deskId = deskTransition.deskId) + is DeskTransition.DeactivateDesk -> handleDeactivateDeskTransition(info, deskTransition) + } + } + + private fun handleDeactivateDeskTransition( + info: TransitionInfo?, + deskTransition: DeskTransition.DeactivateDesk, + ) { + logD("handleDeactivateDeskTransition: %s", deskTransition) + val desktopRepository = desktopUserRepositories.current + var deskChangeFound = false + + val changes = info?.changes ?: emptyList() + for (change in changes) { + val isDeskChange = desksOrganizer.isDeskChange(change, deskTransition.deskId) + if (isDeskChange) { + deskChangeFound = true + continue + } + val taskId = change.taskInfo?.taskId ?: continue + val removedFromDesk = + desktopRepository.getDeskIdForTask(taskId) == deskTransition.deskId && + desksOrganizer.getDeskAtEnd(change) == null + if (removedFromDesk) { + desktopRepository.removeTaskFromDesk( + deskId = deskTransition.deskId, + taskId = taskId, + ) } } + // Always deactivate even if there's no change that confirms the desk was + // deactivated. Some interactions, such as the desk deactivating because it's + // occluded by a fullscreen task result in a transition change, but others, such + // as transitioning from an empty desk to home may not. + if (!deskChangeFound) { + logD("Deactivating desk without transition change") + } + desktopRepository.setDeskInactive(deskId = deskTransition.deskId) } private fun logD(msg: String, vararg arguments: Any?) { ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) } + private fun logW(msg: String, vararg arguments: Any?) { + ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + private companion object { private const val TAG = "DesksTransitionObserver" } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java index 0bf2ea61b0a4..e7492f17835a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java @@ -207,6 +207,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs @Override public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) { + mDesksTransitionObserver.ifPresent(o -> o.onTransitionMerged(merged, playing)); if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) { // TODO(b/367268953): Remove when DesktopTaskListener is introduced. mDesktopImmersiveController.ifPresent(h -> h.onTransitionMerged(merged, playing)); @@ -232,6 +233,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs @Override public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) { + mDesksTransitionObserver.ifPresent(o -> o.onTransitionFinished(transition)); if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) { // TODO(b/367268953): Remove when DesktopTaskListener is introduced. mDesktopImmersiveController.ifPresent(h -> h.onTransitionFinished(transition, aborted)); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java index 4e341ac9b7eb..0e974ef9083b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -31,6 +31,8 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; +import android.os.Debug; +import android.util.Log; import android.view.SurfaceControl; import android.window.DesktopExperienceFlags; import android.window.DisplayAreaInfo; @@ -369,7 +371,13 @@ public class PipController implements ConfigurationChangeListener, mPipBoundsAlgorithm.applySnapFraction(toBounds, snapFraction); mPipBoundsState.setBounds(toBounds); } - t.setBounds(mPipTransitionState.getPipTaskToken(), mPipBoundsState.getBounds()); + if (mPipTransitionState.getPipTaskToken() == null) { + Log.wtf(TAG, "PipController.onDisplayChange no PiP task token" + + " state=" + mPipTransitionState.getState() + + " callers=\n" + Debug.getCallers(4, " ")); + } else { + t.setBounds(mPipTransitionState.getPipTaskToken(), mPipBoundsState.getBounds()); + } // Update the size spec in PipBoundsState afterwards. mPipBoundsState.updateMinMaxSize(mPipBoundsState.getAspectRatio()); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 3e03e001c49b..8e10f15a36cc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -1135,6 +1135,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, if (openingLeafCount > 0) { appearedTargets = new RemoteAnimationTarget[openingLeafCount]; } + boolean onlyOpeningPausedTasks = true; int nextTargetIdx = 0; for (int i = 0; i < openingTasks.size(); ++i) { final TransitionInfo.Change change = openingTasks.get(i); @@ -1188,6 +1189,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, " opening new leaf taskId=%d wasClosing=%b", target.taskId, wasClosing); mOpeningTasks.add(new TaskState(change, target.leash)); + onlyOpeningPausedTasks = false; } else { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " opening new taskId=%d", change.getTaskInfo().taskId); @@ -1196,10 +1198,17 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, // is only animating the leafs. startT.show(change.getLeash()); mOpeningTasks.add(new TaskState(change, null)); + onlyOpeningPausedTasks = false; } } didMergeThings = true; - mState = STATE_NEW_TASK; + if (!onlyOpeningPausedTasks) { + // If we are only opening paused leaf tasks, then we aren't actually quick + // switching or launching a new task from overview, and if Launcher requests to + // finish(toHome=false) as a response to the pausing tasks being opened again, + // we should allow that to be considered returningToApp + mState = STATE_NEW_TASK; + } } if (mPausingTasks.isEmpty()) { // The pausing tasks may be removed by the incoming closing tasks. @@ -1368,8 +1377,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.finishInner: toHome=%b userLeave=%b " - + "willFinishToHome=%b state=%d reason=%s", - mInstanceId, toHome, sendUserLeaveHint, mWillFinishToHome, mState, reason); + + "willFinishToHome=%b state=%d hasPausingTasks=%b reason=%s", + mInstanceId, toHome, sendUserLeaveHint, mWillFinishToHome, mState, + mPausingTasks != null, reason); final SurfaceControl.Transaction t = mFinishTransaction; final WindowContainerTransaction wct = new WindowContainerTransaction(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index a0fb62508cc1..e19ad23b2125 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -394,6 +394,9 @@ public class DefaultMixedHandler implements MixedTransitionHandler, if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) { ((RecentsMixedTransition) mixed).onAnimateRecentsDuringSplitFinishing( returnToApp, finishWct, finishT); + } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_DESKTOP) { + ((RecentsMixedTransition) mixed).onAnimateRecentsDuringDesktopFinishing( + returnToApp, finishWct); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java index 1e926c57ca61..cfefc1f1ac66 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java @@ -198,6 +198,15 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { } } + /** + * Called when the recents animation during desktop is about to finish. + */ + void onAnimateRecentsDuringDesktopFinishing(boolean returnToApp, + @NonNull WindowContainerTransaction finishWct) { + mDesktopTasksController.onRecentsInDesktopAnimationFinishing(mTransition, finishWct, + returnToApp); + } + @Override void mergeAnimation( @NonNull IBinder transition, @NonNull TransitionInfo info, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 003ef1d453fc..4f49ebcd2e83 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -602,6 +602,11 @@ public class Transitions implements RemoteCallable<Transitions>, // Just in case there is a race with another animation (eg. recents finish()). // Changes are visible->visible so it's a problem if it isn't visible. t.show(leash); + // If there is a transient launch followed by a launch of one of the pausing tasks, + // we may end up with TRANSIT_TO_BACK followed by a CHANGE (w/ flag MOVE_TO_TOP), + // but since we are hiding the leash in the finish transaction above, we should also + // update the finish transaction here to reflect the change in visibility + finishT.show(leash); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java index 3182745d813e..f6acca95916f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CarWindowDecoration.java @@ -27,6 +27,7 @@ import android.view.InsetsState; import android.view.SurfaceControl; import android.view.View; import android.view.WindowInsets; +import android.window.DesktopModeFlags; import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; @@ -88,6 +89,9 @@ public class CarWindowDecoration extends WindowDecoration<WindowDecorLinearLayou updateRelayoutParams(mRelayoutParams, taskInfo, isCaptionVisible); relayout(mRelayoutParams, startT, finishT, wct, mRootView, mResult); + if (DesktopModeFlags.ENABLE_DESKTOP_APP_HANDLE_ANIMATION.isTrue()) { + setCaptionVisibility(isCaptionVisible); + } // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo mBgExecutor.execute(() -> mTaskOrganizer.applyTransaction(wct)); @@ -102,6 +106,15 @@ public class CarWindowDecoration extends WindowDecoration<WindowDecorLinearLayou } } + private void setCaptionVisibility(boolean visible) { + if (mRootView == null) { + return; + } + final int v = visible ? View.VISIBLE : View.GONE; + final View captionView = mRootView.findViewById(getCaptionViewId()); + captionView.setVisibility(v); + } + @Override @NonNull Rect calculateValidDragArea() { 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 800faca830f4..07b385bdb045 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 @@ -108,6 +108,8 @@ import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.CompatUIController; +import com.android.wm.shell.compatui.api.CompatUIHandler; +import com.android.wm.shell.compatui.impl.CompatUIRequests; import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; import com.android.wm.shell.desktopmode.DesktopImmersiveController; import com.android.wm.shell.desktopmode.DesktopModeEventLogger; @@ -266,6 +268,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final DesktopTilingDecorViewModel mDesktopTilingDecorViewModel; private final MultiDisplayDragMoveIndicatorController mMultiDisplayDragMoveIndicatorController; private final LatencyTracker mLatencyTracker; + private final CompatUIHandler mCompatUI; public DesktopModeWindowDecorViewModel( Context context, @@ -306,7 +309,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, RecentsTransitionHandler recentsTransitionHandler, DesktopModeCompatPolicy desktopModeCompatPolicy, DesktopTilingDecorViewModel desktopTilingDecorViewModel, - MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController) { + MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController, + CompatUIHandler compatUI) { this( context, shellExecutor, @@ -353,7 +357,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, recentsTransitionHandler, desktopModeCompatPolicy, desktopTilingDecorViewModel, - multiDisplayDragMoveIndicatorController); + multiDisplayDragMoveIndicatorController, + compatUI); } @VisibleForTesting @@ -403,7 +408,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, RecentsTransitionHandler recentsTransitionHandler, DesktopModeCompatPolicy desktopModeCompatPolicy, DesktopTilingDecorViewModel desktopTilingDecorViewModel, - MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController) { + MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController, + CompatUIHandler compatUI) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; @@ -444,6 +450,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mActivityOrientationChangeHandler = activityOrientationChangeHandler; mAssistContentRequester = assistContentRequester; mWindowDecorViewHostSupplier = windowDecorViewHostSupplier; + mCompatUI = compatUI; mOnDisplayChangingListener = (displayId, fromRotation, toRotation, displayAreaInfo, t) -> { DesktopModeWindowDecoration decoration; RunningTaskInfo taskInfo; @@ -1864,6 +1871,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, CompatUIController.launchUserAspectRatioSettings(mContext, taskInfo); return Unit.INSTANCE; }); + windowDecoration.setOnRestartClickListener(() -> { + mCompatUI.sendCompatUIRequest(new CompatUIRequests.DisplayCompatShowRestartDialog( + taskInfo.taskId)); + return Unit.INSTANCE; + }); windowDecoration.setOnMaximizeHoverListener(() -> { if (!windowDecoration.isMaximizeMenuActive()) { mDesktopModeUiEventLogger.log(taskInfo, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index e8019e47e374..49c380a1a736 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -163,6 +163,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private Function0<Unit> mOnNewWindowClickListener; private Function0<Unit> mOnManageWindowsClickListener; private Function0<Unit> mOnChangeAspectRatioClickListener; + private Function0<Unit> mOnRestartClickListener; private Function0<Unit> mOnMaximizeHoverListener; private DragPositioningCallback mDragPositioningCallback; private DragResizeInputListener mDragResizeListener; @@ -408,6 +409,11 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mOnChangeAspectRatioClickListener = listener; } + /** Registers a listener to be called when the aspect ratio action is triggered. */ + void setOnRestartClickListener(Function0<Unit> listener) { + mOnRestartClickListener = listener; + } + /** Registers a listener to be called when the maximize header button is hovered. */ void setOnMaximizeHoverListener(Function0<Unit> listener) { mOnMaximizeHoverListener = listener; @@ -863,7 +869,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (!isAppHandle(mWindowDecorViewHolder)) return; asAppHandle(mWindowDecorViewHolder).bindData(new AppHandleViewHolder.HandleData( mTaskInfo, determineHandlePosition(), mResult.mCaptionWidth, - mResult.mCaptionHeight, isCaptionVisible() + mResult.mCaptionHeight, /* showInputLayer= */ isCaptionVisible(), + /* isCaptionVisible= */ isCaptionVisible() )); } @@ -876,7 +883,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin inFullImmersive, hasGlobalFocus, /* maximizeHoverEnabled= */ canOpenMaximizeMenu( - /* animatingTaskResizeOrReposition= */ false) + /* animatingTaskResizeOrReposition= */ false), + isCaptionVisible() )); } @@ -1459,6 +1467,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin && mMinimumInstancesFound; final boolean shouldShowChangeAspectRatioButton = HandleMenu.Companion .shouldShowChangeAspectRatioButton(mTaskInfo); + final boolean shouldShowRestartButton = HandleMenu.Companion + .shouldShowRestartButton(mTaskInfo); final boolean inDesktopImmersive = mDesktopUserRepositories.getProfile(mTaskInfo.userId) .isTaskInFullImmersiveState(mTaskInfo.taskId); final boolean isBrowserApp = isBrowserApp(); @@ -1475,6 +1485,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin shouldShowManageWindowsButton, shouldShowChangeAspectRatioButton, isDesktopModeSupportedOnDisplay(mContext, mDisplay), + shouldShowRestartButton, isBrowserApp, isBrowserApp ? getAppLink() : getBrowserLink(), mResult.mCaptionWidth, @@ -1511,6 +1522,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } return Unit.INSTANCE; }, + /* onRestartClickListener= */ mOnRestartClickListener, /* onCloseMenuClickListener= */ () -> { closeHandleMenu(); return Unit.INSTANCE; @@ -1866,7 +1878,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin DesktopModeUtils.isTaskMaximized(mTaskInfo, mDisplayController), inFullImmersive, isFocused(), - /* maximizeHoverEnabled= */ canOpenMaximizeMenu(animatingTaskResizeOrReposition))); + /* maximizeHoverEnabled= */ canOpenMaximizeMenu(animatingTaskResizeOrReposition), + isCaptionVisible())); } /** 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 9cc64ac9c276..f64b0d8695d2 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 @@ -37,7 +37,6 @@ import android.view.WindowInsets.Type.systemBars import android.view.WindowManager import android.widget.ImageButton import android.widget.ImageView -import android.widget.LinearLayout import android.widget.Space import android.window.DesktopModeFlags import android.window.SurfaceSyncGroup @@ -95,6 +94,7 @@ class HandleMenu( private val shouldShowManageWindowsButton: Boolean, private val shouldShowChangeAspectRatioButton: Boolean, private val shouldShowDesktopModeButton: Boolean, + private val shouldShowRestartButton: Boolean, private val isBrowserApp: Boolean, private val openInAppOrBrowserIntent: Intent?, private val captionWidth: Int, @@ -139,7 +139,8 @@ class HandleMenu( private val shouldShowMoreActionsPill: Boolean get() = SHOULD_SHOW_SCREENSHOT_BUTTON || shouldShowNewWindowButton || - shouldShowManageWindowsButton || shouldShowChangeAspectRatioButton + shouldShowManageWindowsButton || shouldShowChangeAspectRatioButton || + shouldShowRestartButton private var loadAppInfoJob: Job? = null @@ -157,6 +158,7 @@ class HandleMenu( onChangeAspectRatioClickListener: () -> Unit, openInAppOrBrowserClickListener: (Intent) -> Unit, onOpenByDefaultClickListener: () -> Unit, + onRestartClickListener: () -> Unit, onCloseMenuClickListener: () -> Unit, onOutsideTouchListener: () -> Unit, forceShowSystemBars: Boolean = false, @@ -176,6 +178,7 @@ class HandleMenu( onChangeAspectRatioClickListener = onChangeAspectRatioClickListener, openInAppOrBrowserClickListener = openInAppOrBrowserClickListener, onOpenByDefaultClickListener = onOpenByDefaultClickListener, + onRestartClickListener = onRestartClickListener, onCloseMenuClickListener = onCloseMenuClickListener, onOutsideTouchListener = onOutsideTouchListener, forceShowSystemBars = forceShowSystemBars, @@ -198,6 +201,7 @@ class HandleMenu( onChangeAspectRatioClickListener: () -> Unit, openInAppOrBrowserClickListener: (Intent) -> Unit, onOpenByDefaultClickListener: () -> Unit, + onRestartClickListener: () -> Unit, onCloseMenuClickListener: () -> Unit, onOutsideTouchListener: () -> Unit, forceShowSystemBars: Boolean = false, @@ -212,6 +216,7 @@ class HandleMenu( shouldShowManageWindowsButton = shouldShowManageWindowsButton, shouldShowChangeAspectRatioButton = shouldShowChangeAspectRatioButton, shouldShowDesktopModeButton = shouldShowDesktopModeButton, + shouldShowRestartButton = shouldShowRestartButton, isBrowserApp = isBrowserApp ).apply { bind(taskInfo, shouldShowMoreActionsPill) @@ -225,6 +230,7 @@ class HandleMenu( this.onOpenInAppOrBrowserClickListener = { openInAppOrBrowserClickListener.invoke(openInAppOrBrowserIntent!!) } + this.onRestartClickListener = onRestartClickListener this.onOpenByDefaultClickListener = onOpenByDefaultClickListener this.onCloseMenuClickListener = onCloseMenuClickListener this.onOutsideTouchListener = onOutsideTouchListener @@ -431,6 +437,10 @@ class HandleMenu( R.dimen.desktop_mode_handle_menu_change_aspect_ratio_height ) } + if (!shouldShowRestartButton) { + menuHeight -= loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_restart_button_height) + } if (!shouldShowMoreActionsPill) { menuHeight -= pillTopMargin } @@ -473,6 +483,7 @@ class HandleMenu( private val shouldShowManageWindowsButton: Boolean, private val shouldShowChangeAspectRatioButton: Boolean, private val shouldShowDesktopModeButton: Boolean, + private val shouldShowRestartButton: Boolean, private val isBrowserApp: Boolean ) { val rootView = LayoutInflater.from(context) @@ -501,6 +512,12 @@ class HandleMenu( t = iconButtondrawableBaseInset, b = iconButtondrawableBaseInset, l = 0, r = iconButtondrawableShiftInset ) + private val iconButtonDrawableInsetStart + get() = + if (context.isRtl) iconButtonDrawableInsetsRight else iconButtonDrawableInsetsLeft + private val iconButtonDrawableInsetEnd + get() = + if (context.isRtl) iconButtonDrawableInsetsLeft else iconButtonDrawableInsetsRight // App Info Pill. private val appInfoPill = rootView.requireViewById<View>(R.id.app_info_pill) @@ -544,14 +561,15 @@ class HandleMenu( .requireViewById<HandleMenuActionButton>(R.id.manage_windows_button) private val changeAspectRatioBtn = moreActionsPill .requireViewById<HandleMenuActionButton>(R.id.change_aspect_ratio_button) + private val restartBtn = moreActionsPill + .requireViewById<HandleMenuActionButton>(R.id.handle_menu_restart_button) // Open in Browser/App Pill. private val openInAppOrBrowserPill = rootView.requireViewById<View>( R.id.open_in_app_or_browser_pill ) - private val openInAppOrBrowserBtn = openInAppOrBrowserPill.requireViewById<View>( - R.id.open_in_app_or_browser_button - ) + private val openInAppOrBrowserBtn = openInAppOrBrowserPill + .requireViewById<HandleMenuActionButton>(R.id.open_in_app_or_browser_button) private val openByDefaultBtn = openInAppOrBrowserPill.requireViewById<ImageButton>( R.id.open_by_default_button ) @@ -570,6 +588,7 @@ class HandleMenu( var onChangeAspectRatioClickListener: (() -> Unit)? = null var onOpenInAppOrBrowserClickListener: (() -> Unit)? = null var onOpenByDefaultClickListener: (() -> Unit)? = null + var onRestartClickListener: (() -> Unit)? = null var onCloseMenuClickListener: (() -> Unit)? = null var onOutsideTouchListener: (() -> Unit)? = null @@ -586,6 +605,7 @@ class HandleMenu( newWindowBtn.setOnClickListener { onNewWindowClickListener?.invoke() } manageWindowBtn.setOnClickListener { onManageWindowsClickListener?.invoke() } changeAspectRatioBtn.setOnClickListener { onChangeAspectRatioClickListener?.invoke() } + restartBtn.setOnClickListener { onRestartClickListener?.invoke() } rootView.setOnTouchListener { _, event -> if (event.actionMasked == ACTION_OUTSIDE) { @@ -758,20 +778,16 @@ class HandleMenu( floatingBtn.isEnabled = !taskInfo.isPinned floatingBtn.imageTintList = style.windowingButtonColor desktopBtn.isGone = !shouldShowDesktopModeButton + desktopBtnSpace.isGone = !shouldShowDesktopModeButton desktopBtn.isSelected = taskInfo.isFreeform desktopBtn.isEnabled = !taskInfo.isFreeform desktopBtn.imageTintList = style.windowingButtonColor - val startInsets = if (context.isRtl) iconButtonDrawableInsetsRight - else iconButtonDrawableInsetsLeft - val endInsets = if (context.isRtl) iconButtonDrawableInsetsLeft - else iconButtonDrawableInsetsRight - fullscreenBtn.apply { background = createBackgroundDrawable( color = style.textColor, cornerRadius = iconButtonRippleRadius, - drawableInsets = startInsets + drawableInsets = iconButtonDrawableInsetStart ) } @@ -795,7 +811,7 @@ class HandleMenu( background = createBackgroundDrawable( color = style.textColor, cornerRadius = iconButtonRippleRadius, - drawableInsets = endInsets + drawableInsets = iconButtonDrawableInsetEnd ) } } @@ -808,20 +824,16 @@ class HandleMenu( newWindowBtn to shouldShowNewWindowButton, manageWindowBtn to shouldShowManageWindowsButton, changeAspectRatioBtn to shouldShowChangeAspectRatioButton, - ).forEach { - val button = it.first - val shouldShow = it.second - - val buttonRoot = button.requireViewById<LinearLayout>(R.id.action_button) - val label = buttonRoot.requireViewById<MarqueedTextView>(R.id.label) - val image = buttonRoot.requireViewById<ImageView>(R.id.image) - - button.isGone = !shouldShow - label.apply { - setTextColor(style.textColor) - startMarquee() + restartBtn to shouldShowRestartButton, + ).forEach { (button, shouldShow) -> + button.apply { + isGone = !shouldShow + textView.apply { + setTextColor(style.textColor) + startMarquee() + } + iconView.imageTintList = ColorStateList.valueOf(style.textColor) } - image.imageTintList = ColorStateList.valueOf(style.textColor) } } @@ -837,20 +849,24 @@ class HandleMenu( getString(R.string.open_in_browser_text) } - val buttonRoot = openInAppOrBrowserBtn.requireViewById<LinearLayout>(R.id.action_button) - val label = openInAppOrBrowserBtn.requireViewById<MarqueedTextView>(R.id.label) - val image = openInAppOrBrowserBtn.requireViewById<ImageView>(R.id.image) - openInAppOrBrowserBtn.contentDescription = btnText - buttonRoot.contentDescription = btnText - label.apply { - text = btnText - setTextColor(style.textColor) - startMarquee() + openInAppOrBrowserBtn.apply { + contentDescription = btnText + textView.apply { + text = btnText + setTextColor(style.textColor) + startMarquee() + } + iconView.imageTintList = ColorStateList.valueOf(style.textColor) } - image.imageTintList = ColorStateList.valueOf(style.textColor) - openByDefaultBtn.isGone = isBrowserApp - openByDefaultBtn.imageTintList = ColorStateList.valueOf(style.textColor) + openByDefaultBtn.apply { + isGone = isBrowserApp + imageTintList = ColorStateList.valueOf(style.textColor) + background = createBackgroundDrawable( + color = style.textColor, + cornerRadius = iconButtonRippleRadius, + drawableInsets = iconButtonDrawableInsetEnd) + } } private fun getString(@StringRes resId: Int): String = context.resources.getString(resId) @@ -873,6 +889,13 @@ class HandleMenu( fun shouldShowChangeAspectRatioButton(taskInfo: RunningTaskInfo): Boolean = taskInfo.appCompatTaskInfo.eligibleForUserAspectRatioButton() && taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FULLSCREEN + + /** + * Returns whether the restart button should be shown for the task. It usually means that + * the task has moved to a different display. + */ + fun shouldShowRestartButton(taskInfo: RunningTaskInfo): Boolean = + taskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove } } @@ -891,6 +914,7 @@ interface HandleMenuFactory { shouldShowManageWindowsButton: Boolean, shouldShowChangeAspectRatioButton: Boolean, shouldShowDesktopModeButton: Boolean, + shouldShowRestartButton: Boolean, isBrowserApp: Boolean, openInAppOrBrowserIntent: Intent?, captionWidth: Int, @@ -915,6 +939,7 @@ object DefaultHandleMenuFactory : HandleMenuFactory { shouldShowManageWindowsButton: Boolean, shouldShowChangeAspectRatioButton: Boolean, shouldShowDesktopModeButton: Boolean, + shouldShowRestartButton: Boolean, isBrowserApp: Boolean, openInAppOrBrowserIntent: Intent?, captionWidth: Int, @@ -935,6 +960,7 @@ object DefaultHandleMenuFactory : HandleMenuFactory { shouldShowManageWindowsButton, shouldShowChangeAspectRatioButton, shouldShowDesktopModeButton, + shouldShowRestartButton, isBrowserApp, openInAppOrBrowserIntent, captionWidth, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuActionButton.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuActionButton.kt index a723a7a4ac20..7aba54eef899 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuActionButton.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuActionButton.kt @@ -39,20 +39,18 @@ class HandleMenuActionButton @JvmOverloads constructor( defStyleAttr: Int = 0 ) : LinearLayout(context, attrs, defStyleAttr) { - private val rootElement: LinearLayout - private val iconView: ImageView - private val textView: MarqueedTextView + val iconView: ImageView + val textView: MarqueedTextView init { - val view = LayoutInflater.from(context).inflate( + LayoutInflater.from(context).inflate( R.layout.desktop_mode_window_decor_handle_menu_action_button, this, true) - rootElement = findViewById(R.id.action_button) iconView = findViewById(R.id.image) textView = findViewById(R.id.label) context.withStyledAttributes(attrs, R.styleable.HandleMenuActionButton) { + contentDescription = getString(R.styleable.HandleMenuActionButton_android_text) textView.text = getString(R.styleable.HandleMenuActionButton_android_text) - rootElement.contentDescription = getString(R.styleable.HandleMenuActionButton_android_text) textView.setTextColor(getColor(R.styleable.HandleMenuActionButton_android_textColor, 0)) iconView.setImageResource(getResourceId( R.styleable.HandleMenuActionButton_android_src, 0)) @@ -62,15 +60,6 @@ class HandleMenuActionButton @JvmOverloads constructor( } /** - * Sets a listener to be invoked when this view is clicked. - * - * @param l the [OnClickListener] that receives click events. - */ - override fun setOnClickListener(l: OnClickListener?) { - rootElement.setOnClickListener(l) - } - - /** * Sets the text color for the text inside the button. * * @param color the color to set for the text, as a color integer. 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 6fd963f4203d..6a9b366dfb97 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 @@ -48,6 +48,7 @@ import android.view.View; import android.view.WindowManager; import android.view.WindowlessWindowManager; import android.window.DesktopExperienceFlags; +import android.window.DesktopModeFlags; import android.window.SurfaceSyncGroup; import android.window.TaskConstants; import android.window.WindowContainerToken; @@ -652,7 +653,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> */ private void updateCaptionVisibility(View rootView, @NonNull RelayoutParams params) { mIsCaptionVisible = params.mIsCaptionVisible; - setCaptionVisibility(rootView, mIsCaptionVisible); + if (!DesktopModeFlags.ENABLE_DESKTOP_APP_HANDLE_ANIMATION.isTrue()) { + setCaptionVisibility(rootView, mIsCaptionVisible); + } } void setTaskDragResizer(TaskDragResizer taskDragResizer) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleAnimator.kt new file mode 100644 index 000000000000..f0a85306d177 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleAnimator.kt @@ -0,0 +1,101 @@ +/* + * 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 + +import android.animation.ObjectAnimator +import android.view.View +import android.view.View.Visibility +import android.view.animation.PathInterpolator +import android.widget.ImageButton +import androidx.core.animation.doOnEnd +import com.android.wm.shell.shared.animation.Interpolators + +/** + * Animates the Desktop View's app handle. + */ +class AppHandleAnimator( + private val appHandleView: View, + private val captionHandle: ImageButton, +) { + companion object { + // Constants for animating the whole caption + private const val APP_HANDLE_ALPHA_FADE_IN_ANIMATION_DURATION_MS: Long = 275L + private const val APP_HANDLE_ALPHA_FADE_OUT_ANIMATION_DURATION_MS: Long = 340 + private val APP_HANDLE_ANIMATION_INTERPOLATOR = PathInterpolator( + 0.4f, + 0f, + 0.2f, + 1f + ) + + // Constants for animating the caption's handle + private const val HANDLE_ANIMATION_DURATION: Long = 100 + private val HANDLE_ANIMATION_INTERPOLATOR = Interpolators.FAST_OUT_SLOW_IN + } + + private var animator: ObjectAnimator? = null + + /** Animates the given caption view to the given visibility after a visibility change. */ + fun animateVisibilityChange(@Visibility visible: Int) { + when (visible) { + View.VISIBLE -> animateShowAppHandle() + else -> animateHideAppHandle() + } + } + + /** Animate appearance/disappearance of caption's handle. */ + fun animateCaptionHandleAlpha(startValue: Float, endValue: Float) { + cancel() + animator = ObjectAnimator.ofFloat(captionHandle, View.ALPHA, startValue, endValue).apply { + duration = HANDLE_ANIMATION_DURATION + interpolator = HANDLE_ANIMATION_INTERPOLATOR + start() + } + } + + private fun animateShowAppHandle() { + cancel() + appHandleView.alpha = 0f + appHandleView.visibility = View.VISIBLE + animator = ObjectAnimator.ofFloat(appHandleView, View.ALPHA, 1f).apply { + duration = APP_HANDLE_ALPHA_FADE_IN_ANIMATION_DURATION_MS + interpolator = APP_HANDLE_ANIMATION_INTERPOLATOR + start() + } + } + + private fun animateHideAppHandle() { + cancel() + animator = ObjectAnimator.ofFloat(appHandleView, View.ALPHA, 0f).apply { + duration = APP_HANDLE_ALPHA_FADE_OUT_ANIMATION_DURATION_MS + interpolator = APP_HANDLE_ANIMATION_INTERPOLATOR + doOnEnd { + appHandleView.visibility = View.GONE + } + start() + } + } + + /** + * Cancels any active animations. + */ + fun cancel() { + animator?.removeAllListeners() + animator?.cancel() + animator = null + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt index 0985587a330e..9d16be59ba34 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt @@ -15,7 +15,6 @@ */ package com.android.wm.shell.windowdecor.viewholder -import android.animation.ObjectAnimator import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.res.ColorStateList @@ -40,8 +39,8 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.Accessibilit import com.android.internal.policy.SystemBarUtils import com.android.window.flags.Flags import com.android.wm.shell.R -import com.android.wm.shell.shared.animation.Interpolators import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper +import com.android.wm.shell.windowdecor.AppHandleAnimator import com.android.wm.shell.windowdecor.WindowManagerWrapper import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer @@ -57,22 +56,20 @@ class AppHandleViewHolder( private val handler: Handler ) : WindowDecorationViewHolder<AppHandleViewHolder.HandleData>(rootView) { - companion object { - private const val CAPTION_HANDLE_ANIMATION_DURATION: Long = 100 - } - data class HandleData( val taskInfo: RunningTaskInfo, val position: Point, val width: Int, val height: Int, - val showInputLayer: Boolean + val showInputLayer: Boolean, + val isCaptionVisible: Boolean, ) : Data() private lateinit var taskInfo: RunningTaskInfo private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption) private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle) private val inputManager = context.getSystemService(InputManager::class.java) + private val animator: AppHandleAnimator = AppHandleAnimator(rootView, captionHandle) private var statusBarInputLayerExists = false // An invisible View that takes up the same coordinates as captionHandle but is layered @@ -101,7 +98,14 @@ class AppHandleViewHolder( } override fun bindData(data: HandleData) { - bindData(data.taskInfo, data.position, data.width, data.height, data.showInputLayer) + bindData( + data.taskInfo, + data.position, + data.width, + data.height, + data.showInputLayer, + data.isCaptionVisible + ) } private fun bindData( @@ -109,8 +113,10 @@ class AppHandleViewHolder( position: Point, width: Int, height: Int, - showInputLayer: Boolean + showInputLayer: Boolean, + isCaptionVisible: Boolean ) { + setVisibility(isCaptionVisible) captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo)) this.taskInfo = taskInfo // If handle is not in status bar region(i.e., bottom stage in vertical split), @@ -131,11 +137,11 @@ class AppHandleViewHolder( } override fun onHandleMenuOpened() { - animateCaptionHandleAlpha(startValue = 1f, endValue = 0f) + animator.animateCaptionHandleAlpha(startValue = 1f, endValue = 0f) } override fun onHandleMenuClosed() { - animateCaptionHandleAlpha(startValue = 0f, endValue = 1f) + animator.animateCaptionHandleAlpha(startValue = 0f, endValue = 1f) } private fun createStatusBarInputLayer(handlePosition: Point, @@ -239,6 +245,17 @@ class AppHandleViewHolder( } } + private fun setVisibility(visible: Boolean) { + val v = if (visible) View.VISIBLE else View.GONE + if ( + captionView.visibility == v || + !DesktopModeFlags.ENABLE_DESKTOP_APP_HANDLE_ANIMATION.isTrue() + ) { + return + } + animator.animateVisibilityChange(v) + } + private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int { return if (shouldUseLightCaptionColors(taskInfo)) { context.getColor(R.color.desktop_mode_caption_handle_bar_light) @@ -264,18 +281,10 @@ class AppHandleViewHolder( } ?: false } - /** Animate appearance/disappearance of caption handle as the handle menu is animated. */ - private fun animateCaptionHandleAlpha(startValue: Float, endValue: Float) { - val animator = - ObjectAnimator.ofFloat(captionHandle, View.ALPHA, startValue, endValue).apply { - duration = CAPTION_HANDLE_ANIMATION_DURATION - interpolator = Interpolators.FAST_OUT_SLOW_IN - } - animator.start() + override fun close() { + animator.cancel() } - override fun close() {} - /** Factory class for creating [AppHandleViewHolder] objects. */ class Factory { /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt index 30712b55bdfa..0e2698d0b6fa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -83,6 +83,7 @@ class AppHeaderViewHolder( val inFullImmersiveState: Boolean, val hasGlobalFocus: Boolean, val enableMaximizeLongClick: Boolean, + val isCaptionVisible: Boolean, ) : Data() private val decorThemeUtil = DecorThemeUtil(context) @@ -264,7 +265,8 @@ class AppHeaderViewHolder( data.isTaskMaximized, data.inFullImmersiveState, data.hasGlobalFocus, - data.enableMaximizeLongClick + data.enableMaximizeLongClick, + data.isCaptionVisible, ) } @@ -306,6 +308,7 @@ class AppHeaderViewHolder( inFullImmersiveState: Boolean, hasGlobalFocus: Boolean, enableMaximizeLongClick: Boolean, + isCaptionVisible: Boolean, ) { if (DesktopModeFlags.ENABLE_THEMED_APP_HEADERS.isTrue()) { bindDataWithThemedHeaders( @@ -314,13 +317,21 @@ class AppHeaderViewHolder( inFullImmersiveState, hasGlobalFocus, enableMaximizeLongClick, + isCaptionVisible, ) } else { - bindDataLegacy(taskInfo, hasGlobalFocus) + bindDataLegacy(taskInfo, hasGlobalFocus, isCaptionVisible) } } - private fun bindDataLegacy(taskInfo: RunningTaskInfo, hasGlobalFocus: Boolean) { + private fun bindDataLegacy( + taskInfo: RunningTaskInfo, + hasGlobalFocus: Boolean, + isCaptionVisible: Boolean, + ) { + if (DesktopModeFlags.ENABLE_DESKTOP_APP_HANDLE_ANIMATION.isTrue()) { + setCaptionVisibility(isCaptionVisible) + } captionView.setBackgroundColor(getCaptionBackgroundColor(taskInfo, hasGlobalFocus)) val color = getAppNameAndButtonColor(taskInfo, hasGlobalFocus) val alpha = Color.alpha(color) @@ -359,10 +370,15 @@ class AppHeaderViewHolder( inFullImmersiveState: Boolean, hasGlobalFocus: Boolean, enableMaximizeLongClick: Boolean, + isCaptionVisible: Boolean, ) { val header = fillHeaderInfo(taskInfo, hasGlobalFocus) val headerStyle = getHeaderStyle(header) + if (DesktopModeFlags.ENABLE_DESKTOP_APP_HANDLE_ANIMATION.isTrue()) { + setCaptionVisibility(isCaptionVisible) + } + // Caption Background when (headerStyle.background) { is HeaderStyle.Background.Opaque -> { @@ -464,6 +480,11 @@ class AppHeaderViewHolder( } } + private fun setCaptionVisibility(visible: Boolean) { + val v = if (visible) View.VISIBLE else View.GONE + captionView.visibility = v + } + override fun onHandleMenuOpened() {} override fun onHandleMenuClosed() {} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.java deleted file mode 100644 index 25dbc64f83de..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (C) 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.common.pip; - -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 com.android.window.flags.Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_PIP; -import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP; -import static com.android.wm.shell.Flags.FLAG_ENABLE_PIP2; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertTrue; - -import static org.mockito.Mockito.when; - -import android.app.ActivityManager; -import android.platform.test.annotations.EnableFlags; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.window.DisplayAreaInfo; -import android.window.WindowContainerToken; - -import androidx.test.filters.SmallTest; - -import com.android.wm.shell.RootTaskDisplayAreaOrganizer; -import com.android.wm.shell.desktopmode.DesktopRepository; -import com.android.wm.shell.desktopmode.DesktopUserRepositories; -import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import java.util.Optional; - -/** - * Unit test against {@link PipDesktopState}. - */ -@SmallTest -@TestableLooper.RunWithLooper -@RunWith(AndroidTestingRunner.class) -@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) -public class PipDesktopStateTest { - @Mock private PipDisplayLayoutState mMockPipDisplayLayoutState; - @Mock private Optional<DesktopUserRepositories> mMockDesktopUserRepositoriesOptional; - @Mock private DesktopUserRepositories mMockDesktopUserRepositories; - @Mock private DesktopRepository mMockDesktopRepository; - @Mock - private Optional<DragToDesktopTransitionHandler> mMockDragToDesktopTransitionHandlerOptional; - @Mock private DragToDesktopTransitionHandler mMockDragToDesktopTransitionHandler; - - @Mock private RootTaskDisplayAreaOrganizer mMockRootTaskDisplayAreaOrganizer; - @Mock private ActivityManager.RunningTaskInfo mMockTaskInfo; - - private static final int DISPLAY_ID = 1; - private DisplayAreaInfo mDefaultTda; - private PipDesktopState mPipDesktopState; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - when(mMockDesktopUserRepositoriesOptional.get()).thenReturn(mMockDesktopUserRepositories); - when(mMockDesktopUserRepositories.getCurrent()).thenReturn(mMockDesktopRepository); - when(mMockDesktopUserRepositoriesOptional.isPresent()).thenReturn(true); - - when(mMockDragToDesktopTransitionHandlerOptional.get()).thenReturn( - mMockDragToDesktopTransitionHandler); - when(mMockDragToDesktopTransitionHandlerOptional.isPresent()).thenReturn(true); - - when(mMockTaskInfo.getDisplayId()).thenReturn(DISPLAY_ID); - when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(DISPLAY_ID); - - mDefaultTda = new DisplayAreaInfo(Mockito.mock(WindowContainerToken.class), DISPLAY_ID, 0); - when(mMockRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DISPLAY_ID)).thenReturn( - mDefaultTda); - - mPipDesktopState = new PipDesktopState(mMockPipDisplayLayoutState, - mMockDesktopUserRepositoriesOptional, - mMockDragToDesktopTransitionHandlerOptional, - mMockRootTaskDisplayAreaOrganizer); - } - - @Test - public void isDesktopWindowingPipEnabled_returnsTrue() { - assertTrue(mPipDesktopState.isDesktopWindowingPipEnabled()); - } - - @Test - public void isDesktopWindowingPipEnabled_desktopRepositoryEmpty_returnsFalse() { - when(mMockDesktopUserRepositoriesOptional.isPresent()).thenReturn(false); - - assertFalse(mPipDesktopState.isDesktopWindowingPipEnabled()); - } - - @Test - public void isDesktopWindowingPipEnabled_dragToDesktopTransitionHandlerEmpty_returnsFalse() { - when(mMockDragToDesktopTransitionHandlerOptional.isPresent()).thenReturn(false); - - assertFalse(mPipDesktopState.isDesktopWindowingPipEnabled()); - } - - @Test - @EnableFlags({ - FLAG_ENABLE_CONNECTED_DISPLAYS_PIP, FLAG_ENABLE_PIP2 - }) - public void isConnectedDisplaysPipEnabled_returnsTrue() { - assertTrue(mPipDesktopState.isConnectedDisplaysPipEnabled()); - } - - @Test - public void isPipInDesktopMode_anyDeskActive_returnsTrue() { - when(mMockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true); - - assertTrue(mPipDesktopState.isPipInDesktopMode()); - } - - @Test - public void isPipInDesktopMode_noDeskActive_returnsFalse() { - when(mMockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(false); - - assertFalse(mPipDesktopState.isPipInDesktopMode()); - } - - @Test - public void getOutPipWindowingMode_exitToDesktop_displayFreeform_returnsUndefined() { - when(mMockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true); - setDisplayWindowingMode(WINDOWING_MODE_FREEFORM); - - assertEquals(WINDOWING_MODE_UNDEFINED, mPipDesktopState.getOutPipWindowingMode()); - } - - @Test - public void getOutPipWindowingMode_exitToDesktop_displayFullscreen_returnsFreeform() { - when(mMockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true); - setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN); - - assertEquals(WINDOWING_MODE_FREEFORM, mPipDesktopState.getOutPipWindowingMode()); - } - - @Test - public void getOutPipWindowingMode_exitToFullscreen_displayFullscreen_returnsUndefined() { - setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN); - - assertEquals(WINDOWING_MODE_UNDEFINED, mPipDesktopState.getOutPipWindowingMode()); - } - - @Test - public void isDragToDesktopInProgress_inProgress_returnsTrue() { - when(mMockDragToDesktopTransitionHandler.getInProgress()).thenReturn(true); - - assertTrue(mPipDesktopState.isDragToDesktopInProgress()); - } - - @Test - public void isDragToDesktopInProgress_notInProgress_returnsFalse() { - when(mMockDragToDesktopTransitionHandler.getInProgress()).thenReturn(false); - - assertFalse(mPipDesktopState.isDragToDesktopInProgress()); - } - - private void setDisplayWindowingMode(int windowingMode) { - mDefaultTda.configuration.windowConfiguration.setWindowingMode(windowingMode); - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.kt new file mode 100644 index 000000000000..2c50cd9d0c81 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.kt @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.common.pip + +import android.app.ActivityManager +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.window.DisplayAreaInfo +import android.window.WindowContainerToken +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_PIP +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP +import com.android.wm.shell.Flags.FLAG_ENABLE_PIP2 +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.desktopmode.DesktopRepository +import com.android.wm.shell.desktopmode.DesktopUserRepositories +import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** + * Unit test against [PipDesktopState]. + */ +@SmallTest +@RunWithLooper +@RunWith(AndroidTestingRunner::class) +@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) +class PipDesktopStateTest : ShellTestCase() { + private val mockPipDisplayLayoutState = mock<PipDisplayLayoutState>() + private val mockDesktopUserRepositories = mock<DesktopUserRepositories>() + private val mockDesktopRepository = mock<DesktopRepository>() + private val mockDragToDesktopTransitionHandler = mock<DragToDesktopTransitionHandler>() + private val mockRootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>() + private val mockTaskInfo = mock<ActivityManager.RunningTaskInfo>() + private lateinit var defaultTda: DisplayAreaInfo + private lateinit var pipDesktopState: PipDesktopState + + @Before + fun setUp() { + whenever(mockDesktopUserRepositories.current).thenReturn(mockDesktopRepository) + whenever(mockTaskInfo.getDisplayId()).thenReturn(DISPLAY_ID) + whenever(mockPipDisplayLayoutState.displayId).thenReturn(DISPLAY_ID) + + defaultTda = DisplayAreaInfo(mock<WindowContainerToken>(), DISPLAY_ID, /* featureId = */ 0) + whenever(mockRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DISPLAY_ID)).thenReturn( + defaultTda + ) + + pipDesktopState = + PipDesktopState( + mockPipDisplayLayoutState, + Optional.of(mockDesktopUserRepositories), + Optional.of(mockDragToDesktopTransitionHandler), + mockRootTaskDisplayAreaOrganizer + ) + } + + @Test + fun isDesktopWindowingPipEnabled_returnsTrue() { + assertThat(pipDesktopState.isDesktopWindowingPipEnabled()).isTrue() + } + + @Test + @EnableFlags( + FLAG_ENABLE_CONNECTED_DISPLAYS_PIP, + FLAG_ENABLE_PIP2 + ) + fun isConnectedDisplaysPipEnabled_returnsTrue() { + assertThat(pipDesktopState.isConnectedDisplaysPipEnabled()).isTrue() + } + + @Test + fun isPipInDesktopMode_anyDeskActive_returnsTrue() { + whenever(mockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true) + + assertThat(pipDesktopState.isPipInDesktopMode()).isTrue() + } + + @Test + fun isPipInDesktopMode_noDeskActive_returnsFalse() { + whenever(mockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(false) + + assertThat(pipDesktopState.isPipInDesktopMode()).isFalse() + } + + @Test + fun outPipWindowingMode_exitToDesktop_displayFreeform_returnsUndefined() { + whenever(mockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true) + setDisplayWindowingMode(WINDOWING_MODE_FREEFORM) + + assertThat(pipDesktopState.getOutPipWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test + fun outPipWindowingMode_exitToDesktop_displayFullscreen_returnsFreeform() { + whenever(mockDesktopRepository.isAnyDeskActive(DISPLAY_ID)).thenReturn(true) + setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN) + + assertThat(pipDesktopState.getOutPipWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @Test + fun outPipWindowingMode_exitToFullscreen_displayFullscreen_returnsUndefined() { + setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN) + + assertThat(pipDesktopState.getOutPipWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test + fun isDragToDesktopInProgress_inProgress_returnsTrue() { + whenever(mockDragToDesktopTransitionHandler.inProgress).thenReturn(true) + + assertThat(pipDesktopState.isDragToDesktopInProgress()).isTrue() + } + + @Test + fun isDragToDesktopInProgress_notInProgress_returnsFalse() { + whenever(mockDragToDesktopTransitionHandler.inProgress).thenReturn(false) + + assertThat(pipDesktopState.isDragToDesktopInProgress()).isFalse() + } + + private fun setDisplayWindowingMode(windowingMode: Int) { + defaultTda.configuration.windowConfiguration.windowingMode = windowingMode + } + + companion object { + private const val DISPLAY_ID = 1 + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java index 22a85fc49a4b..9f2534eb2662 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java @@ -71,6 +71,7 @@ public class FlexParallaxSpecTests { when(mockSnapAlgorithm.getMiddleTarget()).thenReturn(mockMiddleTarget); when(mockSnapAlgorithm.getLastSplitTarget()).thenReturn(mockLastTarget); when(mockSnapAlgorithm.getDismissEndTarget()).thenReturn(mockEndEdge); + when(mockSnapAlgorithm.areOffscreenRatiosSupported()).thenReturn(true); when(mockStartEdge.getPosition()).thenReturn(0); when(mockFirstTarget.getPosition()).thenReturn(250); 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 597e4a55ed0e..9035df28aa7c 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 @@ -680,7 +680,8 @@ public class CompatUIControllerTest extends ShellTestCase { // Create transparent task final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true, - /* isVisible */ true, /* isFocused */ true, /* isTopActivityTransparent */ true); + /* isVisible */ true, /* isFocused */ true, /* isTopActivityTransparent */ true, + /* isRestartMenuEnabledForDisplayMove */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo1); @@ -742,32 +743,38 @@ public class CompatUIControllerTest extends ShellTestCase { @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)); + final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, + /* isVisible */ true, /* isFocused */ true, /* isTopActivityTransparent */ false, + /* isRestartMenuEnabledForDisplayMove */ true); doReturn(true).when(mCompatUIConfiguration).isRestartDialogEnabled(); doReturn(true).when(mCompatUIConfiguration).shouldShowRestartDialogAgain(eq(taskInfo)); - mController.sendCompatUIRequest(new CompatUIRequests.DisplayCompatShowRestartDialog( - taskInfo, mMockTaskListener)); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); + verify(mMockRestartDialogLayout).setRequestRestartDialog(false); + + mController.sendCompatUIRequest( + new CompatUIRequests.DisplayCompatShowRestartDialog(taskInfo.taskId)); + verify(mMockRestartDialogLayout).setRequestRestartDialog(true); } private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat) { return createTaskInfo(displayId, taskId, hasSizeCompat, /* isVisible */ false, - /* isFocused */ false, /* isTopActivityTransparent */ false); + /* isFocused */ false, /* isTopActivityTransparent */ false, + /* isRestartMenuEnabledForDisplayMove */ false); } private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat, boolean isVisible, boolean isFocused) { return createTaskInfo(displayId, taskId, hasSizeCompat, - isVisible, isFocused, /* isTopActivityTransparent */ false); + isVisible, isFocused, /* isTopActivityTransparent */ false, + /* isRestartMenuEnabledForDisplayMove */ false); } private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat, - boolean isVisible, boolean isFocused, boolean isTopActivityTransparent) { + boolean isVisible, boolean isFocused, boolean isTopActivityTransparent, + boolean isRestartMenuEnabledForDisplayMove) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; taskInfo.displayId = displayId; @@ -777,6 +784,8 @@ public class CompatUIControllerTest extends ShellTestCase { taskInfo.isTopActivityTransparent = isTopActivityTransparent; taskInfo.appCompatTaskInfo.setLetterboxEducationEnabled(true); taskInfo.appCompatTaskInfo.setTopActivityLetterboxed(true); + taskInfo.appCompatTaskInfo.setRestartMenuEnabledForDisplayMove( + isRestartMenuEnabledForDisplayMove); return taskInfo; } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt index d58f8a34c98e..94fe03084989 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt @@ -37,6 +37,8 @@ import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE import com.android.window.flags.Flags.FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.TaskStackListenerImpl import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask @@ -96,12 +98,15 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer @Mock lateinit var userManager: UserManager @Mock lateinit var shellController: ShellController + @Mock lateinit var displayController: DisplayController + @Mock lateinit var displayLayout: DisplayLayout private lateinit var mockitoSession: StaticMockitoSession private lateinit var handler: DesktopActivityOrientationChangeHandler private lateinit var shellInit: ShellInit private lateinit var userRepositories: DesktopUserRepositories private lateinit var testScope: CoroutineScope + // Mock running tasks are registered here so we can get the list from mock shell task organizer. private val runningTasks = mutableListOf<RunningTaskInfo>() @@ -131,6 +136,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }) .thenReturn(Desktop.getDefaultInstance()) + whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) handler = DesktopActivityOrientationChangeHandler( @@ -140,6 +146,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { taskStackListener, resizeTransitionHandler, userRepositories, + displayController, ) shellInit.init() @@ -171,6 +178,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { taskStackListener, resizeTransitionHandler, userRepositories, + displayController, ) verify(shellInit, never()) @@ -251,6 +259,11 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { val oldBounds = task.configuration.windowConfiguration.bounds val newTask = setUpFreeformTask(isResizeable = false, orientation = SCREEN_ORIENTATION_LANDSCAPE) + whenever(displayLayout.height()).thenReturn(800) + whenever(displayLayout.width()).thenReturn(2000) + whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(Rect(0, 0, 2000, 800)) + } handler.handleActivityOrientationChange(task, newTask) @@ -279,6 +292,11 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { bounds = oldBounds, ) val newTask = setUpFreeformTask(isResizeable = false, bounds = oldBounds) + whenever(displayLayout.height()).thenReturn(2000) + whenever(displayLayout.width()).thenReturn(800) + whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(Rect(0, 0, 800, 2000)) + } handler.handleActivityOrientationChange(task, newTask) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt index 2aebcdcc3bf5..9268db60aa51 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt @@ -24,13 +24,16 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.ExtendedMockito.never import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.window.flags.Flags +import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.sysui.UserChangeListener import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow @@ -46,6 +49,7 @@ import org.mockito.Mockito.spy import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -60,6 +64,8 @@ import org.mockito.quality.Strictness class DesktopDisplayEventHandlerTest : ShellTestCase() { @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var displayController: DisplayController + @Mock private lateinit var mockShellController: ShellController + @Mock private lateinit var mockRootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var mockDesktopUserRepositories: DesktopUserRepositories @Mock private lateinit var mockDesktopRepository: DesktopRepository @Mock private lateinit var mockDesktopTasksController: DesktopTasksController @@ -89,7 +95,9 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { context, shellInit, testScope.backgroundScope, + mockShellController, displayController, + mockRootTaskDisplayAreaOrganizer, desktopRepositoryInitializer, mockDesktopUserRepositories, mockDesktopTasksController, @@ -107,6 +115,7 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_createsDesk() = testScope.runTest { whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) @@ -119,6 +128,7 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun testDisplayAdded_supportsDesks_desktopRepositoryNotInitialized_doesNotCreateDesk() = testScope.runTest { whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) @@ -130,6 +140,7 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun testDisplayAdded_supportsDesks_desktopRepositoryInitializedTwice_createsDeskOnce() = testScope.runTest { whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) @@ -143,6 +154,7 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_deskExists_doesNotCreateDesk() = testScope.runTest { whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) @@ -156,33 +168,71 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { } @Test - fun testDisplayAdded_cannotEnterDesktopMode_doesNotCreateDesk() { - whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false) + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun testDisplayAdded_cannotEnterDesktopMode_doesNotCreateDesk() = + testScope.runTest { + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false) + desktopRepositoryInitializer.initialize(mockDesktopUserRepositories) - onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) + onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) + runCurrent() - verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY) - } + verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY) + } @Test @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) - fun testDeskRemoved_noDesksRemain_createsDesk() { - whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(0) + fun testDeskRemoved_noDesksRemain_createsDesk() = + testScope.runTest { + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) + whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(0) + desktopRepositoryInitializer.initialize(mockDesktopUserRepositories) - handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1) + handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1) + runCurrent() - verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY) - } + verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY) + } @Test @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) - fun testDeskRemoved_desksRemain_doesNotCreateDesk() { - whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(1) + fun testDeskRemoved_desksRemain_doesNotCreateDesk() = + testScope.runTest { + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) + whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(1) + desktopRepositoryInitializer.initialize(mockDesktopUserRepositories) + + handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1) + runCurrent() - handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1) + verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY) + } - verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY) - } + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun testUserChanged_createsDeskWhenNeeded() = + testScope.runTest { + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) + val userChangeListenerCaptor = argumentCaptor<UserChangeListener>() + verify(mockShellController).addUserChangeListener(userChangeListenerCaptor.capture()) + whenever(mockDesktopRepository.getNumberOfDesks(displayId = 2)).thenReturn(0) + whenever(mockDesktopRepository.getNumberOfDesks(displayId = 3)).thenReturn(0) + whenever(mockDesktopRepository.getNumberOfDesks(displayId = 4)).thenReturn(1) + whenever(mockRootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(2, 3, 4)) + desktopRepositoryInitializer.initialize(mockDesktopUserRepositories) + handler.onDisplayAdded(displayId = 2) + handler.onDisplayAdded(displayId = 3) + handler.onDisplayAdded(displayId = 4) + runCurrent() + + clearInvocations(mockDesktopTasksController) + userChangeListenerCaptor.lastValue.onUserChanged(1, context) + runCurrent() + + verify(mockDesktopTasksController).createDesk(displayId = 2) + verify(mockDesktopTasksController).createDesk(displayId = 3) + verify(mockDesktopTasksController, never()).createDesk(displayId = 4) + } @Test fun testConnectExternalDisplay() { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt index d510570e8839..e40da5e8498d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt @@ -50,7 +50,6 @@ import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.FocusTransitionObserver import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel -import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -120,11 +119,11 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() { whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda) doAnswer { - keyGestureEventHandler = (it.arguments[0] as KeyGestureEventHandler) + keyGestureEventHandler = (it.arguments[1] as KeyGestureEventHandler) null } .whenever(inputManager) - .registerKeyGestureEventHandler(any()) + .registerKeyGestureEventHandler(any(), any()) shellInit.init() desktopModeKeyGestureHandler = @@ -176,10 +175,9 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() { .setKeycodes(intArrayOf(KeyEvent.KEYCODE_D)) .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON) .build() - val result = keyGestureEventHandler.handleKeyGestureEvent(event, null) + keyGestureEventHandler.handleKeyGestureEvent(event, null) testExecutor.flushAll() - assertThat(result).isTrue() verify(desktopTasksController).moveToNextDisplay(task.taskId) } @@ -197,10 +195,9 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() { .setKeycodes(intArrayOf(KeyEvent.KEYCODE_LEFT_BRACKET)) .setModifierState(KeyEvent.META_META_ON) .build() - val result = keyGestureEventHandler.handleKeyGestureEvent(event, null) + keyGestureEventHandler.handleKeyGestureEvent(event, null) testExecutor.flushAll() - assertThat(result).isTrue() verify(desktopModeWindowDecorViewModel) .onSnapResize( task.taskId, @@ -224,10 +221,9 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() { .setKeycodes(intArrayOf(KeyEvent.KEYCODE_RIGHT_BRACKET)) .setModifierState(KeyEvent.META_META_ON) .build() - val result = keyGestureEventHandler.handleKeyGestureEvent(event, null) + keyGestureEventHandler.handleKeyGestureEvent(event, null) testExecutor.flushAll() - assertThat(result).isTrue() verify(desktopModeWindowDecorViewModel) .onSnapResize( task.taskId, @@ -251,10 +247,9 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() { .setKeycodes(intArrayOf(KeyEvent.KEYCODE_EQUALS)) .setModifierState(KeyEvent.META_META_ON) .build() - val result = keyGestureEventHandler.handleKeyGestureEvent(event, null) + keyGestureEventHandler.handleKeyGestureEvent(event, null) testExecutor.flushAll() - assertThat(result).isTrue() verify(desktopTasksController) .toggleDesktopTaskSize( task, @@ -280,10 +275,9 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() { .setKeycodes(intArrayOf(KeyEvent.KEYCODE_MINUS)) .setModifierState(KeyEvent.META_META_ON) .build() - val result = keyGestureEventHandler.handleKeyGestureEvent(event, null) + keyGestureEventHandler.handleKeyGestureEvent(event, null) testExecutor.flushAll() - assertThat(result).isTrue() verify(desktopTasksController).minimizeTask(task, MinimizeReason.KEY_GESTURE) } 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 71e46bcb0698..b577667d8279 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 @@ -1526,6 +1526,37 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + fun launchIntent_taskInDesktopMode_onSecondaryDisplay_transitionStarted() { + setUpLandscapeDisplay() + taskRepository.addDesk(SECOND_DISPLAY, deskId = 2) + val intent = Intent().setComponent(homeComponentName) + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + eq(TRANSIT_OPEN), + any(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(Binder()) + + controller.startLaunchIntentTransition(intent, Bundle.EMPTY, SECOND_DISPLAY) + + val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN) + // We expect two actions: open the app and start the desk + assertThat(wct.hierarchyOps).hasSize(2) + val hOps0 = wct.hierarchyOps[0] + val hOps1 = wct.hierarchyOps[1] + assertThat(hOps0.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT) + val activityOptions0 = ActivityOptions.fromBundle(hOps0.launchOptions) + assertThat(activityOptions0.launchDisplayId).isEqualTo(SECOND_DISPLAY) + assertThat(hOps1.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT) + val activityOptions1 = ActivityOptions.fromBundle(hOps1.launchOptions) + assertThat(activityOptions1.launchDisplayId).isEqualTo(SECOND_DISPLAY) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) fun addMoveToDeskTaskChanges_landscapeDevice_userFullscreenOverride_defaultPortraitBounds() { setUpLandscapeDisplay() @@ -7615,6 +7646,106 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() assertNull(latestWct.hierarchyOps.find { op -> op.container == wallpaperToken.asBinder() }) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onRecentsInDesktopAnimationFinishing_returningToApp_noDeskDeactivation() { + val deskId = 0 + taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId) + + val transition = Binder() + val finishWct = WindowContainerTransaction() + controller.onRecentsInDesktopAnimationFinishing( + transition = transition, + finishWct = finishWct, + returnToApp = true, + ) + + verify(desksOrganizer, never()).deactivateDesk(finishWct, deskId) + verify(desksTransitionsObserver, never()) + .addPendingTransition( + argThat { t -> t.token == transition && t is DeskTransition.DeactivateDesk } + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onRecentsInDesktopAnimationFinishing_noActiveDesk_noDeskDeactivation() { + val deskId = 0 + taskRepository.setDeskInactive(deskId) + + val transition = Binder() + val finishWct = WindowContainerTransaction() + controller.onRecentsInDesktopAnimationFinishing( + transition = transition, + finishWct = finishWct, + returnToApp = false, + ) + + verify(desksOrganizer, never()).deactivateDesk(finishWct, deskId) + verify(desksTransitionsObserver, never()) + .addPendingTransition( + argThat { t -> t.token == transition && t is DeskTransition.DeactivateDesk } + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onRecentsInDesktopAnimationFinishing_activeDesk_notReturningToDesk_deactivatesDesk() { + val deskId = 0 + taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId) + + val transition = Binder() + val finishWct = WindowContainerTransaction() + controller.onRecentsInDesktopAnimationFinishing( + transition = transition, + finishWct = finishWct, + returnToApp = false, + ) + + verify(desksOrganizer).deactivateDesk(finishWct, deskId) + verify(desksTransitionsObserver) + .addPendingTransition(DeskTransition.DeactivateDesk(transition, deskId)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onRecentsInDesktopAnimationFinishing_activeDesk_notReturningToDesk_notifiesDesktopExit() { + val deskId = 0 + taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId) + + val transition = Binder() + val finishWct = WindowContainerTransaction() + controller.onRecentsInDesktopAnimationFinishing( + transition = transition, + finishWct = finishWct, + returnToApp = false, + ) + + verify(desktopModeEnterExitTransitionListener).onExitDesktopModeTransitionStarted(any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onRecentsInDesktopAnimationFinishing_activeDesk_notReturningToDesk_doesNotBringUpWallpaperOrHome() { + val deskId = 0 + taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId) + + val transition = Binder() + val finishWct = WindowContainerTransaction() + controller.onRecentsInDesktopAnimationFinishing( + transition = transition, + finishWct = finishWct, + returnToApp = false, + ) + + finishWct.assertWithoutHop { hop -> + hop.type == HIERARCHY_OP_TYPE_REORDER && + hop.container == wallpaperToken.asBinder() && + !hop.toTop + } + finishWct.assertWithoutHop { hop -> hop.type == HIERARCHY_OP_TYPE_PENDING_INTENT } + } + private class RunOnStartTransitionCallback : ((IBinder) -> Unit) { var invocations = 0 private set diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt index 3983bfbb2080..75f8d9e819cb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.desktopmode +import android.animation.AnimatorTestRule import android.app.ActivityManager import android.app.ActivityManager.RunningTaskInfo import android.graphics.Rect @@ -29,6 +30,7 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.SurfaceControl import android.view.SurfaceControlViewHost import android.view.View +import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE import com.android.wm.shell.ShellTestCase @@ -43,6 +45,7 @@ import com.android.wm.shell.windowdecor.tiling.SnapEventHandler import com.google.common.truth.Truth.assertThat import kotlin.test.Test import org.junit.Before +import org.junit.Rule import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock @@ -67,6 +70,9 @@ import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) class VisualIndicatorViewContainerTest : ShellTestCase() { + + @JvmField @Rule val animatorTestRule = AnimatorTestRule(this) + @Mock private lateinit var view: View @Mock private lateinit var displayLayout: DisplayLayout @Mock private lateinit var displayController: DisplayController @@ -297,6 +303,95 @@ class VisualIndicatorViewContainerTest : ShellTestCase() { verify(spyViewContainer, never()).fadeInIndicatorInternal(any(), any(), any(), any()) } + @Test + @EnableFlags( + com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + ) + fun testCreateView_bubblesEnabled_indicatorIsFrameLayout() { + val spyViewContainer = setupSpyViewContainer() + assertThat(spyViewContainer.indicatorView).isInstanceOf(FrameLayout::class.java) + } + + @Test + @EnableFlags( + com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + ) + fun testFadeInOutBubbleIndicator_addAndRemoveBarIndicator() { + setUpBubbleBoundsProvider() + val spyViewContainer = setupSpyViewContainer() + spyViewContainer.fadeInIndicator( + displayLayout, + DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR, + DEFAULT_DISPLAY, + ) + desktopExecutor.flushAll() + animatorTestRule.advanceTimeBy(200) + assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNotNull() + + spyViewContainer.fadeOutIndicator( + displayLayout, + DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR, + finishCallback = null, + DEFAULT_DISPLAY, + snapEventHandler, + ) + desktopExecutor.flushAll() + animatorTestRule.advanceTimeBy(250) + assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNull() + } + + @Test + @EnableFlags( + com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + ) + fun testTransitionIndicator_fullscreenToBubble_addBarIndicator() { + setUpBubbleBoundsProvider() + val spyViewContainer = setupSpyViewContainer() + + spyViewContainer.transitionIndicator( + taskInfo, + displayController, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR, + ) + desktopExecutor.flushAll() + animatorTestRule.advanceTimeBy(200) + + assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNotNull() + } + + @Test + @EnableFlags( + com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + ) + fun testTransitionIndicator_bubbleToFullscreen_removeBarIndicator() { + setUpBubbleBoundsProvider() + val spyViewContainer = setupSpyViewContainer() + spyViewContainer.fadeInIndicator( + displayLayout, + DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR, + DEFAULT_DISPLAY, + ) + desktopExecutor.flushAll() + animatorTestRule.advanceTimeBy(200) + assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNotNull() + + spyViewContainer.transitionIndicator( + taskInfo, + displayController, + DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + ) + desktopExecutor.flushAll() + animatorTestRule.advanceTimeBy(200) + + assertThat((spyViewContainer.indicatorView as FrameLayout).getChildAt(0)).isNull() + } + private fun setupSpyViewContainer(): VisualIndicatorViewContainer { val viewContainer = VisualIndicatorViewContainer( @@ -331,7 +426,22 @@ class VisualIndicatorViewContainerTest : ShellTestCase() { .build() } + private fun setUpBubbleBoundsProvider() { + bubbleDropTargetBoundsProvider = + object : BubbleDropTargetBoundsProvider { + override fun getBubbleBarExpandedViewDropTargetBounds(onLeft: Boolean): Rect { + return BUBBLE_INDICATOR_BOUNDS + } + + override fun getBarDropTargetBounds(onLeft: Boolean): Rect { + return BAR_INDICATOR_BOUNDS + } + } + } + companion object { private val DISPLAY_BOUNDS = Rect(0, 0, 1000, 1000) + private val BUBBLE_INDICATOR_BOUNDS = Rect(800, 200, 900, 900) + private val BAR_INDICATOR_BOUNDS = Rect(880, 950, 900, 960) } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt index 409ca57715fc..e55d7cbb73e1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt @@ -264,6 +264,36 @@ class DesksTransitionObserverTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionFinish_deactivateDesk_updatesRepository() { + val transition = Binder() + val deactivateTransition = DeskTransition.DeactivateDesk(transition, deskId = 5) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + repository.setActiveDesk(DEFAULT_DISPLAY, deskId = 5) + + observer.addPendingTransition(deactivateTransition) + observer.onTransitionFinished(transition) + + assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionMergedAndFinished_deactivateDesk_updatesRepository() { + val transition = Binder() + val deactivateTransition = DeskTransition.DeactivateDesk(transition, deskId = 5) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + repository.setActiveDesk(DEFAULT_DISPLAY, deskId = 5) + + observer.addPendingTransition(deactivateTransition) + val bookEndTransition = Binder() + observer.onTransitionMerged(merged = transition, playing = bookEndTransition) + observer.onTransitionFinished(bookEndTransition) + + assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun onTransitionReady_twoPendingTransitions_handlesBoth() { val transition = Binder() // Active one desk and deactivate another in different displays, such as in some diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java index 714e5f486285..69a42164071b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java @@ -373,6 +373,25 @@ public class FreeformTaskTransitionObserverTest extends ShellTestCase { verify(mDesksTransitionObserver).onTransitionReady(transition, info); } + @Test + public void onTransitionMerged_forwardsToDesksTransitionObserver() { + final IBinder merged = mock(IBinder.class); + final IBinder playing = mock(IBinder.class); + + mTransitionObserver.onTransitionMerged(merged, playing); + + verify(mDesksTransitionObserver).onTransitionMerged(merged, playing); + } + + @Test + public void onTransitionFinished_forwardsToDesksTransitionObserver() { + final IBinder transition = mock(IBinder.class); + + mTransitionObserver.onTransitionFinished(transition, /* aborted = */ false); + + verify(mDesksTransitionObserver).onTransitionFinished(transition); + } + private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) { final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.taskId = taskId; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index 677330790bab..9849b1174d8e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -37,6 +37,7 @@ import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; +import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_SYNC; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; @@ -1742,6 +1743,53 @@ public class ShellTransitionTests extends ShellTestCase { eq(R.styleable.WindowAnimation_activityCloseEnterAnimation), anyBoolean()); } + @Test + public void testTransientHideWithMoveToTop() { + Transitions transitions = createTestTransitions(); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + final TransitionAnimation transitionAnimation = new TransitionAnimation(mContext, false, + Transitions.TAG); + spyOn(transitionAnimation); + + // Prepare for a TO_BACK transition + final RunningTaskInfo taskInfo = createTaskInfo(1); + final IBinder closeTransition = new Binder(); + final SurfaceControl.Transaction closeTransitionFinishT = + mock(SurfaceControl.Transaction.class); + + // Start a TO_BACK transition + transitions.requestStartTransition(closeTransition, + new TransitionRequestInfo(TRANSIT_TO_BACK, null /* trigger */, null /* remote */)); + TransitionInfo closeInfo = new TransitionInfoBuilder(TRANSIT_TO_BACK) + .addChange(TRANSIT_TO_BACK, taskInfo) + .build(); + transitions.onTransitionReady(closeTransition, closeInfo, new StubTransaction(), + closeTransitionFinishT); + + // Verify that the transition hides the task surface in the finish transaction + verify(closeTransitionFinishT).hide(any()); + + // Prepare for a CHANGE transition + final IBinder changeTransition = new Binder(); + final SurfaceControl.Transaction changeTransitionFinishT = + mock(SurfaceControl.Transaction.class); + + // Start a CHANGE transition w/ MOVE_TO_FRONT that is merged into the TO_BACK + mDefaultHandler.setShouldMerge(changeTransition); + transitions.requestStartTransition(changeTransition, + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); + TransitionInfo changeInfo = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_CHANGE, FLAG_MOVED_TO_TOP, taskInfo) + .build(); + transitions.onTransitionReady(changeTransition, changeInfo, new StubTransaction(), + changeTransitionFinishT); + + // Verify that the transition shows the task surface in the finish transaction so that the + // when the original transition finishes, the finish transaction does not clobber the + // visibility of the merged transition + verify(changeTransitionFinishT).show(any()); + } + class TestTransitionHandler implements Transitions.TransitionHandler { ArrayList<Pair<IBinder, Transitions.TransitionFinishCallback>> mFinishes = new ArrayList<>(); 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 4c9c2f14d805..ed69d912c4ef 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 @@ -55,6 +55,7 @@ import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.compatui.api.CompatUIHandler import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler import com.android.wm.shell.desktopmode.DesktopImmersiveController import com.android.wm.shell.desktopmode.DesktopModeEventLogger @@ -144,6 +145,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { mock<DesktopActivityOrientationChangeHandler>() protected val mockMultiDisplayDragMoveIndicatorController = mock<MultiDisplayDragMoveIndicatorController>() + protected val mockCompatUIHandler = mock<CompatUIHandler>() protected val mockInputManager = mock<InputManager>() private val mockTaskPositionerFactory = mock<DesktopModeWindowDecorViewModel.TaskPositionerFactory>() @@ -243,6 +245,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { desktopModeCompatPolicy, mockTilingWindowDecoration, mockMultiDisplayDragMoveIndicatorController, + mockCompatUIHandler, ) desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index f7b9c3352dea..77513adf0088 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -303,7 +303,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt()); when(mMockHandleMenuFactory.create(any(), any(), any(), any(), any(), anyInt(), any(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), - any(), anyInt(), anyInt(), anyInt(), anyInt())) + anyBoolean(), any(), anyInt(), anyInt(), anyInt(), anyInt())) .thenReturn(mMockHandleMenu); when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any(), anyInt())) .thenReturn(false); @@ -1450,6 +1450,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), anyBoolean() ); // Run runnable to set captured link to used @@ -1487,6 +1488,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), anyBoolean() ); openInBrowserCaptor.getValue().invoke(new Intent(Intent.ACTION_MAIN, TEST_URI1)); @@ -1519,6 +1521,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), anyBoolean() ); @@ -1584,6 +1587,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), closeClickListener.capture(), any(), anyBoolean() @@ -1617,6 +1621,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), /* forceShowSystemBars= */ eq(true) ); } @@ -1790,7 +1795,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private void verifyHandleMenuCreated(@Nullable Uri uri) { verify(mMockHandleMenuFactory).create(any(), any(), any(), any(), any(), anyInt(), any(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), - anyBoolean(), argThat(intent -> + anyBoolean(), anyBoolean(), argThat(intent -> (uri == null && intent == null) || intent.getData().equals(uri)), anyInt(), anyInt(), anyInt(), anyInt()); } @@ -1916,6 +1921,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { .setTaskDescriptionBuilder(taskDescriptionBuilder) .setVisible(visible) .build(); + taskInfo.isVisibleRequested = visible; taskInfo.realActivity = new ComponentName("com.android.wm.shell.windowdecor", "DesktopModeWindowDecorationTests"); taskInfo.baseActivity = new ComponentName("com.android.wm.shell.windowdecor", diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt index 2e46f6312d03..e4b897264883 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt @@ -287,6 +287,7 @@ class HandleMenuTest : ShellTestCase() { shouldShowManageWindowsButton = false, shouldShowChangeAspectRatioButton = false, shouldShowDesktopModeButton = true, + shouldShowRestartButton = true, isBrowserApp = false, null /* openInAppOrBrowserIntent */, captionWidth = HANDLE_WIDTH, @@ -304,6 +305,7 @@ class HandleMenuTest : ShellTestCase() { onChangeAspectRatioClickListener = mock(), openInAppOrBrowserClickListener = mock(), onOpenByDefaultClickListener = mock(), + onRestartClickListener = mock(), onCloseMenuClickListener = mock(), onOutsideTouchListener = mock(), forceShowSystemBars = forceShowSystemBars diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolderTest.kt index bc4865a07f7f..2c3009cb8dc4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolderTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolderTest.kt @@ -78,7 +78,8 @@ class AppHandleViewHolderTest : ShellTestCase() { position = captionPosition, width = CAPTION_WIDTH, height = CAPTION_HEIGHT, - showInputLayer = false + showInputLayer = false, + isCaptionVisible = true ) ) diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index 714f6e41ff05..e09ab5fd1643 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -879,10 +879,10 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( // if we don't have a result yet if (!final_result || // or this config is better before the locale than the existing result - result->config.isBetterThanBeforeLocale(final_result->config, desired_config) || + result->config.isBetterThanBeforeLocale(final_result->config, *desired_config) || // or the existing config isn't better before locale and this one specifies a locale // whereas the existing one doesn't - (!final_result->config.isBetterThanBeforeLocale(result->config, desired_config) + (!final_result->config.isBetterThanBeforeLocale(result->config, *desired_config) && has_locale && !final_has_locale)) { final_result = result.value(); final_overlaid = overlaid; diff --git a/libs/androidfw/CursorWindow.cpp b/libs/androidfw/CursorWindow.cpp index a592749c5398..6e11d430c5ea 100644 --- a/libs/androidfw/CursorWindow.cpp +++ b/libs/androidfw/CursorWindow.cpp @@ -55,7 +55,7 @@ status_t CursorWindow::create(const String8 &name, size_t inflatedSize, CursorWi window->mName = name; window->mSize = std::min(kInlineSize, inflatedSize); window->mInflatedSize = inflatedSize; - window->mData = malloc(window->mSize); + window->mData = calloc(window->mSize, 1); if (!window->mData) goto fail; window->mReadOnly = false; diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index 8ecd6ba9b253..6ec605c2ced5 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -2615,16 +2615,14 @@ bool ResTable_config::isLocaleBetterThan(const ResTable_config& o, } bool ResTable_config::isBetterThanBeforeLocale(const ResTable_config& o, - const ResTable_config* requested) const { - if (requested) { - if (imsi || o.imsi) { - if ((mcc != o.mcc) && requested->mcc) { - return (mcc); - } + const ResTable_config& requested) const { + if (imsi || o.imsi) { + if ((mcc != o.mcc) && requested.mcc) { + return mcc; + } - if ((mnc != o.mnc) && requested->mnc) { - return (mnc); - } + if ((mnc != o.mnc) && requested.mnc) { + return mnc; } } return false; diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index 63b28da075cd..bd72d3741460 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -1416,7 +1416,10 @@ struct ResTable_config // match the requested configuration at all. bool isLocaleBetterThan(const ResTable_config& o, const ResTable_config* requested) const; - bool isBetterThanBeforeLocale(const ResTable_config& o, const ResTable_config* requested) const; + // The first part of isBetterThan() that only compares the fields that are higher priority than + // the locale. Use it when you need to do custom locale matching to filter out the configs prior + // to that. + bool isBetterThanBeforeLocale(const ResTable_config& o, const ResTable_config& requested) const; String8 toString() const; diff --git a/libs/hwui/renderthread/ReliableSurface.cpp b/libs/hwui/renderthread/ReliableSurface.cpp index 01e8010444c0..64d38b9ef466 100644 --- a/libs/hwui/renderthread/ReliableSurface.cpp +++ b/libs/hwui/renderthread/ReliableSurface.cpp @@ -149,25 +149,9 @@ ANativeWindowBuffer* ReliableSurface::acquireFallbackBuffer(int error) { return AHardwareBuffer_to_ANativeWindowBuffer(mScratchBuffer.get()); } - int width = -1; - int result = mWindow->query(mWindow, NATIVE_WINDOW_DEFAULT_WIDTH, &width); - if (result != OK || width < 0) { - ALOGW("Failed to query window default width: %s (%d) value=%d", strerror(-result), result, - width); - width = 1; - } - - int height = -1; - result = mWindow->query(mWindow, NATIVE_WINDOW_DEFAULT_HEIGHT, &height); - if (result != OK || height < 0) { - ALOGW("Failed to query window default height: %s (%d) value=%d", strerror(-result), result, - height); - height = 1; - } - AHardwareBuffer_Desc desc = AHardwareBuffer_Desc{ - .width = static_cast<uint32_t>(width), - .height = static_cast<uint32_t>(height), + .width = 1, + .height = 1, .layers = 1, .format = mFormat, .usage = mUsage, @@ -176,9 +160,9 @@ ANativeWindowBuffer* ReliableSurface::acquireFallbackBuffer(int error) { }; AHardwareBuffer* newBuffer; - result = AHardwareBuffer_allocate(&desc, &newBuffer); + int result = AHardwareBuffer_allocate(&desc, &newBuffer); - if (result != OK) { + if (result != NO_ERROR) { // Allocate failed, that sucks ALOGW("Failed to allocate scratch buffer, error=%d", result); return nullptr; diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index a67aea466c1c..0cd9c53c830f 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -238,6 +238,7 @@ void VulkanManager::setupDevice(skgpu::VulkanExtensions& grExtensions, for (uint32_t i = 0; i < queueCount; i++) { queuePriorityProps[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_GLOBAL_PRIORITY_PROPERTIES_EXT; queuePriorityProps[i].pNext = nullptr; + queueProps[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_PROPERTIES_2; queueProps[i].pNext = &queuePriorityProps[i]; } mGetPhysicalDeviceQueueFamilyProperties2(mPhysicalDevice, &queueCount, queueProps.get()); diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 4aba491c291e..0a1bfd55e77f 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -19,14 +19,15 @@ package android.media; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; import static android.content.Context.DEVICE_ID_DEFAULT; -import static android.media.audio.Flags.FLAG_UNIFY_ABSOLUTE_VOLUME_MANAGEMENT; -import static android.media.audio.Flags.autoPublicVolumeApiHardening; -import static android.media.audio.Flags.cacheGetStreamMinMaxVolume; -import static android.media.audio.Flags.cacheGetStreamVolume; import static android.media.audio.Flags.FLAG_DEPRECATE_STREAM_BT_SCO; import static android.media.audio.Flags.FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING; import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API; +import static android.media.audio.Flags.FLAG_REGISTER_VOLUME_CALLBACK_API_HARDENING; import static android.media.audio.Flags.FLAG_SUPPORTED_DEVICE_TYPES_API; +import static android.media.audio.Flags.FLAG_UNIFY_ABSOLUTE_VOLUME_MANAGEMENT; +import static android.media.audio.Flags.autoPublicVolumeApiHardening; +import static android.media.audio.Flags.cacheGetStreamMinMaxVolume; +import static android.media.audio.Flags.cacheGetStreamVolume; import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION; import android.Manifest; @@ -65,7 +66,7 @@ import android.media.audiopolicy.AudioPolicy; import android.media.audiopolicy.AudioPolicy.AudioPolicyFocusListener; import android.media.audiopolicy.AudioProductStrategy; import android.media.audiopolicy.AudioVolumeGroup; -import android.media.audiopolicy.AudioVolumeGroupChangeHandler; +import android.media.audiopolicy.IAudioVolumeChangeDispatcher; import android.media.projection.MediaProjection; import android.media.session.MediaController; import android.media.session.MediaSession; @@ -128,8 +129,6 @@ public class AudioManager { private static final String TAG = "AudioManager"; private static final boolean DEBUG = false; private static final AudioPortEventHandler sAudioPortEventHandler = new AudioPortEventHandler(); - private static final AudioVolumeGroupChangeHandler sAudioAudioVolumeGroupChangedHandler = - new AudioVolumeGroupChangeHandler(); private static WeakReference<Context> sContext; @@ -8761,9 +8760,13 @@ public class AudioManager { } } + //==================================================================== + // Notification of volume group changes /** + * Callback to receive updates on volume group changes, register using + * {@link AudioManager#registerVolumeGroupCallback(Executor, AudioVolumeCallback)}. + * * @hide - * Callback registered by client to be notified upon volume group change. */ @SystemApi public abstract static class VolumeGroupCallback { @@ -8774,34 +8777,69 @@ public class AudioManager { public void onAudioVolumeGroupChanged(int group, int flags) {} } - /** - * @hide - * Register an audio volume group change listener. - * @param callback the {@link VolumeGroupCallback} to register - */ + /** + * Register an audio volume group change listener. + * + * @param executor {@link Executor} to handle the callbacks + * @param callback the callback to receive the audio volume group changes + * @throws SecurityException if the caller doesn't have the required permission. + * + * @hide + */ @SystemApi - public void registerVolumeGroupCallback( - @NonNull Executor executor, + @FlaggedApi(FLAG_REGISTER_VOLUME_CALLBACK_API_HARDENING) + @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") + public void registerVolumeGroupCallback(@NonNull Executor executor, @NonNull VolumeGroupCallback callback) { - Preconditions.checkNotNull(executor, "executor must not be null"); - Preconditions.checkNotNull(callback, "volume group change cb must not be null"); - sAudioAudioVolumeGroupChangedHandler.init(); - // TODO: make use of executor - sAudioAudioVolumeGroupChangedHandler.registerListener(callback); + mVolumeChangedListenerMgr.addListener(executor, callback, "registerVolumeGroupCallback", + () -> new AudioVolumeChangeDispatcherStub()); } - /** - * @hide - * Unregister an audio volume group change listener. - * @param callback the {@link VolumeGroupCallback} to unregister - */ + /** + * Unregister an audio volume group change listener. + * + * @param callback the {@link VolumeGroupCallback} to unregister + * + * @hide + */ @SystemApi - public void unregisterVolumeGroupCallback( - @NonNull VolumeGroupCallback callback) { - Preconditions.checkNotNull(callback, "volume group change cb must not be null"); - sAudioAudioVolumeGroupChangedHandler.unregisterListener(callback); + @FlaggedApi(FLAG_REGISTER_VOLUME_CALLBACK_API_HARDENING) + @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") + public void unregisterVolumeGroupCallback(@NonNull VolumeGroupCallback callback) { + mVolumeChangedListenerMgr.removeListener(callback, "unregisterVolumeGroupCallback"); + } + + /** + * Manages the VolumeGroupCallback listeners and the AudioVolumeChangeDispatcherStub + */ + private final CallbackUtil.LazyListenerManager<VolumeGroupCallback> mVolumeChangedListenerMgr = + new CallbackUtil.LazyListenerManager(); + + final class AudioVolumeChangeDispatcherStub extends IAudioVolumeChangeDispatcher.Stub + implements CallbackUtil.DispatcherStub { + + @Override + public void register(boolean register) { + try { + if (register) { + getService().registerAudioVolumeCallback(this); + } else { + getService().unregisterAudioVolumeCallback(this); + } + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + @Override + public void onAudioVolumeGroupChanged(int group, int flags) { + mVolumeChangedListenerMgr.callListeners((listener) -> + listener.onAudioVolumeGroupChanged(group, flags)); + } } + //==================================================================== + /** * Return if an asset contains haptic channels or not. * diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index ad6f2e52fd97..4906cd3fb1e5 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -2732,4 +2732,25 @@ public class AudioSystem * @hide */ public static native void triggerSystemPropertyUpdate(long handle); + + /** + * Registers the given {@link INativeAudioVolumeGroupCallback} to native audioserver. + * @param callback to register + * @return {@link #SUCCESS} if successfully registered. + * + * @hide + */ + public static native int registerAudioVolumeGroupCallback( + INativeAudioVolumeGroupCallback callback); + + /** + * Unegisters the given {@link INativeAudioVolumeGroupCallback} from native audioserver + * previously registered via {@link #registerAudioVolumeGroupCallback}. + * @param callback to register + * @return {@link #SUCCESS} if successfully registered. + * + * @hide + */ + public static native int unregisterAudioVolumeGroupCallback( + INativeAudioVolumeGroupCallback callback); } diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 8aadb418cf5a..c505bcee0332 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -65,6 +65,7 @@ import android.media.audiopolicy.AudioPolicyConfig; import android.media.audiopolicy.AudioProductStrategy; import android.media.audiopolicy.AudioVolumeGroup; import android.media.audiopolicy.IAudioPolicyCallback; +import android.media.audiopolicy.IAudioVolumeChangeDispatcher; import android.media.projection.IMediaProjection; import android.net.Uri; import android.os.PersistableBundle; @@ -446,6 +447,12 @@ interface IAudioService { boolean isAudioServerRunning(); + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + void registerAudioVolumeCallback(IAudioVolumeChangeDispatcher avc); + + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + oneway void unregisterAudioVolumeCallback(IAudioVolumeChangeDispatcher avc); + int setUidDeviceAffinity(in IAudioPolicyCallback pcb, in int uid, in int[] deviceTypes, in String[] deviceAddresses); diff --git a/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java b/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java deleted file mode 100644 index 022cfeeb4e43..000000000000 --- a/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.media.audiopolicy; - -import android.annotation.NonNull; -import android.media.AudioManager; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Message; - -import com.android.internal.util.Preconditions; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; - -/** - * The AudioVolumeGroupChangeHandler handles AudioManager.OnAudioVolumeGroupChangedListener - * callbacks posted from JNI - * - * TODO: Make use of Executor of callbacks. - * @hide - */ -public class AudioVolumeGroupChangeHandler { - private Handler mHandler; - private HandlerThread mHandlerThread; - private final ArrayList<AudioManager.VolumeGroupCallback> mListeners = - new ArrayList<AudioManager.VolumeGroupCallback>(); - - private static final String TAG = "AudioVolumeGroupChangeHandler"; - - private static final int AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED = 1000; - private static final int AUDIOVOLUMEGROUP_EVENT_NEW_LISTENER = 4; - - /** - * Accessed by native methods: JNI Callback context. - */ - @SuppressWarnings("unused") - private long mJniCallback; - - /** - * Initialization - */ - public void init() { - synchronized (this) { - if (mHandler != null) { - return; - } - // create a new thread for our new event handler - mHandlerThread = new HandlerThread(TAG); - mHandlerThread.start(); - - if (mHandlerThread.getLooper() == null) { - mHandler = null; - return; - } - mHandler = new Handler(mHandlerThread.getLooper()) { - @Override - public void handleMessage(Message msg) { - ArrayList<AudioManager.VolumeGroupCallback> listeners; - synchronized (this) { - if (msg.what == AUDIOVOLUMEGROUP_EVENT_NEW_LISTENER) { - listeners = - new ArrayList<AudioManager.VolumeGroupCallback>(); - if (mListeners.contains(msg.obj)) { - listeners.add( - (AudioManager.VolumeGroupCallback) msg.obj); - } - } else { - listeners = (ArrayList<AudioManager.VolumeGroupCallback>) - mListeners.clone(); - } - } - if (listeners.isEmpty()) { - return; - } - - switch (msg.what) { - case AUDIOVOLUMEGROUP_EVENT_VOLUME_CHANGED: - for (int i = 0; i < listeners.size(); i++) { - listeners.get(i).onAudioVolumeGroupChanged((int) msg.arg1, - (int) msg.arg2); - } - break; - - default: - break; - } - } - }; - native_setup(new WeakReference<AudioVolumeGroupChangeHandler>(this)); - } - } - - private native void native_setup(Object moduleThis); - - @Override - protected void finalize() { - native_finalize(); - if (mHandlerThread.isAlive()) { - mHandlerThread.quit(); - } - } - private native void native_finalize(); - - /** - * @param cb the {@link AudioManager.VolumeGroupCallback} to register - */ - public void registerListener(@NonNull AudioManager.VolumeGroupCallback cb) { - Preconditions.checkNotNull(cb, "volume group callback shall not be null"); - synchronized (this) { - mListeners.add(cb); - } - if (mHandler != null) { - Message m = mHandler.obtainMessage( - AUDIOVOLUMEGROUP_EVENT_NEW_LISTENER, 0, 0, cb); - mHandler.sendMessage(m); - } - } - - /** - * @param cb the {@link AudioManager.VolumeGroupCallback} to unregister - */ - public void unregisterListener(@NonNull AudioManager.VolumeGroupCallback cb) { - Preconditions.checkNotNull(cb, "volume group callback shall not be null"); - synchronized (this) { - mListeners.remove(cb); - } - } - - Handler handler() { - return mHandler; - } - - @SuppressWarnings("unused") - private static void postEventFromNative(Object moduleRef, - int what, int arg1, int arg2, Object obj) { - AudioVolumeGroupChangeHandler eventHandler = - (AudioVolumeGroupChangeHandler) ((WeakReference) moduleRef).get(); - if (eventHandler == null) { - return; - } - - if (eventHandler != null) { - Handler handler = eventHandler.handler(); - if (handler != null) { - Message m = handler.obtainMessage(what, arg1, arg2, obj); - // Do not remove previous messages, as we would lose notification of group changes - handler.sendMessage(m); - } - } - } -} diff --git a/media/java/android/media/audiopolicy/IAudioVolumeChangeDispatcher.aidl b/media/java/android/media/audiopolicy/IAudioVolumeChangeDispatcher.aidl new file mode 100644 index 000000000000..e6f9024cfd1e --- /dev/null +++ b/media/java/android/media/audiopolicy/IAudioVolumeChangeDispatcher.aidl @@ -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 android.media.audiopolicy; + +/** + * AIDL for the AudioService to signal audio volume groups changes + * + * {@hide} + */ +oneway interface IAudioVolumeChangeDispatcher { + + /** + * Called when a volume group has been changed + * @param group id of the volume group that has changed. + * @param flags one or more flags to describe the volume change. + */ + void onAudioVolumeGroupChanged(int group, int flags); +} diff --git a/media/java/android/media/quality/MediaQualityContract.java b/media/java/android/media/quality/MediaQualityContract.java index fccdba8e727f..ece87a66556f 100644 --- a/media/java/android/media/quality/MediaQualityContract.java +++ b/media/java/android/media/quality/MediaQualityContract.java @@ -72,6 +72,43 @@ public class MediaQualityContract { */ public static final String LEVEL_OFF = "level_off"; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @StringDef(prefix = "COLOR_TEMP", value = { + COLOR_TEMP_USER, + COLOR_TEMP_COOL, + COLOR_TEMP_STANDARD, + COLOR_TEMP_WARM, + COLOR_TEMP_USER_HDR10PLUS, + COLOR_TEMP_COOL_HDR10PLUS, + COLOR_TEMP_STANDARD_HDR10PLUS, + COLOR_TEMP_WARM_HDR10PLUS, + COLOR_TEMP_FMMSDR, + COLOR_TEMP_FMMHDR, + }) + public @interface ColorTempValue {} + + /** @hide */ + public static final String COLOR_TEMP_USER = "color_temp_user"; + /** @hide */ + public static final String COLOR_TEMP_COOL = "color_temp_cool"; + /** @hide */ + public static final String COLOR_TEMP_STANDARD = "color_temp_standard"; + /** @hide */ + public static final String COLOR_TEMP_WARM = "color_temp_warm"; + /** @hide */ + public static final String COLOR_TEMP_USER_HDR10PLUS = "color_temp_user_hdr10plus"; + /** @hide */ + public static final String COLOR_TEMP_COOL_HDR10PLUS = "color_temp_cool_hdr10plus"; + /** @hide */ + public static final String COLOR_TEMP_STANDARD_HDR10PLUS = "color_temp_standard_hdr10plus"; + /** @hide */ + public static final String COLOR_TEMP_WARM_HDR10PLUS = "color_temp_warm_hdr10plus"; + /** @hide */ + public static final String COLOR_TEMP_FMMSDR = "color_temp_fmmsdr"; + /** @hide */ + public static final String COLOR_TEMP_FMMHDR = "color_temp_fmmhdr"; + /** * @hide @@ -82,7 +119,6 @@ public class MediaQualityContract { String PARAMETER_NAME = "_name"; String PARAMETER_PACKAGE = "_package"; String PARAMETER_INPUT_ID = "_input_id"; - String VENDOR_PARAMETERS = "_vendor_parameters"; } /** diff --git a/media/tests/AudioPolicyTest/AndroidManifest.xml b/media/tests/AudioPolicyTest/AndroidManifest.xml index 5c911b135a5d..466da7e66fbf 100644 --- a/media/tests/AudioPolicyTest/AndroidManifest.xml +++ b/media/tests/AudioPolicyTest/AndroidManifest.xml @@ -19,6 +19,7 @@ <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> + <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" /> <uses-permission android:name="android.permission.CHANGE_ACCESSIBILITY_VOLUME" /> diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java deleted file mode 100644 index 82394a2eb420..000000000000 --- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioVolumeGroupChangeHandlerTest.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.audiopolicytest; - -import static androidx.test.core.app.ApplicationProvider.getApplicationContext; - -import static com.android.audiopolicytest.AudioVolumeTestUtil.DEFAULT_ATTRIBUTES; -import static com.android.audiopolicytest.AudioVolumeTestUtil.incrementVolumeIndex; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -import android.media.AudioAttributes; -import android.media.AudioManager; -import android.media.audiopolicy.AudioVolumeGroup; -import android.media.audiopolicy.AudioVolumeGroupChangeHandler; -import android.platform.test.annotations.Presubmit; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.ArrayList; -import java.util.List; - -@Presubmit -@RunWith(AndroidJUnit4.class) -public class AudioVolumeGroupChangeHandlerTest { - private static final String TAG = "AudioVolumeGroupChangeHandlerTest"; - - @Rule - public final AudioVolumesTestRule rule = new AudioVolumesTestRule(); - - private AudioManager mAudioManager; - - @Before - public void setUp() { - mAudioManager = getApplicationContext().getSystemService(AudioManager.class); - } - - @Test - public void testRegisterInvalidCallback() { - final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler = - new AudioVolumeGroupChangeHandler(); - - audioAudioVolumeGroupChangedHandler.init(); - - assertThrows(NullPointerException.class, () -> { - AudioManager.VolumeGroupCallback nullCb = null; - audioAudioVolumeGroupChangedHandler.registerListener(nullCb); - }); - } - - @Test - public void testUnregisterInvalidCallback() { - final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler = - new AudioVolumeGroupChangeHandler(); - - audioAudioVolumeGroupChangedHandler.init(); - - final AudioVolumeGroupCallbackHelper cb = new AudioVolumeGroupCallbackHelper(); - audioAudioVolumeGroupChangedHandler.registerListener(cb); - - assertThrows(NullPointerException.class, () -> { - AudioManager.VolumeGroupCallback nullCb = null; - audioAudioVolumeGroupChangedHandler.unregisterListener(nullCb); - }); - audioAudioVolumeGroupChangedHandler.unregisterListener(cb); - } - - @Test - public void testRegisterUnregisterCallback() { - final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler = - new AudioVolumeGroupChangeHandler(); - - audioAudioVolumeGroupChangedHandler.init(); - final AudioVolumeGroupCallbackHelper validCb = new AudioVolumeGroupCallbackHelper(); - - // Should not assert, otherwise test will fail - audioAudioVolumeGroupChangedHandler.registerListener(validCb); - - // Should not assert, otherwise test will fail - audioAudioVolumeGroupChangedHandler.unregisterListener(validCb); - } - - @Test - public void testCallbackReceived() { - final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler = - new AudioVolumeGroupChangeHandler(); - - audioAudioVolumeGroupChangedHandler.init(); - - final AudioVolumeGroupCallbackHelper validCb = new AudioVolumeGroupCallbackHelper(); - audioAudioVolumeGroupChangedHandler.registerListener(validCb); - - List<AudioVolumeGroup> audioVolumeGroups = mAudioManager.getAudioVolumeGroups(); - assertTrue(audioVolumeGroups.size() > 0); - - try { - for (final AudioVolumeGroup audioVolumeGroup : audioVolumeGroups) { - int volumeGroupId = audioVolumeGroup.getId(); - - List<AudioAttributes> avgAttributes = audioVolumeGroup.getAudioAttributes(); - // Set the volume per attributes (if valid) and wait the callback - if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(DEFAULT_ATTRIBUTES)) { - // Some volume groups may not have valid attributes, used for internal - // volume management like patch/rerouting - // so bailing out strategy retrieval from attributes - continue; - } - final AudioAttributes aa = avgAttributes.get(0); - - int index = mAudioManager.getVolumeIndexForAttributes(aa); - int indexMax = mAudioManager.getMaxVolumeIndexForAttributes(aa); - int indexMin = mAudioManager.getMinVolumeIndexForAttributes(aa); - - final int indexForAa = incrementVolumeIndex(index, indexMin, indexMax); - - // Set the receiver to filter only the current group callback - validCb.setExpectedVolumeGroup(volumeGroupId); - mAudioManager.setVolumeIndexForAttributes(aa, indexForAa, 0/*flags*/); - assertTrue(validCb.waitForExpectedVolumeGroupChanged( - AudioVolumeGroupCallbackHelper.ASYNC_TIMEOUT_MS)); - - final int readIndex = mAudioManager.getVolumeIndexForAttributes(aa); - assertEquals(readIndex, indexForAa); - } - } finally { - audioAudioVolumeGroupChangedHandler.unregisterListener(validCb); - } - } - - @Test - public void testMultipleCallbackReceived() { - - final AudioVolumeGroupChangeHandler audioAudioVolumeGroupChangedHandler = - new AudioVolumeGroupChangeHandler(); - - audioAudioVolumeGroupChangedHandler.init(); - - final int callbackCount = 10; - final List<AudioVolumeGroupCallbackHelper> validCbs = - new ArrayList<AudioVolumeGroupCallbackHelper>(); - for (int i = 0; i < callbackCount; i++) { - validCbs.add(new AudioVolumeGroupCallbackHelper()); - } - for (final AudioVolumeGroupCallbackHelper cb : validCbs) { - audioAudioVolumeGroupChangedHandler.registerListener(cb); - } - - List<AudioVolumeGroup> audioVolumeGroups = mAudioManager.getAudioVolumeGroups(); - assertTrue(audioVolumeGroups.size() > 0); - - try { - for (final AudioVolumeGroup audioVolumeGroup : audioVolumeGroups) { - int volumeGroupId = audioVolumeGroup.getId(); - - List<AudioAttributes> avgAttributes = audioVolumeGroup.getAudioAttributes(); - // Set the volume per attributes (if valid) and wait the callback - if (avgAttributes.size() == 0 || avgAttributes.get(0).equals(DEFAULT_ATTRIBUTES)) { - // Some volume groups may not have valid attributes, used for internal - // volume management like patch/rerouting - // so bailing out strategy retrieval from attributes - continue; - } - AudioAttributes aa = avgAttributes.get(0); - - int index = mAudioManager.getVolumeIndexForAttributes(aa); - int indexMax = mAudioManager.getMaxVolumeIndexForAttributes(aa); - int indexMin = mAudioManager.getMinVolumeIndexForAttributes(aa); - - final int indexForAa = incrementVolumeIndex(index, indexMin, indexMax); - - // Set the receiver to filter only the current group callback - for (final AudioVolumeGroupCallbackHelper cb : validCbs) { - cb.setExpectedVolumeGroup(volumeGroupId); - } - mAudioManager.setVolumeIndexForAttributes(aa, indexForAa, 0/*flags*/); - - for (final AudioVolumeGroupCallbackHelper cb : validCbs) { - assertTrue(cb.waitForExpectedVolumeGroupChanged( - AudioVolumeGroupCallbackHelper.ASYNC_TIMEOUT_MS)); - } - int readIndex = mAudioManager.getVolumeIndexForAttributes(aa); - assertEquals(readIndex, indexForAa); - } - } finally { - for (final AudioVolumeGroupCallbackHelper cb : validCbs) { - audioAudioVolumeGroupChangedHandler.unregisterListener(cb); - } - } - } -} diff --git a/media/tests/projection/Android.bp b/media/tests/projection/Android.bp index 94db2c02eb28..48621e4e2094 100644 --- a/media/tests/projection/Android.bp +++ b/media/tests/projection/Android.bp @@ -16,7 +16,6 @@ android_test { name: "MediaProjectionTests", srcs: ["**/*.java"], - libs: [ "android.test.base.stubs.system", "android.test.mock.stubs.system", @@ -30,6 +29,7 @@ android_test { "frameworks-base-testutils", "mockito-target-extended-minus-junit4", "platform-test-annotations", + "cts-mediaprojection-common", "testng", "testables", "truth", @@ -42,7 +42,11 @@ android_test { "libstaticjvmtiagent", ], - test_suites: ["device-tests"], + data: [ + ":CtsMediaProjectionTestCasesHelperApp", + ], + + test_suites: ["general-tests"], platform_apis: true, diff --git a/media/tests/projection/AndroidManifest.xml b/media/tests/projection/AndroidManifest.xml index 0c9760400ce0..514fb5f689c9 100644 --- a/media/tests/projection/AndroidManifest.xml +++ b/media/tests/projection/AndroidManifest.xml @@ -20,6 +20,8 @@ android:sharedUserId="com.android.uid.test"> <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" /> <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" /> + <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> + <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <application android:debuggable="true" android:testOnly="true"> diff --git a/media/tests/projection/AndroidTest.xml b/media/tests/projection/AndroidTest.xml index f64930a0eb3f..99b42d1cd263 100644 --- a/media/tests/projection/AndroidTest.xml +++ b/media/tests/projection/AndroidTest.xml @@ -22,6 +22,15 @@ <option name="install-arg" value="-t" /> <option name="test-file-name" value="MediaProjectionTests.apk" /> </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="force-install-mode" value="FULL"/>ss + <option name="test-file-name" value="CtsMediaProjectionTestCasesHelperApp.apk" /> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="run-command" value="input keyevent KEYCODE_WAKEUP" /> + <option name="run-command" value="wm dismiss-keyguard" /> + </target_preparer> <option name="test-tag" value="MediaProjectionTests" /> <test class="com.android.tradefed.testtype.AndroidJUnitTest"> diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionStoppingTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionStoppingTest.java new file mode 100644 index 000000000000..0b84e01c4d02 --- /dev/null +++ b/media/tests/projection/src/android/media/projection/MediaProjectionStoppingTest.java @@ -0,0 +1,293 @@ +/* + * 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.media.projection; + +import static com.android.compatibility.common.util.FeatureUtil.isAutomotive; +import static com.android.compatibility.common.util.FeatureUtil.isTV; +import static com.android.compatibility.common.util.FeatureUtil.isWatch; +import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +import android.Manifest; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.media.cts.MediaProjectionRule; +import android.os.UserHandle; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.telecom.TelecomManager; +import android.telephony.TelephonyCallback; +import android.telephony.TelephonyManager; +import android.util.Log; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.Until; + +import com.android.compatibility.common.util.ApiTest; +import com.android.compatibility.common.util.FrameworkSpecificTest; +import com.android.media.projection.flags.Flags; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * Test {@link MediaProjection} stopping behavior. + * + * Run with: + * atest MediaProjectionTests:MediaProjectionStoppingTest + */ +@FrameworkSpecificTest +public class MediaProjectionStoppingTest { + private static final String TAG = "MediaProjectionStoppingTest"; + private static final int STOP_DIALOG_WAIT_TIMEOUT_MS = 5000; + private static final String CALL_HELPER_START_CALL = "start_call"; + private static final String CALL_HELPER_STOP_CALL = "stop_call"; + private static final String STOP_DIALOG_TITLE_RES_ID = "android:id/alertTitle"; + private static final String STOP_DIALOG_CLOSE_BUTTON_RES_ID = "android:id/button2"; + + @Rule public MediaProjectionRule mMediaProjectionRule = new MediaProjectionRule(); + + private Context mContext; + private int mTimeoutMs; + private TelecomManager mTelecomManager; + private TelephonyManager mTelephonyManager; + private TestCallStateListener mTestCallStateListener; + + @Before + public void setUp() throws InterruptedException { + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + runWithShellPermissionIdentity( + () -> { + mContext.getPackageManager() + .revokeRuntimePermission( + mContext.getPackageName(), + Manifest.permission.SYSTEM_ALERT_WINDOW, + new UserHandle(mContext.getUserId())); + }); + mTimeoutMs = 1000; + + mTestCallStateListener = new TestCallStateListener(mContext); + } + + @After + public void cleanup() { + mTestCallStateListener.release(); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_STOP_MEDIA_PROJECTION_ON_CALL_END) + @ApiTest(apis = "android.media.projection.MediaProjection.Callback#onStop") + public void testMediaProjectionStop_callStartedAfterMediaProjection_doesNotStop() + throws Exception { + assumeTrue(mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM)); + + mMediaProjectionRule.startMediaProjection(); + + CountDownLatch latch = new CountDownLatch(1); + mMediaProjectionRule.registerCallback( + new MediaProjection.Callback() { + @Override + public void onStop() { + latch.countDown(); + } + }); + mMediaProjectionRule.createVirtualDisplay(); + + try { + startPhoneCall(); + } finally { + endPhoneCall(); + } + + assertWithMessage("MediaProjection should not be stopped on call end") + .that(latch.await(mTimeoutMs, TimeUnit.MILLISECONDS)).isFalse(); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_STOP_MEDIA_PROJECTION_ON_CALL_END) + @RequiresFlagsDisabled(Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END) + @ApiTest(apis = "android.media.projection.MediaProjection.Callback#onStop") + public void + testMediaProjectionStop_callStartedBeforeMediaProjection_stopDialogFlagDisabled__shouldStop() + throws Exception { + assumeTrue(mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM)); + CountDownLatch latch = new CountDownLatch(1); + try { + startPhoneCall(); + + mMediaProjectionRule.startMediaProjection(); + + mMediaProjectionRule.registerCallback( + new MediaProjection.Callback() { + @Override + public void onStop() { + latch.countDown(); + } + }); + mMediaProjectionRule.createVirtualDisplay(); + + } finally { + endPhoneCall(); + } + + assertWithMessage("MediaProjection was not stopped after call end") + .that(latch.await(mTimeoutMs, TimeUnit.MILLISECONDS)).isTrue(); + } + + @Test + @RequiresFlagsEnabled({ + Flags.FLAG_STOP_MEDIA_PROJECTION_ON_CALL_END, + Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END + }) + public void + callEnds_mediaProjectionStartedDuringCallAndIsActive_stopDialogFlagEnabled_showsStopDialog() + throws Exception { + // MediaProjection stop Dialog is only available on phones. + assumeFalse(isWatch()); + assumeFalse(isAutomotive()); + assumeFalse(isTV()); + + assumeTrue(mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM)); + + try { + startPhoneCall(); + mMediaProjectionRule.startMediaProjection(); + + mMediaProjectionRule.registerCallback( + new MediaProjection.Callback() { + @Override + public void onStop() { + fail( + "MediaProjection should not be stopped when" + + " FLAG_SHOW_STOP_DIALOG_POST_CALL_END is enabled"); + } + }); + mMediaProjectionRule.createVirtualDisplay(); + + } finally { + endPhoneCall(); + } + + UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + boolean isDialogShown = + device.wait( + Until.hasObject(By.res(STOP_DIALOG_TITLE_RES_ID)), + STOP_DIALOG_WAIT_TIMEOUT_MS); + assertWithMessage("Stop dialog should be visible").that(isDialogShown).isTrue(); + + // Find and click the "Close" button + boolean hasCloseButton = + device.wait( + Until.hasObject(By.res(STOP_DIALOG_CLOSE_BUTTON_RES_ID)), + STOP_DIALOG_WAIT_TIMEOUT_MS); + if (hasCloseButton) { + device.findObject(By.res(STOP_DIALOG_CLOSE_BUTTON_RES_ID)).click(); + Log.d(TAG, "Clicked on 'Close' button to dismiss the stop dialog."); + } else { + fail("Close button not found, unable to dismiss stop dialog."); + } + } + + private void startPhoneCall() throws InterruptedException { + mTestCallStateListener.assertCallState(false); + mContext.startActivity(getCallHelperIntent(CALL_HELPER_START_CALL)); + mTestCallStateListener.waitForNextCallState(true, mTimeoutMs, TimeUnit.MILLISECONDS); + } + + private void endPhoneCall() throws InterruptedException { + mTestCallStateListener.assertCallState(true); + mContext.startActivity(getCallHelperIntent(CALL_HELPER_STOP_CALL)); + mTestCallStateListener.waitForNextCallState(false, mTimeoutMs, TimeUnit.MILLISECONDS); + } + + private Intent getCallHelperIntent(String action) { + return new Intent(action) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK) + .setComponent( + new ComponentName( + "android.media.projection.cts.helper", + "android.media.projection.cts.helper.CallHelperActivity")); + } + + private static final class TestCallStateListener extends TelephonyCallback + implements TelephonyCallback.CallStateListener { + private final BlockingQueue<Boolean> mCallStates = new LinkedBlockingQueue<>(); + private final TelecomManager mTelecomManager; + private final TelephonyManager mTelephonyManager; + + private TestCallStateListener(Context context) throws InterruptedException { + mTelecomManager = context.getSystemService(TelecomManager.class); + mTelephonyManager = context.getSystemService(TelephonyManager.class); + mCallStates.offer(isInCall()); + + assertThat(mCallStates.take()).isFalse(); + + runWithShellPermissionIdentity( + () -> + mTelephonyManager.registerTelephonyCallback( + context.getMainExecutor(), this)); + } + + public void release() { + runWithShellPermissionIdentity( + () -> mTelephonyManager.unregisterTelephonyCallback(this)); + } + + @Override + public void onCallStateChanged(int state) { + mCallStates.offer(isInCall()); + } + + public void waitForNextCallState(boolean expectedCallState, long timeout, TimeUnit unit) + throws InterruptedException { + String message = + String.format( + "Call was not %s after timeout", + expectedCallState ? "started" : "ended"); + + boolean value; + do { + value = mCallStates.poll(timeout, unit); + } while (value != expectedCallState); + assertWithMessage(message).that(value).isEqualTo(expectedCallState); + } + + private boolean isInCall() { + return runWithShellPermissionIdentity(mTelecomManager::isInCall); + } + + public void assertCallState(boolean expected) { + assertWithMessage("Unexpected call state").that(isInCall()).isEqualTo(expected); + } + } +} diff --git a/packages/CredentialManager/wear/AndroidManifest.xml b/packages/CredentialManager/wear/AndroidManifest.xml index b480ac30d2cb..c91bf13bf98e 100644 --- a/packages/CredentialManager/wear/AndroidManifest.xml +++ b/packages/CredentialManager/wear/AndroidManifest.xml @@ -32,7 +32,8 @@ android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:label="@string/app_name" - android:supportsRtl="true"> + android:supportsRtl="true" + android:theme="@style/Theme.CredentialSelector"> <!-- Activity called by GMS has to be exactly: com.android.credentialmanager.CredentialSelectorActivity --> @@ -42,7 +43,8 @@ android:exported="true" android:label="@string/app_name" android:launchMode="singleTop" - android:permission="android.permission.LAUNCH_CREDENTIAL_SELECTOR" /> + android:permission="android.permission.LAUNCH_CREDENTIAL_SELECTOR" + android:theme="@style/Theme.CredentialSelector"/> </application> </manifest> diff --git a/packages/CredentialManager/wear/res/values/themes.xml b/packages/CredentialManager/wear/res/values/themes.xml new file mode 100644 index 000000000000..22329e9ff2ce --- /dev/null +++ b/packages/CredentialManager/wear/res/values/themes.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<resources> + <style name="Theme.CredentialSelector" parent="@*android:style/ThemeOverlay.DeviceDefault.Accent.DayNight"> + <item name="android:windowContentOverlay">@null</item> + <item name="android:windowNoTitle">true</item> + <item name="android:windowBackground">@android:color/transparent</item> + <item name="android:windowIsTranslucent">true</item> + </style> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/AdaptiveIcon/Android.bp b/packages/SettingsLib/AdaptiveIcon/Android.bp index 67b6fb5f2ed9..c9409e44c8ee 100644 --- a/packages/SettingsLib/AdaptiveIcon/Android.bp +++ b/packages/SettingsLib/AdaptiveIcon/Android.bp @@ -18,8 +18,9 @@ android_library { resource_dirs: ["res"], static_libs: [ - "androidx.annotation_annotation", "SettingsLibTile", + "androidx.annotation_annotation", + "androidx.core_core", ], min_sdk_version: "21", diff --git a/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveIconShapeDrawable.java b/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveIconShapeDrawable.java index 4d7610cf97b6..aa1d35afab4a 100644 --- a/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveIconShapeDrawable.java +++ b/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveIconShapeDrawable.java @@ -23,7 +23,8 @@ import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.PathShape; import android.util.AttributeSet; -import android.util.PathParser; + +import androidx.core.graphics.PathParser; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; diff --git a/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java b/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java index 12c234ecd739..c7ca18a9c339 100644 --- a/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java +++ b/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java @@ -27,12 +27,12 @@ import android.graphics.Rect; import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.DrawableWrapper; import android.os.RemoteException; -import android.util.PathParser; import android.view.IWindowManager; import android.view.WindowManagerGlobal; import androidx.annotation.IntDef; import androidx.annotation.VisibleForTesting; +import androidx.core.graphics.PathParser; import com.android.settingslib.widget.adaptiveicon.R; diff --git a/packages/SettingsLib/SettingsTheme/res/layout/settingslib_expressive_collapsable_textview.xml b/packages/SettingsLib/SettingsTheme/res/layout/settingslib_expressive_collapsable_textview.xml index 7d7bec14ed78..cc55cacb9e5c 100644 --- a/packages/SettingsLib/SettingsTheme/res/layout/settingslib_expressive_collapsable_textview.xml +++ b/packages/SettingsLib/SettingsTheme/res/layout/settingslib_expressive_collapsable_textview.xml @@ -44,10 +44,11 @@ <com.android.settingslib.widget.LinkableTextView android:id="@+id/settingslib_expressive_learn_more" - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@android:id/title" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" android:paddingTop="@dimen/settingslib_expressive_space_extrasmall6" android:textAlignment="viewStart" android:clickable="true" diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt index 976711bdc5f3..007dc5143262 100644 --- a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt +++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt @@ -85,6 +85,7 @@ class CollapsableTextView @JvmOverloads constructor( Gravity.CENTER_VERTICAL, Gravity.CENTER, Gravity.CENTER_HORIZONTAL -> { centerHorizontally(titleTextView) centerHorizontally(collapseButton) + centerHorizontally(learnMoreTextView) } } isCollapsable = getBoolean(isCollapsableAttr, DEFAULT_COLLAPSABLE) diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt index 2672787a0519..d1c88de3f399 100644 --- a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt +++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsPreferenceGroupAdapter.kt @@ -71,18 +71,27 @@ open class SettingsPreferenceGroupAdapter(preferenceGroup: PreferenceGroup) : override fun onPreferenceHierarchyChange(preference: Preference) { super.onPreferenceHierarchyChange(preference) - // Post after super class has posted their sync runnable to update preferences. - mHandler.removeCallbacks(syncRunnable) - mHandler.post(syncRunnable) + if (SettingsThemeHelper.isExpressiveTheme(preference.context)) { + // Post after super class has posted their sync runnable to update preferences. + mHandler.removeCallbacks(syncRunnable) + mHandler.post(syncRunnable) + } } @SuppressLint("RestrictedApi") override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) { super.onBindViewHolder(holder, position) - updateBackground(holder, position) + + if (SettingsThemeHelper.isExpressiveTheme(holder.itemView.context)) { + updateBackground(holder, position) + } } private fun updatePreferencesList() { + if (!SettingsThemeHelper.isExpressiveTheme(mPreferenceGroup.context)) { + return + } + val oldList = ArrayList(mRoundCornerMappingList) mRoundCornerMappingList = ArrayList() mappingPreferenceGroup(mRoundCornerMappingList, mPreferenceGroup) diff --git a/packages/SettingsLib/Spa/build.gradle.kts b/packages/SettingsLib/Spa/build.gradle.kts index 50408190b3ef..25406d794af9 100644 --- a/packages/SettingsLib/Spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/build.gradle.kts @@ -28,7 +28,7 @@ val androidTop: String = File(rootDir, "../../../../..").canonicalPath allprojects { extra["androidTop"] = androidTop - extra["jetpackComposeVersion"] = "1.8.0-beta02" + extra["jetpackComposeVersion"] = "1.8.0-rc01" } subprojects { @@ -36,11 +36,11 @@ subprojects { plugins.withType<AndroidBasePlugin> { configure<BaseExtension> { - compileSdkVersion(35) + compileSdkVersion(36) defaultConfig { minSdk = 21 - targetSdk = 35 + targetSdk = 36 } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt index 8636524ed23c..b9a3a7f4b48f 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt @@ -124,4 +124,6 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { // For debugging override val searchProviderAuthorities = "com.android.spa.gallery.search.provider" + + override val isSpaExpressiveEnabled = true } diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml index b074f4b91ed0..d041eb011986 100644 --- a/packages/SettingsLib/Spa/gradle/libs.versions.toml +++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml @@ -15,10 +15,10 @@ # [versions] -agp = "8.8.1" +agp = "8.9.0" dexmaker-mockito = "2.28.3" jvm = "21" -kotlin = "2.0.21" +kotlin = "2.1.10" truth = "1.4.4" [libraries] diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts index 7ce5b71f678e..57f5520fd659 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/spa/build.gradle.kts @@ -52,18 +52,18 @@ android { dependencies { api(project(":SettingsLibColor")) api("androidx.appcompat:appcompat:1.7.0") - api("androidx.compose.material3:material3:1.4.0-alpha08") - api("androidx.compose.material:material-icons-extended") + api("androidx.compose.material3:material3:1.4.0-alpha10") + api("androidx.compose.material:material-icons-extended:1.7.8") api("androidx.compose.runtime:runtime-livedata:$jetpackComposeVersion") api("androidx.compose.ui:ui-tooling-preview:$jetpackComposeVersion") api("androidx.graphics:graphics-shapes-android:1.0.1") api("androidx.lifecycle:lifecycle-livedata-ktx") api("androidx.lifecycle:lifecycle-runtime-compose") - api("androidx.navigation:navigation-compose:2.9.0-alpha06") + api("androidx.navigation:navigation-compose:2.9.0-alpha08") api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha") api("com.google.android.material:material:1.13.0-alpha08") debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion") - implementation("com.airbnb.android:lottie-compose:6.4.0") + implementation("com.airbnb.android:lottie-compose:6.5.2") androidTestImplementation(project(":testutils")) androidTestImplementation(libs.dexmaker.mockito) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt index f10f96afd389..395328f86047 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt @@ -78,6 +78,8 @@ fun CategoryTitle(title: String) { /** * A container that is used to group similar items. A [Category] displays a [CategoryTitle] and * visually separates groups of items. + * + * @param content The content of the category. */ @Composable fun Category( @@ -126,7 +128,8 @@ fun Category( * be decided by the index. * @param bottomPadding Optional. Bottom outside padding of the category. * @param state Optional. State of LazyList. - * @param content Optional. Content to be shown at the top of the category. + * @param footer Optional. Content to be shown at the bottom of the category. + * @param header Optional. Content to be shown at the top of the category. */ @Composable fun LazyCategory( @@ -136,7 +139,8 @@ fun LazyCategory( title: ((Int) -> String?)? = null, bottomPadding: Dp = SettingsDimension.paddingSmall, state: LazyListState = rememberLazyListState(), - content: @Composable () -> Unit, + footer: @Composable () -> Unit = {}, + header: @Composable () -> Unit, ) { Column( Modifier.padding( @@ -154,12 +158,14 @@ fun LazyCategory( verticalArrangement = Arrangement.spacedBy(SettingsDimension.paddingTiny), state = state, ) { - item { CompositionLocalProvider(LocalIsInCategory provides true) { content() } } + item { CompositionLocalProvider(LocalIsInCategory provides true) { header() } } items(count = list.size, key = key) { title?.invoke(it)?.let { title -> CategoryTitle(title) } CompositionLocalProvider(LocalIsInCategory provides true) { entry(it)() } } + + item { CompositionLocalProvider(LocalIsInCategory provides true) { footer() } } } } } @@ -189,3 +195,28 @@ private fun CategoryPreview() { } } } + +@Preview +@Composable +private fun LazyCategoryPreview() { + SettingsTheme { + LazyCategory( + list = listOf(1, 2, 3), + entry = { key -> + @Composable { + Preference( + object : PreferenceModel { + override val title = key.toString() + } + ) + } + }, + footer = @Composable { + Footer("Footer") + }, + header = @Composable { + Text("Header") + }, + ) + } +} diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt index 4b4a8c20b39e..7d199511044a 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt @@ -71,10 +71,17 @@ class CategoryTest { } @Test - fun lazyCategory_content_displayed() { + fun lazyCategory_headerDisplayed() { composeTestRule.setContent { TestLazyCategory() } - composeTestRule.onNodeWithText("text").assertExists() + composeTestRule.onNodeWithText("Header").assertExists() + } + + @Test + fun lazyCategory_footerDisplayed() { + composeTestRule.setContent { TestLazyCategory() } + + composeTestRule.onNodeWithText("Footer").assertExists() } @Test @@ -102,8 +109,8 @@ private fun TestLazyCategory() { list = list, entry = { index: Int -> @Composable { Preference(list[index]) } }, title = { index: Int -> if (index == 0) "LazyCategory $index" else null }, - ) { - Text("text") - } + footer = @Composable { Footer("Footer") }, + header = @Composable { Text("Header") }, + ) } } diff --git a/packages/SettingsLib/StatusBannerPreference/res/layout/settingslib_expressive_preference_statusbanner.xml b/packages/SettingsLib/StatusBannerPreference/res/layout/settingslib_expressive_preference_statusbanner.xml index 083b862e8a5c..c778fb03e04f 100644 --- a/packages/SettingsLib/StatusBannerPreference/res/layout/settingslib_expressive_preference_statusbanner.xml +++ b/packages/SettingsLib/StatusBannerPreference/res/layout/settingslib_expressive_preference_statusbanner.xml @@ -55,6 +55,27 @@ android:layout_gravity="center" android:scaleType="centerInside"/> + <com.google.android.material.progressindicator.CircularProgressIndicator + android:id="@+id/progress_indicator" + style="@style/Widget.Material3.CircularProgressIndicator" + android:layout_width="@dimen/settingslib_expressive_space_medium4" + android:layout_height="@dimen/settingslib_expressive_space_medium4" + android:layout_gravity="center" + android:scaleType="centerInside" + android:indeterminate="false" + android:max="100" + android:progress="0" + android:visibility="gone" /> + + <com.google.android.material.loadingindicator.LoadingIndicator + android:id="@+id/loading_indicator" + style="@style/Widget.Material3.LoadingIndicator" + android:layout_width="@dimen/settingslib_expressive_space_medium4" + android:layout_height="@dimen/settingslib_expressive_space_medium4" + android:layout_gravity="center" + android:scaleType="centerInside" + android:visibility="gone" /> + </FrameLayout> <LinearLayout diff --git a/packages/SettingsLib/StatusBannerPreference/res/values/attrs.xml b/packages/SettingsLib/StatusBannerPreference/res/values/attrs.xml index deda2586c2e0..bb9a5ad689cd 100644 --- a/packages/SettingsLib/StatusBannerPreference/res/values/attrs.xml +++ b/packages/SettingsLib/StatusBannerPreference/res/values/attrs.xml @@ -22,6 +22,8 @@ <enum name="medium" value="2"/> <enum name="high" value="3"/> <enum name="off" value="4"/> + <enum name="loading_determinate" value="5"/> + <enum name="loading_indeterminate" value="6"/> </attr> <attr name="buttonLevel" format="enum"> <enum name="generic" value="0"/> diff --git a/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt b/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt index eda281c07053..e6c6638f7de4 100644 --- a/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt +++ b/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt @@ -28,6 +28,7 @@ import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import com.android.settingslib.widget.preference.statusbanner.R import com.google.android.material.button.MaterialButton +import com.google.android.material.progressindicator.CircularProgressIndicator class StatusBannerPreference @JvmOverloads constructor( context: Context, @@ -41,7 +42,9 @@ class StatusBannerPreference @JvmOverloads constructor( LOW, MEDIUM, HIGH, - OFF + OFF, + LOADING_DETERMINATE, // The loading progress is set by the caller. + LOADING_INDETERMINATE // No loading progress. Just loading animation } var iconLevel: BannerStatus = BannerStatus.GENERIC set(value) { @@ -60,6 +63,8 @@ class StatusBannerPreference @JvmOverloads constructor( } private var listener: View.OnClickListener? = null + private var circularProgressIndicator: CircularProgressIndicator? = null + init { layoutResource = R.layout.settingslib_expressive_preference_statusbanner isSelectable = false @@ -89,6 +94,8 @@ class StatusBannerPreference @JvmOverloads constructor( 2 -> BannerStatus.MEDIUM 3 -> BannerStatus.HIGH 4 -> BannerStatus.OFF + 5 -> BannerStatus.LOADING_DETERMINATE + 6 -> BannerStatus.LOADING_INDETERMINATE else -> BannerStatus.GENERIC } @@ -102,7 +109,38 @@ class StatusBannerPreference @JvmOverloads constructor( } holder.findViewById(android.R.id.icon_frame)?.apply { - visibility = if (icon != null) View.VISIBLE else View.GONE + visibility = + if ( + icon != null || iconLevel == BannerStatus.LOADING_DETERMINATE || + iconLevel == BannerStatus.LOADING_INDETERMINATE + ) + View.VISIBLE + else View.GONE + } + + holder.findViewById(android.R.id.icon)?.apply { + visibility = + if (iconLevel == BannerStatus.LOADING_DETERMINATE || + iconLevel == BannerStatus.LOADING_INDETERMINATE) + View.GONE + else View.VISIBLE + } + + circularProgressIndicator = holder.findViewById(R.id.progress_indicator) + as? CircularProgressIndicator + + (circularProgressIndicator)?.apply { + visibility = + if (iconLevel == BannerStatus.LOADING_DETERMINATE) + View.VISIBLE + else View.GONE + } + + holder.findViewById(R.id.loading_indicator)?.apply { + visibility = + if (iconLevel == BannerStatus.LOADING_INDETERMINATE) + View.VISIBLE + else View.GONE } (holder.findViewById(R.id.status_banner_button) as? MaterialButton)?.apply { @@ -116,6 +154,10 @@ class StatusBannerPreference @JvmOverloads constructor( } } + fun getProgressIndicator(): CircularProgressIndicator? { + return circularProgressIndicator + } + /** * Sets the text to be displayed in button. */ @@ -203,7 +245,7 @@ class StatusBannerPreference @JvmOverloads constructor( R.drawable.settingslib_expressive_background_level_high ) - // GENERIC and OFF are using the same background drawable. + // Using the same background drawable for other levels. else -> ContextCompat.getDrawable( context, R.drawable.settingslib_expressive_background_generic diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig index 349d13a29b05..90e5a010416c 100644 --- a/packages/SettingsLib/aconfig/settingslib.aconfig +++ b/packages/SettingsLib/aconfig/settingslib.aconfig @@ -37,16 +37,6 @@ flag { } flag { - name: "enable_set_preferred_transport_for_le_audio_device" - namespace: "bluetooth" - description: "Enable setting preferred transport for Le Audio device" - bug: "330581926" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "enable_determining_advanced_details_header_with_metadata" namespace: "pixel_cross_device_control" description: "Use metadata instead of device type to determine whether a bluetooth device should use advanced details header." diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUiController.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUiController.java index e7ddc46093e3..f7da8a57410f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUiController.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUiController.java @@ -80,6 +80,7 @@ public class AmbientVolumeUiController implements mLocalDataManager = new HearingDeviceLocalDataManager(context); mLocalDataManager.setOnDeviceLocalDataChangeListener(this, ThreadUtils.getBackgroundExecutor()); + mLocalDataManager.start(); } @VisibleForTesting diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index ae9ad958b287..33dcb051d194 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -1068,18 +1068,42 @@ public class BluetoothUtils { /** Get primary device Uri in broadcast. */ @NonNull public static String getPrimaryGroupIdUriForBroadcast() { + // TODO: once API is stable, deprecate SettingsProvider solution return "bluetooth_le_broadcast_fallback_active_group_id"; } - /** Get primary device group id in broadcast. */ + /** Get primary device group id in broadcast from SettingsProvider. */ @WorkerThread public static int getPrimaryGroupIdForBroadcast(@NonNull ContentResolver contentResolver) { + // TODO: once API is stable, deprecate SettingsProvider solution return Settings.Secure.getInt( contentResolver, getPrimaryGroupIdUriForBroadcast(), BluetoothCsipSetCoordinator.GROUP_ID_INVALID); } + /** + * Get primary device group id in broadcast. + * + * If Flags.adoptPrimaryGroupManagementApiV2 is enabled, get group id by API, + * Otherwise, still get value from SettingsProvider. + */ + @WorkerThread + public static int getPrimaryGroupIdForBroadcast(@NonNull ContentResolver contentResolver, + @Nullable LocalBluetoothManager manager) { + if (Flags.adoptPrimaryGroupManagementApiV2()) { + LeAudioProfile leaProfile = manager == null ? null : + manager.getProfileManager().getLeAudioProfile(); + if (leaProfile == null) { + Log.d(TAG, "getPrimaryGroupIdForBroadcast: profile is null"); + return BluetoothCsipSetCoordinator.GROUP_ID_INVALID; + } + return leaProfile.getBroadcastToUnicastFallbackGroup(); + } else { + return getPrimaryGroupIdForBroadcast(contentResolver); + } + } + /** Get develop option value for audio sharing preview. */ @WorkerThread public static boolean getAudioSharingPreviewValue(@Nullable ContentResolver contentResolver) { @@ -1101,7 +1125,7 @@ public class BluetoothUtils { LocalBluetoothLeBroadcast broadcast = localBtManager.getProfileManager().getLeAudioBroadcastProfile(); if (broadcast == null || !broadcast.isEnabled(null)) return null; - int primaryGroupId = getPrimaryGroupIdForBroadcast(contentResolver); + int primaryGroupId = getPrimaryGroupIdForBroadcast(contentResolver, localBtManager); if (primaryGroupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) return null; LocalBluetoothLeBroadcastAssistant assistant = localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 011b2fc15807..edec2e427315 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -75,6 +75,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; +import java.util.stream.IntStream; import java.util.stream.Stream; /** @@ -154,8 +155,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> private boolean mIsLeAudioProfileConnectedFail = false; private boolean mUnpairing; @Nullable - private final InputDevice mInputDevice; - private final boolean mIsDeviceStylus; + private InputDevice mInputDevice; + private boolean mIsDeviceStylus; // Group second device for Hearing Aid private CachedBluetoothDevice mSubDevice; @@ -313,8 +314,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> mLocalNapRoleConnected = true; } } - if (Flags.enableSetPreferredTransportForLeAudioDevice() - && profile instanceof HidProfile) { + if (profile instanceof HidProfile) { updatePreferredTransport(); } } else if (profile instanceof MapProfile @@ -329,8 +329,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> mLocalNapRoleConnected = false; } - if (Flags.enableSetPreferredTransportForLeAudioDevice() - && profile instanceof LeAudioProfile) { + if (profile instanceof LeAudioProfile) { updatePreferredTransport(); } @@ -762,11 +761,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> * {@link BluetoothDevice#BATTERY_LEVEL_UNKNOWN} */ public int getMinBatteryLevelWithMemberDevices() { - return Stream.concat(Stream.of(this), mMemberDevices.stream()) - .mapToInt(cachedDevice -> cachedDevice.getBatteryLevel()) - .filter(batteryLevel -> batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) - .min() - .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN); + return getMinBatteryLevels(Stream.concat(Stream.of(this), mMemberDevices.stream()) + .mapToInt(CachedBluetoothDevice::getBatteryLevel)); } /** @@ -789,6 +785,13 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> : null; } + private int getMinBatteryLevels(IntStream batteryLevels) { + return batteryLevels + .filter(battery -> battery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) + .min() + .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN); + } + void refresh() { ListenableFuture<Void> future = ThreadUtils.getBackgroundExecutor().submit(() -> { if (BluetoothUtils.isAdvancedDetailsHeader(mDevice)) { @@ -1358,7 +1361,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> // Gets summary for the buds which are in the audio sharing. int groupId = BluetoothUtils.getGroupId(this); int primaryGroupId = BluetoothUtils.getPrimaryGroupIdForBroadcast( - mContext.getContentResolver()); + mContext.getContentResolver(), mBluetoothManager); if ((primaryGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) ? (groupId == primaryGroupId) : isActiveDevice(BluetoothProfile.LE_AUDIO)) { // The buds are primary buds @@ -1674,10 +1677,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> return null; } else { int overallBattery = - Arrays.stream(new int[]{leftBattery, rightBattery, caseBattery}) - .filter(battery -> battery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) - .min() - .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN); + getMinBatteryLevels( + Arrays.stream(new int[]{leftBattery, rightBattery, caseBattery})); Log.d(TAG, "Acquired battery info from metadata for untethered device " + mDevice.getAnonymizedAddress() + " left earbud battery: " + leftBattery @@ -1711,10 +1712,75 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> @Nullable private BatteryLevelsInfo getBatteryFromBluetoothService() { - // TODO(b/397847825): Implement the logic to get battery from Bluetooth service. - return null; + BatteryLevelsInfo batteryLevelsInfo; + if (isConnectedHearingAidDevice()) { + // If the device is hearing aid device, sides can be distinguished by HearingAidInfo. + batteryLevelsInfo = getBatteryOfHearingAidDeviceComponents(); + if (batteryLevelsInfo != null) { + return batteryLevelsInfo; + } + } + if (isConnectedLeAudioDevice()) { + // If the device is LE Audio device, sides can be distinguished by LeAudioProfile. + batteryLevelsInfo = getBatteryOfLeAudioDeviceComponents(); + if (batteryLevelsInfo != null) { + return batteryLevelsInfo; + } + } + int overallBattery = getMinBatteryLevelWithMemberDevices(); + return overallBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN + ? new BatteryLevelsInfo( + BluetoothDevice.BATTERY_LEVEL_UNKNOWN, + BluetoothDevice.BATTERY_LEVEL_UNKNOWN, + BluetoothDevice.BATTERY_LEVEL_UNKNOWN, + overallBattery) + : null; } + @Nullable + private BatteryLevelsInfo getBatteryOfHearingAidDeviceComponents() { + if (getDeviceSide() == HearingAidInfo.DeviceSide.SIDE_LEFT_AND_RIGHT) { + return new BatteryLevelsInfo( + BluetoothDevice.BATTERY_LEVEL_UNKNOWN, + BluetoothDevice.BATTERY_LEVEL_UNKNOWN, + BluetoothDevice.BATTERY_LEVEL_UNKNOWN, + mDevice.getBatteryLevel()); + } + + int leftBattery = getHearingAidSideBattery(HearingAidInfo.DeviceSide.SIDE_LEFT); + int rightBattery = getHearingAidSideBattery(HearingAidInfo.DeviceSide.SIDE_RIGHT); + int overallBattery = getMinBatteryLevels( + Arrays.stream(new int[]{leftBattery, rightBattery})); + + Log.d(TAG, "Acquired battery info from Bluetooth service for hearing aid device " + + mDevice.getAnonymizedAddress() + + " left battery: " + leftBattery + + " right battery: " + rightBattery + + " overall battery: " + overallBattery); + return overallBattery > BluetoothDevice.BATTERY_LEVEL_UNKNOWN + ? new BatteryLevelsInfo( + leftBattery, + rightBattery, + BluetoothDevice.BATTERY_LEVEL_UNKNOWN, + overallBattery) + : null; + } + + private int getHearingAidSideBattery(int side) { + Optional<CachedBluetoothDevice> connectedHearingAidSide = getConnectedHearingAidSide(side); + return connectedHearingAidSide.isPresent() + ? connectedHearingAidSide + .map(CachedBluetoothDevice::getBatteryLevel) + .filter(batteryLevel -> batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) + .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN) + : BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + } + + @Nullable + private BatteryLevelsInfo getBatteryOfLeAudioDeviceComponents() { + // TODO(b/397847825): Implement the logic to get battery of LE audio device components. + return null; + } private CharSequence getTvBatterySummary(int mainBattery, int leftBattery, int rightBattery, int lowBatteryColorRes) { // Since there doesn't seem to be a way to use format strings to add the @@ -1833,10 +1899,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> // Retrieve hearing aids (ASHA, HAP) individual side battery level if (leftBattery == BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { - leftBattery = getConnectedHearingAidSide(HearingAidInfo.DeviceSide.SIDE_LEFT) - .map(CachedBluetoothDevice::getBatteryLevel) - .filter(batteryLevel -> batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) - .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN); + leftBattery = getHearingAidSideBattery(HearingAidInfo.DeviceSide.SIDE_LEFT); } return leftBattery; @@ -1852,10 +1915,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> // Retrieve hearing aids (ASHA, HAP) individual side battery level if (rightBattery == BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { - rightBattery = getConnectedHearingAidSide(HearingAidInfo.DeviceSide.SIDE_RIGHT) - .map(CachedBluetoothDevice::getBatteryLevel) - .filter(batteryLevel -> batteryLevel > BluetoothDevice.BATTERY_LEVEL_UNKNOWN) - .orElse(BluetoothDevice.BATTERY_LEVEL_UNKNOWN); + rightBattery = getHearingAidSideBattery(HearingAidInfo.DeviceSide.SIDE_RIGHT); } return rightBattery; @@ -2263,6 +2323,16 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> mBluetoothManager = bluetoothManager; } + @VisibleForTesting + void setIsDeviceStylus(Boolean isDeviceStylus) { + mIsDeviceStylus = isDeviceStylus; + } + + @VisibleForTesting + void setInputDevice(@Nullable InputDevice inputDevice) { + mInputDevice = inputDevice; + } + private boolean isAndroidAuto() { try { ParcelUuid[] uuids = mDevice.getUuids(); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index bf86911ee683..f2c013598cdc 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -431,7 +431,10 @@ public class CsipDeviceManager { for (BluetoothDevice device : sinksToSync) { log("addMemberDevicesIntoMainDevice: sync audio sharing source to " + device.getAnonymizedAddress()); - assistant.addSource(device, metadata, /* isGroupOp= */ false); + if (assistant.getConnectionStatus(device) + == BluetoothProfile.STATE_CONNECTED) { + assistant.addSource(device, metadata, /* isGroupOp= */ false); + } } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt index c4e724554c04..21d518a644a9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt @@ -27,7 +27,6 @@ import android.content.IntentFilter import android.database.ContentObserver import android.os.Handler import android.provider.Settings -import com.android.settingslib.flags.Flags import com.android.settingslib.notification.modes.ZenMode import com.android.settingslib.notification.modes.ZenModesBackend import java.time.Duration @@ -35,6 +34,7 @@ import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow @@ -72,7 +72,7 @@ class ZenModeRepositoryImpl( private val notificationManager: NotificationManager, private val backend: ZenModesBackend, private val contentResolver: ContentResolver, - val scope: CoroutineScope, + val applicationScope: CoroutineScope, val backgroundCoroutineContext: CoroutineContext, // This is nullable just to simplify testing, since SettingsLib doesn't have a good way // to create a fake handler. @@ -104,7 +104,7 @@ class ZenModeRepositoryImpl( awaitClose { context.unregisterReceiver(receiver) } } .flowOn(backgroundCoroutineContext) - .shareIn(started = SharingStarted.WhileSubscribed(), scope = scope) + .shareIn(started = SharingStarted.WhileSubscribed(), scope = applicationScope) } override val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?> by lazy { @@ -129,14 +129,11 @@ class ZenModeRepositoryImpl( .map { mapper(it) } .onStart { emit(mapper(null)) } .flowOn(backgroundCoroutineContext) - .stateIn(scope, SharingStarted.WhileSubscribed(), null) + .stateIn(applicationScope, SharingStarted.WhileSubscribed(), null) private val zenConfigChanged by lazy { if (android.app.Flags.modesUi()) { callbackFlow { - // emit an initial value - trySend(Unit) - val observer = object : ContentObserver(backgroundHandler) { override fun onChange(selfChange: Boolean) { @@ -163,16 +160,18 @@ class ZenModeRepositoryImpl( } } - override val modes: Flow<List<ZenMode>> by lazy { - if (android.app.Flags.modesUi()) { + override val modes: StateFlow<List<ZenMode>> = + if (android.app.Flags.modesUi()) zenConfigChanged .map { backend.modes } .distinctUntilChanged() .flowOn(backgroundCoroutineContext) - } else { - flowOf(emptyList()) - } - } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = backend.modes, + ) + else MutableStateFlow<List<ZenMode>>(emptyList()) /** * Gets the current list of [ZenMode] instances according to the backend. diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java index b7814127b716..8fc4aa81b53f 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java @@ -936,15 +936,60 @@ public class BluetoothUtilsTest { } @Test + @EnableFlags(Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2) + public void getSecondaryDeviceForBroadcast_adoptAPI_noSecondary_returnNull() { + when(mBroadcast.isEnabled(any())).thenReturn(true); + when(mLeAudioProfile.getBroadcastToUnicastFallbackGroup()).thenReturn(1); + when(mDeviceManager.findDevice(mBluetoothDevice)).thenReturn(mCachedBluetoothDevice); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + when(mCachedBluetoothDevice.getGroupId()).thenReturn(1); + BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); + when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(ImmutableList.of(state)); + when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(mBluetoothDevice)); + + assertThat( + BluetoothUtils.getSecondaryDeviceForBroadcast( + mContext.getContentResolver(), mLocalBluetoothManager)) + .isNull(); + } + + @Test + @EnableFlags(Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2) + public void getSecondaryDeviceForBroadcast_adoptAPI_returnCorrectDevice() { + when(mBroadcast.isEnabled(any())).thenReturn(true); + when(mLeAudioProfile.getBroadcastToUnicastFallbackGroup()).thenReturn(1); + CachedBluetoothDevice cachedBluetoothDevice = mock(CachedBluetoothDevice.class); + BluetoothDevice bluetoothDevice = mock(BluetoothDevice.class); + when(cachedBluetoothDevice.getDevice()).thenReturn(bluetoothDevice); + when(cachedBluetoothDevice.getGroupId()).thenReturn(1); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + when(mCachedBluetoothDevice.getGroupId()).thenReturn(2); + when(mDeviceManager.findDevice(bluetoothDevice)).thenReturn(cachedBluetoothDevice); + when(mDeviceManager.findDevice(mBluetoothDevice)).thenReturn(mCachedBluetoothDevice); + BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); + List<Long> bisSyncState = new ArrayList<>(); + bisSyncState.add(1L); + when(state.getBisSyncState()).thenReturn(bisSyncState); + when(mAssistant.getAllSources(any(BluetoothDevice.class))) + .thenReturn(ImmutableList.of(state)); + when(mAssistant.getAllConnectedDevices()) + .thenReturn(ImmutableList.of(mBluetoothDevice, bluetoothDevice)); + + assertThat( + BluetoothUtils.getSecondaryDeviceForBroadcast( + mContext.getContentResolver(), mLocalBluetoothManager)) + .isEqualTo(mCachedBluetoothDevice); + } + + @Test + @DisableFlags(Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2) public void getSecondaryDeviceForBroadcast_noSecondary_returnNull() { Settings.Secure.putInt( mContext.getContentResolver(), BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), 1); when(mBroadcast.isEnabled(any())).thenReturn(true); - CachedBluetoothDeviceManager deviceManager = mock(CachedBluetoothDeviceManager.class); - when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(deviceManager); - when(deviceManager.findDevice(mBluetoothDevice)).thenReturn(mCachedBluetoothDevice); + when(mDeviceManager.findDevice(mBluetoothDevice)).thenReturn(mCachedBluetoothDevice); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); when(mCachedBluetoothDevice.getGroupId()).thenReturn(1); BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class); @@ -958,6 +1003,7 @@ public class BluetoothUtilsTest { } @Test + @DisableFlags(Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2) public void getSecondaryDeviceForBroadcast_returnCorrectDevice() { Settings.Secure.putInt( mContext.getContentResolver(), diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java index f57ee0c0930e..b4384b74ccbe 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java @@ -16,7 +16,6 @@ package com.android.settingslib.bluetooth; import static com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING; -import static com.android.settingslib.flags.Flags.FLAG_ENABLE_SET_PREFERRED_TRANSPORT_FOR_LE_AUDIO_DEVICE; import static com.android.settingslib.flags.Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI; import static com.google.common.truth.Truth.assertThat; @@ -42,6 +41,8 @@ import android.content.Context; import android.graphics.drawable.BitmapDrawable; import android.hardware.input.InputManager; import android.media.AudioManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.text.Spannable; @@ -138,7 +139,6 @@ public class CachedBluetoothDeviceTest { public void setUp() { MockitoAnnotations.initMocks(this); mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_TV_MEDIA_OUTPUT_DIALOG); - mSetFlagsRule.enableFlags(FLAG_ENABLE_SET_PREFERRED_TRANSPORT_FOR_LE_AUDIO_DEVICE); mSetFlagsRule.enableFlags(FLAG_ENABLE_LE_AUDIO_SHARING); mSetFlagsRule.enableFlags(FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI); mContext = RuntimeEnvironment.application; @@ -163,6 +163,7 @@ public class CachedBluetoothDeviceTest { when(mHidProfile.getProfileId()).thenReturn(BluetoothProfile.HID_HOST); when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager); when(mBroadcast.isEnabled(any())).thenReturn(false); + when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile); when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant); mCachedDevice = spy(new CachedBluetoothDevice(mContext, mProfileManager, mDevice)); @@ -2004,6 +2005,70 @@ public class CachedBluetoothDeviceTest { } @Test + @EnableFlags(com.android.settingslib.flags.Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2) + public void getConnectionSummary_adoptAPI_isBroadcastPrimary_fallbackDevice_returnActive() { + when(mBroadcast.isEnabled(any())).thenReturn(true); + when(mCachedDevice.getDevice()).thenReturn(mDevice); + when(mLeAudioProfile.getBroadcastToUnicastFallbackGroup()).thenReturn(1); + + List<Long> bisSyncState = new ArrayList<>(); + bisSyncState.add(1L); + when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState); + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(any())).thenReturn(sourceList); + + when(mCachedDevice.getGroupId()).thenReturn(1); + + assertThat(mCachedDevice.getConnectionSummary(false)) + .isEqualTo(mContext.getString(R.string.bluetooth_active_no_battery_level)); + } + + @Test + @EnableFlags(com.android.settingslib.flags.Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2) + public void getConnectionSummary_adoptAPI_isBroadcastPrimary_activeDevice_returnActive() { + when(mBroadcast.isEnabled(any())).thenReturn(true); + when(mCachedDevice.getDevice()).thenReturn(mDevice); + when(mLeAudioProfile.getBroadcastToUnicastFallbackGroup()).thenReturn( + BluetoothCsipSetCoordinator.GROUP_ID_INVALID); + + List<Long> bisSyncState = new ArrayList<>(); + bisSyncState.add(1L); + when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState); + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(any())).thenReturn(sourceList); + + when(mCachedDevice.getGroupId()).thenReturn(1); + when(mCachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO)).thenReturn(true); + + assertThat(mCachedDevice.getConnectionSummary(false)) + .isEqualTo(mContext.getString(R.string.bluetooth_active_no_battery_level)); + } + + @Test + @EnableFlags(com.android.settingslib.flags.Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2) + public void getConnectionSummary_adoptAPI_isBroadcastNotPrimary_returnActiveMedia() { + when(mBroadcast.isEnabled(any())).thenReturn(true); + when(mCachedDevice.getDevice()).thenReturn(mDevice); + when(mLeAudioProfile.getBroadcastToUnicastFallbackGroup()).thenReturn(1); + + List<Long> bisSyncState = new ArrayList<>(); + bisSyncState.add(1L); + when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState); + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(any())).thenReturn(sourceList); + + when(mCachedDevice.getGroupId()).thenReturn(BluetoothCsipSetCoordinator.GROUP_ID_INVALID); + + assertThat(mCachedDevice.getConnectionSummary(false)) + .isEqualTo( + mContext.getString(R.string.bluetooth_active_media_only_no_battery_level)); + } + + @Test + @DisableFlags(com.android.settingslib.flags.Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2) public void getConnectionSummary_isBroadcastPrimary_fallbackDevice_returnActive() { when(mBroadcast.isEnabled(any())).thenReturn(true); when(mCachedDevice.getDevice()).thenReturn(mDevice); @@ -2026,6 +2091,7 @@ public class CachedBluetoothDeviceTest { } @Test + @DisableFlags(com.android.settingslib.flags.Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2) public void getConnectionSummary_isBroadcastPrimary_activeDevice_returnActive() { when(mBroadcast.isEnabled(any())).thenReturn(true); when(mCachedDevice.getDevice()).thenReturn(mDevice); @@ -2049,6 +2115,7 @@ public class CachedBluetoothDeviceTest { } @Test + @DisableFlags(com.android.settingslib.flags.Flags.FLAG_ADOPT_PRIMARY_GROUP_MANAGEMENT_API_V2) public void getConnectionSummary_isBroadcastNotPrimary_returnActiveMedia() { when(mBroadcast.isEnabled(any())).thenReturn(true); when(mCachedDevice.getDevice()).thenReturn(mDevice); @@ -2231,11 +2298,7 @@ public class CachedBluetoothDeviceTest { "false".getBytes()); when(mDevice.getMetadata(BluetoothDevice.METADATA_MAIN_BATTERY)).thenReturn( MAIN_BATTERY.getBytes()); - when(mContext.getSystemService(InputManager.class)).thenReturn(mInputManager); - when(mInputManager.getInputDeviceIds()).thenReturn(new int[]{TEST_DEVICE_ID}); - when(mInputManager.getInputDeviceBluetoothAddress(TEST_DEVICE_ID)).thenReturn( - DEVICE_ADDRESS); - when(mInputManager.getInputDevice(TEST_DEVICE_ID)).thenReturn(mInputDevice); + mCachedDevice.setInputDevice(mInputDevice); BatteryLevelsInfo batteryLevelsInfo = mCachedDevice.getBatteryLevelsInfo(); @@ -2253,10 +2316,9 @@ public class CachedBluetoothDeviceTest { public void getBatteryLevelsInfo_stylusDeviceWithBattery_returnBatteryLevelsInfo() { when(mDevice.getMetadata(BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)).thenReturn( "false".getBytes()); - when(mDevice.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE)).thenReturn( - BluetoothDevice.DEVICE_TYPE_STYLUS.getBytes()); when(mDevice.getMetadata(BluetoothDevice.METADATA_MAIN_BATTERY)).thenReturn( MAIN_BATTERY.getBytes()); + mCachedDevice.setIsDeviceStylus(true); BatteryLevelsInfo batteryLevelsInfo = mCachedDevice.getBatteryLevelsInfo(); @@ -2270,6 +2332,31 @@ public class CachedBluetoothDeviceTest { Integer.parseInt(MAIN_BATTERY)); } + @Test + public void getBatteryLevelsInfo_hearingAidDeviceWithBattery_returnBatteryLevelsInfo() { + when(mDevice.getMetadata(BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)).thenReturn( + "false".getBytes()); + when(mProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile); + updateProfileStatus(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED); + mSubCachedDevice.setHearingAidInfo(getLeftAshaHearingAidInfo()); + when(mSubCachedDevice.getBatteryLevel()).thenReturn(Integer.parseInt(TWS_BATTERY_LEFT)); + updateSubDeviceProfileStatus(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED); + mCachedDevice.setSubDevice(mSubCachedDevice); + mCachedDevice.setHearingAidInfo(getRightAshaHearingAidInfo()); + when(mCachedDevice.getBatteryLevel()).thenReturn(Integer.parseInt(TWS_BATTERY_RIGHT)); + + BatteryLevelsInfo batteryLevelsInfo = mCachedDevice.getBatteryLevelsInfo(); + + assertThat(batteryLevelsInfo.getLeftBatteryLevel()).isEqualTo( + Integer.parseInt(TWS_BATTERY_LEFT)); + assertThat(batteryLevelsInfo.getRightBatteryLevel()).isEqualTo( + Integer.parseInt(TWS_BATTERY_RIGHT)); + assertThat(batteryLevelsInfo.getCaseBatteryLevel()).isEqualTo( + BluetoothDevice.BATTERY_LEVEL_UNKNOWN); + assertThat(batteryLevelsInfo.getOverallBatteryLevel()).isEqualTo( + Integer.parseInt(TWS_BATTERY_LEFT)); + } + private void updateProfileStatus(LocalBluetoothProfile profile, int status) { doReturn(status).when(profile).getConnectionStatus(mDevice); mCachedDevice.onProfileStateChanged(profile, status); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java index fd14d1ff6786..4314982752c3 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java @@ -377,6 +377,7 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state)); + when(mAssistant.getConnectionStatus(mDevice1)).thenReturn(BluetoothAdapter.STATE_CONNECTED); when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); when(mUserManager.isManagedProfile()).thenReturn(true); @@ -407,6 +408,7 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state)); + when(mAssistant.getConnectionStatus(mDevice1)).thenReturn(BluetoothAdapter.STATE_CONNECTED); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) .isTrue(); @@ -474,6 +476,8 @@ public class CsipDeviceManagerTest { BluetoothLeBroadcastReceiveState.class); when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L)); when(mAssistant.getAllSources(mDevice1)).thenReturn(ImmutableList.of(state)); + when(mAssistant.getConnectionStatus(mDevice2)).thenReturn(BluetoothAdapter.STATE_CONNECTED); + when(mAssistant.getConnectionStatus(mDevice3)).thenReturn(BluetoothAdapter.STATE_CONNECTED); assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice)) .isTrue(); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt index b364368df473..ec7baf6bf081 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt @@ -75,10 +75,14 @@ class ZenModeRepositoryTest { private val testScope: TestScope = TestScope() + private val initialModes = listOf(TestModeBuilder().setId("Built-in").build()) + @Before fun setup() { MockitoAnnotations.initMocks(this) + `when`(zenModesBackend.modes).thenReturn(initialModes) + underTest = ZenModeRepositoryImpl( context, @@ -151,8 +155,8 @@ class ZenModeRepositoryTest { fun modesListEmitsOnSettingsChange() { testScope.runTest { val values = mutableListOf<List<ZenMode>>() - val modes1 = listOf(TestModeBuilder().setId("One").build()) - `when`(zenModesBackend.modes).thenReturn(modes1) + + // an initial list of modes is read when the stateflow is created underTest.modes.onEach { values.add(it) }.launchIn(backgroundScope) runCurrent() @@ -172,7 +176,7 @@ class ZenModeRepositoryTest { triggerZenModeSettingUpdate() runCurrent() - assertThat(values).containsExactly(modes1, modes2, modes3).inOrder() + assertThat(values).containsExactly(initialModes, modes2, modes3).inOrder() } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java index ed11e12c32ff..2273b4f81eea 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java @@ -54,6 +54,7 @@ import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManage import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; @@ -650,6 +651,10 @@ public class SettingsHelper { * e.g. current locale "en-US,zh-CN" and backup locale "ja-JP,zh-Hans-CN,en-US" are merged to * "en-US,zh-CN,ja-JP". * + * - Same language codes and scripts are dropped. + * e.g. current locale "en-US, zh-Hans-TW" and backup locale "en-UK, en-GB, zh-Hans-HK" are + * merged to "en-US, zh-Hans-TW". + * * - Unsupported locales are dropped. * e.g. current locale "en-US" and backup locale "ja-JP,zh-CN" but the supported locales * are "en-US,zh-CN", the merged locale list is "en-US,zh-CN". @@ -683,13 +688,23 @@ public class SettingsHelper { filtered.add(locale); } + final HashSet<String> existingLanguageAndScript = new HashSet<>(); for (int i = 0; i < restore.size(); i++) { final Locale restoredLocaleWithExtension = copyExtensionToTargetLocale(restoredLocale, getFilteredLocale(restore.get(i), allLocales)); + if (restoredLocaleWithExtension != null) { - filtered.add(restoredLocaleWithExtension); + String language = restoredLocaleWithExtension.getLanguage(); + String script = restoredLocaleWithExtension.getScript(); + + String restoredLanguageAndScript = + script == null ? language : language + "-" + script; + if (existingLanguageAndScript.add(restoredLanguageAndScript)) { + filtered.add(restoredLocaleWithExtension); + } } } + if (filtered.size() == current.size()) { return current; // Nothing added to current locale list. } diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java index 40654b0e2f37..48c778542d66 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java @@ -388,7 +388,11 @@ public class SettingsHelperTest { LocaleList.forLanguageTags("zh-Hant-TW"), // current new String[] { "fa-Arab-AF-u-nu-latn", "zh-Hant-TW" })); // supported - + assertEquals(LocaleList.forLanguageTags("en-US,zh-Hans-TW"), + SettingsHelper.resolveLocales( + LocaleList.forLanguageTags("en-UK,en-GB,zh-Hans-HK"), // restore + LocaleList.forLanguageTags("en-US,zh-Hans-TW"), // current + new String[] { "en-US,zh-Hans-TW,en-UK,en-GB,zh-Hans-HK" })); // supported } @Test diff --git a/packages/SystemUI/aconfig/desktop_users_and_accounts.aconfig b/packages/SystemUI/aconfig/desktop_users_and_accounts.aconfig new file mode 100644 index 000000000000..c7e9c9fbee2e --- /dev/null +++ b/packages/SystemUI/aconfig/desktop_users_and_accounts.aconfig @@ -0,0 +1,9 @@ +package: "com.android.systemui" +container: "system" + +flag { + name: "user_switcher_add_sign_out_option" + namespace: "desktop_users_and_accounts" + description: "Add a sign out option to the user switcher menu if sign out is possible" + bug: "381478261" +}
\ No newline at end of file diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 1c37687a6eb8..7800a5059092 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -289,6 +289,15 @@ flag { } } +flag { + name: "notification_skip_silent_updates" + namespace: "systemui" + description: "Do not notify HeadsUpManager for silent updates." + bug: "401068530" + metadata { + purpose: PURPOSE_BUGFIX + } +} flag { name: "scene_container" @@ -533,6 +542,14 @@ flag { } flag { + name: "icon_refresh_2025" + namespace: "systemui" + description: "Build time flag for 2025 icon refresh" + bug: "391605373" + is_fixed_read_only: true +} + +flag { name: "promote_notifications_automatically" namespace: "systemui" description: "Flag to automatically turn certain notifications into promoted notifications so " @@ -1856,6 +1873,14 @@ flag { } flag { + name: "shade_header_font_update" + namespace: "systemui" + description: "Updates the fonts of the shade header" + bug: "393609960" + is_fixed_read_only: true +} + +flag { name: "keyboard_shortcut_helper_shortcut_customizer" namespace: "systemui" description: "An implementation of shortcut customizations through shortcut helper." @@ -1900,13 +1925,6 @@ flag { } flag { - name: "spatial_model_launcher_pushback" - namespace: "systemui" - description: "Implement the depth push scaling effect on Launcher when users pull down shade." - bug: "370562309" -} - -flag { name: "spatial_model_app_pushback" namespace: "systemui" description: "Implement the depth push scaling effect on the current app when users pull down shade." @@ -2012,7 +2030,14 @@ flag { flag { name: "permission_helper_ui_rich_ongoing" namespace: "systemui" - description: "[RONs] Guards inline permission helper for demoting RONs" + description: "[RONs] Guards inline permission helper for demoting RONs [Guts/card version]" + bug: "379186372" +} + +flag { + name: "permission_helper_inline_ui_rich_ongoing" + namespace: "systemui" + description: "[RONs] Guards inline permission helper for demoting RONs [Inline version]" bug: "379186372" } @@ -2138,3 +2163,12 @@ flag { } } +flag { + name: "keyguard_wm_reorder_atms_calls" + namespace: "systemui" + description: "Calls ATMS#setLockScreenShown before default display callbacks in case they're slow" + bug: "399693427" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt index 5599db7689c2..1fb7901dcb59 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt @@ -751,7 +751,8 @@ constructor( OriginTransition(createLongLivedRunner(controllerFactory, scope, forLaunch = true)), "${cookie}_launchTransition", ) - transitionRegister.register(launchFilter, launchRemoteTransition, includeTakeover = true) + // TODO(b/403529740): re-enable takeovers once we solve the Compose jank issues. + transitionRegister.register(launchFilter, launchRemoteTransition, includeTakeover = false) // Cross-task close transitions should not use this animation, so we only register it for // when the opening window is Launcher. @@ -777,7 +778,8 @@ constructor( ), "${cookie}_returnTransition", ) - transitionRegister.register(returnFilter, returnRemoteTransition, includeTakeover = true) + // TODO(b/403529740): re-enable takeovers once we solve the Compose jank issues. + transitionRegister.register(returnFilter, returnRemoteTransition, includeTakeover = false) longLivedTransitions[cookie] = Pair(launchRemoteTransition, returnRemoteTransition) } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt index 1bb8ae5019fb..07a571b94ce4 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/effect/OffsetOverscrollEffect.kt @@ -16,7 +16,6 @@ package com.android.compose.gesture.effect -import androidx.annotation.VisibleForTesting import androidx.compose.animation.core.AnimationSpec import androidx.compose.foundation.OverscrollEffect import androidx.compose.foundation.OverscrollFactory @@ -94,7 +93,6 @@ class OffsetOverscrollEffect(animationScope: CoroutineScope, animationSpec: Anim companion object { private val MaxDistance = 400.dp - @VisibleForTesting fun computeOffset(density: Density, overscrollDistance: Float): Int { val maxDistancePx = with(density) { MaxDistance.toPx() } val progress = ProgressConverter.Default.convert(overscrollDistance / maxDistancePx) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt index 1f98cd8e07c0..90311ed93987 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt @@ -35,7 +35,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize @@ -83,10 +83,7 @@ import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch @Composable -fun PinInputDisplay( - viewModel: PinBouncerViewModel, - modifier: Modifier = Modifier, -) { +fun PinInputDisplay(viewModel: PinBouncerViewModel, modifier: Modifier = Modifier) { val hintedPinLength: Int? by viewModel.hintedPinLength.collectAsStateWithLifecycle() val shapeAnimations = rememberShapeAnimations(viewModel.pinShapes) @@ -173,7 +170,10 @@ private fun HintingPinInputDisplay( LaunchedEffect(Unit) { playAnimation = true } val dotColor = MaterialTheme.colorScheme.onSurfaceVariant - Row(modifier = modifier.heightIn(min = shapeAnimations.shapeSize)) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.height(shapeAnimations.shapeSize), + ) { pinEntryDrawable.forEachIndexed { index, drawable -> // Key the loop by [index] and [drawable], so that updating a shape drawable at the same // index will play the new animation (by remembering a new [atEnd]). @@ -316,17 +316,15 @@ private fun SimArea(viewModel: PinBouncerViewModel) { Box(modifier = Modifier.padding(bottom = 20.dp)) { // If isLockedEsim is null, then we do not show anything. if (isLockedEsim == true) { - PlatformOutlinedButton( - onClick = { viewModel.onDisableEsimButtonClicked() }, - ) { + PlatformOutlinedButton(onClick = { viewModel.onDisableEsimButtonClicked() }) { Row( horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Image( painter = painterResource(id = R.drawable.ic_no_sim), contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface) + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface), ) Text( text = stringResource(R.string.disable_carrier_button_text), @@ -339,15 +337,13 @@ private fun SimArea(viewModel: PinBouncerViewModel) { Image( painter = painterResource(id = R.drawable.ic_lockscreen_sim), contentDescription = null, - colorFilter = ColorFilter.tint(colorResource(id = R.color.background_protected)) + colorFilter = ColorFilter.tint(colorResource(id = R.color.background_protected)), ) } } } -private class PinInputRow( - val shapeAnimations: ShapeAnimations, -) { +private class PinInputRow(val shapeAnimations: ShapeAnimations) { private val entries = mutableStateListOf<PinInputEntry>() @Composable @@ -359,10 +355,11 @@ private class PinInputRow( contentAlignment = Alignment.Center, ) { Row( - modifier - .heightIn(min = shapeAnimations.shapeSize) - // Pins overflowing horizontally should still be shown as scrolling. - .wrapContentSize(unbounded = true) + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.height(shapeAnimations.shapeSize) + // Pins overflowing horizontally should still be shown as scrolling. + .wrapContentSize(unbounded = true), ) { entries.forEach { entry -> key(entry.digit) { entry.Content() } } } @@ -439,10 +436,7 @@ private class PinInputRow( } } -private class PinInputEntry( - val digit: Digit, - val shapeAnimations: ShapeAnimations, -) { +private class PinInputEntry(val digit: Digit, val shapeAnimations: ShapeAnimations) { private val shape = shapeAnimations.getShapeToDot(digit.sequenceNumber) // horizontal space occupied, used to shift contents as individual digits are animated in/out private val entryWidth = @@ -474,7 +468,7 @@ private class PinInputEntry( suspend fun animateRemoval() = coroutineScope { awaitAll( async { entryWidth.animateTo(0.dp, shapeAnimations.inputShiftAnimationSpec) }, - async { shapeSize.animateTo(0.dp, shapeAnimations.deleteShapeSizeAnimationSpec) } + async { shapeSize.animateTo(0.dp, shapeAnimations.deleteShapeSizeAnimationSpec) }, ) } @@ -505,7 +499,7 @@ private class PinInputEntry( layout(animatedEntryWidth.roundToPx(), shapeHeight.roundToPx()) { placeable.place( ((animatedEntryWidth - animatedShapeSize) / 2f).roundToPx(), - ((shapeHeight - animatedShapeSize) / 2f).roundToPx() + ((shapeHeight - animatedShapeSize) / 2f).roundToPx(), ) } }, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt index 0db2bb51c971..5fac6863e931 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt @@ -33,6 +33,7 @@ import com.android.compose.animation.scene.ElementKey import com.android.systemui.biometrics.AuthController import com.android.systemui.customization.R as customR import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder @@ -49,12 +50,14 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import dagger.Lazy import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope class LockSection @Inject constructor( @Application private val applicationScope: CoroutineScope, + @Main private val mainDispatcher: CoroutineDispatcher, private val windowManager: WindowManager, private val authController: AuthController, private val featureFlags: FeatureFlagsClassic, @@ -80,6 +83,7 @@ constructor( id = R.id.device_entry_icon_view DeviceEntryIconViewBinder.bind( applicationScope, + mainDispatcher, this, deviceEntryIconViewModel.get(), deviceEntryForegroundViewModel.get(), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimFlingBehavior.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimFlingBehavior.kt new file mode 100644 index 000000000000..bc38ef0abb25 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimFlingBehavior.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notifications.ui.composable + +import androidx.compose.animation.core.AnimationState +import androidx.compose.animation.core.DecayAnimationSpec +import androidx.compose.animation.core.animateDecay +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.ScrollScope +import androidx.compose.ui.MotionDurationScale +import com.android.systemui.scene.session.ui.composable.rememberSession +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.withContext +import kotlin.math.abs + +/** + * Fork of [androidx.compose.foundation.gestures.DefaultFlingBehavior] to allow us to use it with + * [rememberSession]. + */ +internal class NotificationScrimFlingBehavior( + private var flingDecay: DecayAnimationSpec<Float>, + private val motionDurationScale: MotionDurationScale = NotificationScrimMotionDurationScale +) : FlingBehavior { + override suspend fun ScrollScope.performFling(initialVelocity: Float): Float { + // come up with the better threshold, but we need it since spline curve gives us NaNs + return withContext(motionDurationScale) { + if (abs(initialVelocity) > 1f) { + var velocityLeft = initialVelocity + var lastValue = 0f + val animationState = + AnimationState( + initialValue = 0f, + initialVelocity = initialVelocity, + ) + try { + animationState.animateDecay(flingDecay) { + val delta = value - lastValue + val consumed = scrollBy(delta) + lastValue = value + velocityLeft = this.velocity + // avoid rounding errors and stop if anything is unconsumed + if (abs(delta - consumed) > 0.5f) this.cancelAnimation() + } + } catch (exception: CancellationException) { + velocityLeft = animationState.velocity + } + velocityLeft + } else { + initialVelocity + } + } + } +} + +internal val NotificationScrimMotionDurationScale = + object : MotionDurationScale { + override val scaleFactor: Float + get() = 1f + }
\ No newline at end of file diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 09b8d178cc8e..79b346439d5d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -20,12 +20,13 @@ package com.android.systemui.notifications.ui.composable import android.util.Log import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.DecayAnimationSpec import androidx.compose.animation.core.tween +import androidx.compose.animation.splineBasedDecay import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.gestures.rememberScrollableState import androidx.compose.foundation.gestures.scrollBy @@ -59,7 +60,6 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment @@ -97,6 +97,7 @@ import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadi import com.android.systemui.res.R import com.android.systemui.scene.session.ui.composable.SaveableSession import com.android.systemui.scene.session.ui.composable.rememberSession +import com.android.systemui.scene.session.ui.composable.sessionCoroutineScope import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.ui.composable.ShadeHeader import com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent @@ -298,7 +299,7 @@ fun ContentScope.NotificationScrollingStack( onEmptySpaceClick: (() -> Unit)? = null, modifier: Modifier = Modifier, ) { - val coroutineScope = rememberCoroutineScope() + val coroutineScope = shadeSession.sessionCoroutineScope() val density = LocalDensity.current val screenCornerRadius = LocalScreenCornerRadius.current val scrimCornerRadius = dimensionResource(R.dimen.notification_scrim_corner_radius) @@ -308,8 +309,6 @@ fun ContentScope.NotificationScrollingStack( ScrollState(initial = 0) } val syntheticScroll = viewModel.syntheticScroll.collectAsStateWithLifecycle(0f) - val isCurrentGestureOverscroll = - viewModel.isCurrentGestureOverscroll.collectAsStateWithLifecycle(false) val expansionFraction by viewModel.expandFraction.collectAsStateWithLifecycle(0f) val shadeToQsFraction by viewModel.shadeToQsFraction.collectAsStateWithLifecycle(0f) @@ -454,15 +453,15 @@ fun ContentScope.NotificationScrollingStack( } } - val flingBehavior = ScrollableDefaults.flingBehavior() val scrimNestedScrollConnection = shadeSession.rememberSession( scrimOffset, - maxScrimTop, minScrimTop, - isCurrentGestureOverscroll, - flingBehavior, + viewModel.isCurrentGestureOverscroll, + density, ) { + val flingSpec: DecayAnimationSpec<Float> = splineBasedDecay(density) + val flingBehavior = NotificationScrimFlingBehavior(flingSpec) NotificationScrimNestedScrollConnection( scrimOffset = { scrimOffset.value }, snapScrimOffset = { value -> coroutineScope.launch { scrimOffset.snapTo(value) } }, @@ -473,7 +472,7 @@ fun ContentScope.NotificationScrollingStack( maxScrimOffset = 0f, contentHeight = { stackHeight.intValue.toFloat() }, minVisibleScrimHeight = minVisibleScrimHeight, - isCurrentGestureOverscroll = { isCurrentGestureOverscroll.value }, + isCurrentGestureOverscroll = { viewModel.isCurrentGestureOverscroll }, flingBehavior = flingBehavior, ) } @@ -574,11 +573,11 @@ fun ContentScope.NotificationScrollingStack( ) { Column( modifier = - Modifier.thenIf(supportNestedScrolling) { + Modifier.disableSwipesWhenScrolling(NestedScrollableBound.BottomRight) + .thenIf(supportNestedScrolling) { Modifier.nestedScroll(scrimNestedScrollConnection) } .stackVerticalOverscroll(coroutineScope) { scrollState.canScrollForward } - .disableSwipesWhenScrolling(NestedScrollableBound.BottomRight) .verticalScroll(scrollState) .padding(top = stackTopPadding, bottom = stackBottomPadding) .fillMaxWidth() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/ui/composable/Session.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/ui/composable/Session.kt index 4eaacf31e23d..1750b11a998b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/ui/composable/Session.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/ui/composable/Session.kt @@ -17,10 +17,14 @@ package com.android.systemui.scene.session.ui.composable import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffectResult +import androidx.compose.runtime.DisposableEffectScope +import androidx.compose.runtime.RememberObserver import androidx.compose.runtime.SideEffect import androidx.compose.runtime.currentCompositeKeyHash import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCompositionContext import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.SaverScope import androidx.compose.runtime.saveable.mapSaver @@ -28,6 +32,10 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import com.android.systemui.scene.session.shared.SessionStorage import com.android.systemui.util.kotlin.mapValuesNotNullTo +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job /** * An explicit storage for remembering composable state outside of the lifetime of a composition. @@ -89,6 +97,55 @@ fun <T> Session.rememberSession(vararg inputs: Any?, key: String? = null, init: rememberSession(key, *inputs, init = init) /** + * A side effect of composition that must be reversed or cleaned up if the [Session] ends. + * + * @see androidx.compose.runtime.DisposableEffect + */ +@Composable +fun Session.SessionDisposableEffect( + vararg inputs: Any?, + key: String? = null, + effect: DisposableEffectScope.() -> DisposableEffectResult, +) { + rememberSession(inputs, key) { + object : RememberObserver { + + var onDispose: DisposableEffectResult? = null + + override fun onAbandoned() { + // no-op + } + + override fun onForgotten() { + onDispose?.dispose() + onDispose = null + } + + override fun onRemembered() { + onDispose = DisposableEffectScope().effect() + } + } + } +} + +/** + * Return a [CoroutineScope] bound to this [Session] using the optional [CoroutineContext] provided + * by [getContext]. [getContext] will only be called once and the same [CoroutineScope] instance + * will be returned for the duration of the [Session]. + * + * @see androidx.compose.runtime.rememberCoroutineScope + */ +@Composable +fun Session.sessionCoroutineScope( + getContext: () -> CoroutineContext = { EmptyCoroutineContext } +): CoroutineScope { + val effectContext: CoroutineContext = rememberCompositionContext().effectCoroutineContext + val job = rememberSession { Job() } + SessionDisposableEffect { onDispose { job.cancel() } } + return rememberSession { CoroutineScope(effectContext + job + getContext()) } +} + +/** * An explicit storage for remembering composable state outside of the lifetime of a composition. * * Specifically, this allows easy conversion of standard [rememberSession] invocations to ones that @@ -147,15 +204,10 @@ interface SaveableSession : Session { * location in the composition tree. */ @Composable -fun rememberSaveableSession( - vararg inputs: Any?, - key: String? = null, -): SaveableSession = +fun rememberSaveableSession(vararg inputs: Any?, key: String? = null): SaveableSession = rememberSaveable(*inputs, SaveableSessionImpl.SessionSaver, key) { SaveableSessionImpl() } -private class SessionImpl( - private val storage: SessionStorage = SessionStorage(), -) : Session { +private class SessionImpl(private val storage: SessionStorage = SessionStorage()) : Session { @Composable override fun <T> rememberSession(key: String?, vararg inputs: Any?, init: () -> T): T { val storage = storage.storage @@ -169,16 +221,31 @@ private class SessionImpl( } if (finalKey !in storage) { val value = init() - SideEffect { storage[finalKey] = SessionStorage.StorageEntry(inputs, value) } + SideEffect { + storage[finalKey] = SessionStorage.StorageEntry(inputs, value) + if (value is RememberObserver) { + value.onRemembered() + } + } return value } val entry = storage[finalKey]!! if (!inputs.contentEquals(entry.keys)) { val value = init() - SideEffect { entry.stored = value } + SideEffect { + val oldValue = entry.stored + if (oldValue is RememberObserver) { + oldValue.onForgotten() + } + entry.stored = value + if (value is RememberObserver) { + value.onRemembered() + } + } return value } - @Suppress("UNCHECKED_CAST") return entry.stored as T + @Suppress("UNCHECKED_CAST") + return entry.stored as T } } @@ -228,7 +295,8 @@ private class SaveableSessionImpl( } return value } - @Suppress("UNCHECKED_CAST") return entry.stored as T + @Suppress("UNCHECKED_CAST") + return entry.stored as T } } } @@ -263,7 +331,7 @@ private class SaveableSessionImpl( v?.let { StorageEntry.Unrestored(v) } } ) - } + }, ) } 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 86c8fc34a63c..03debb6fa7ca 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 @@ -43,6 +43,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -67,12 +68,15 @@ import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.modifiers.thenIf import com.android.compose.theme.colorAttr import com.android.settingslib.Utils +import com.android.systemui.Flags import com.android.systemui.battery.BatteryMeterView import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius import com.android.systemui.compose.modifiers.sysuiResTag +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.buildSpec import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes @@ -86,8 +90,12 @@ import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHi import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.phone.StatusIconContainer import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModelKairosComposeWrapper import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModelKairos +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.composeWrapper import com.android.systemui.statusbar.policy.Clock +import com.android.systemui.util.composable.kairos.ActivatedKairosSpec object ShadeHeader { object Elements { @@ -285,6 +293,25 @@ fun ContentScope.OverlayShadeHeader( viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier, ) { + OverlayShadeHeaderPartialStateless( + viewModel, + viewModel.showClock, + modifier, + ) +} + +/** + * Ideally, we should have a stateless function for overlay shade header, which facilitates testing. + * However, it is cumbersome to implement such a stateless function, especially when some of the + * overlay shade header's children accept a view model as the param. Therefore, this function only + * break up the clock visibility. It is where "PartialStateless" comes from. + */ +@Composable +fun ContentScope.OverlayShadeHeaderPartialStateless( + viewModel: ShadeHeaderViewModel, + showClock: Boolean, + modifier: Modifier = Modifier, +) { val horizontalPadding = max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding) @@ -300,7 +327,7 @@ fun ContentScope.OverlayShadeHeader( modifier = Modifier.padding(horizontal = horizontalPadding), ) { val chipHighlight = viewModel.notificationsChipHighlight - if (viewModel.showClock) { + if (showClock) { Clock( onClick = viewModel::onClockClicked, modifier = Modifier.padding(horizontal = 4.dp), @@ -520,8 +547,14 @@ private fun BatteryIcon( ) } +@OptIn(ExperimentalKairosApi::class) @Composable private fun ShadeCarrierGroup(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier) { + if (Flags.statusBarMobileIconKairos()) { + ShadeCarrierGroupKairos(viewModel, modifier) + return + } + Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(5.dp)) { for (subId in viewModel.mobileSubIds) { AndroidView( @@ -543,6 +576,49 @@ private fun ShadeCarrierGroup(viewModel: ShadeHeaderViewModel, modifier: Modifie } } +@ExperimentalKairosApi +@Composable +private fun ShadeCarrierGroupKairos( + viewModel: ShadeHeaderViewModel, + modifier: Modifier = Modifier, +) { + Row(modifier = modifier) { + ActivatedKairosSpec( + buildSpec = viewModel.mobileIconsViewModelKairos.get().composeWrapper(), + kairosNetwork = viewModel.kairosNetwork, + ) { iconsViewModel: MobileIconsViewModelKairosComposeWrapper -> + for ((subId, icon) in iconsViewModel.icons) { + Spacer(modifier = Modifier.width(5.dp)) + val scope = rememberCoroutineScope() + AndroidView( + factory = { context -> + ModernShadeCarrierGroupMobileView.constructAndBind( + context = context, + logger = iconsViewModel.logger, + slot = "mobile_carrier_shade_group", + viewModel = + buildSpec { + ShadeCarrierGroupMobileIconViewModelKairos( + icon, + icon.iconInteractor, + ) + }, + scope = scope, + subscriptionId = subId, + location = StatusBarLocation.SHADE_CARRIER_GROUP, + kairosNetwork = viewModel.kairosNetwork, + ) + .first + .also { + it.setOnClickListener { viewModel.onShadeCarrierGroupClicked() } + } + } + ) + } + } + } +} + @Composable private fun ContentScope.StatusIcons( viewModel: ShadeHeaderViewModel, diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt index 96c3ac75587e..8744357a74c9 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt @@ -144,23 +144,25 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController listOf( GSFAxes.WEIGHT.toClockAxis( type = AxisType.Float, - currentValue = 475f, + currentValue = 400f, name = "Weight", description = "Glyph Weight", ), GSFAxes.WIDTH.toClockAxis( type = AxisType.Float, - currentValue = 85f, + currentValue = 80f, name = "Width", description = "Glyph Width", ), GSFAxes.ROUND.toClockAxis( type = AxisType.Boolean, + currentValue = 100f, name = "Round", description = "Glyph Roundness", ), GSFAxes.SLANT.toClockAxis( type = AxisType.Boolean, + currentValue = 0f, name = "Slant", description = "Glyph Slant", ), diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java index 2845f6a2983a..e75f60736435 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java @@ -507,8 +507,8 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { 0 /* flags */); users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */, false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */, null /* enforcedAdmin */, - false /* isManageUsers */)); + false /* isAddSupervisedUser */, false /* isSignOut */, + null /* enforcedAdmin */, false /* isManageUsers */)); } return users; } 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 d118ace08b85..530aba4993a8 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 @@ -176,6 +176,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { .thenReturn(SETTINGS_PACKAGE_NAME); when(mDevice.getBondState()).thenReturn(BOND_BONDED); when(mDevice.isConnected()).thenReturn(true); + when(mDevice.getAddress()).thenReturn(DEVICE_ADDRESS); + when(mDevice.getAnonymizedAddress()).thenReturn(DEVICE_ADDRESS); when(mCachedDevice.getDevice()).thenReturn(mDevice); when(mCachedDevice.getAddress()).thenReturn(DEVICE_ADDRESS); when(mCachedDevice.getName()).thenReturn(DEVICE_NAME); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt index c1feca29906a..91ec1cbce8a4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt @@ -77,6 +77,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { private lateinit var resources: TestableResources private lateinit var trustRepository: FakeTrustRepository private lateinit var testScope: TestScope + private val TEST_REASON = "reason" @Before fun setUp() { @@ -118,7 +119,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { mainHandler.setMode(FakeHandler.Mode.QUEUEING) // WHEN bouncer show is requested - underTest.show(true) + underTest.show(true, TEST_REASON) // WHEN all queued messages are dispatched mainHandler.dispatchQueuedMessages() @@ -134,7 +135,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { @Test fun testShow_isScrimmed() { - underTest.show(true) + underTest.show(true, TEST_REASON) verify(repository).setKeyguardAuthenticatedBiometrics(null) verify(repository).setPrimaryStartingToHide(false) verify(repository).setPrimaryScrimmed(true) @@ -162,7 +163,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { @Test fun testShowReturnsFalseWhenDelegateIsNotSet() { whenever(bouncerView.delegate).thenReturn(null) - assertThat(underTest.show(true)).isEqualTo(false) + assertThat(underTest.show(true, TEST_REASON)).isEqualTo(false) } @Test @@ -171,7 +172,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { whenever(keyguardSecurityModel.getSecurityMode(anyInt())) .thenReturn(KeyguardSecurityModel.SecurityMode.SimPuk) - underTest.show(true) + underTest.show(true, TEST_REASON) verify(repository).setPrimaryShow(false) verify(repository).setPrimaryShow(true) } @@ -352,7 +353,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { whenever(faceAuthInteractor.canFaceAuthRun()).thenReturn(true) // WHEN bouncer show is requested - underTest.show(true) + underTest.show(true, TEST_REASON) // THEN primary show & primary showing soon aren't updated immediately verify(repository, never()).setPrimaryShow(true) @@ -375,7 +376,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { whenever(faceAuthInteractor.canFaceAuthRun()).thenReturn(false) // WHEN bouncer show is requested - underTest.show(true) + underTest.show(true, TEST_REASON) // THEN primary show & primary showing soon are updated immediately verify(repository).setPrimaryShow(true) @@ -394,7 +395,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { runCurrent() // WHEN bouncer show is requested - underTest.show(true) + underTest.show(true, TEST_REASON) // THEN primary show & primary showing soon were scheduled to update verify(repository, never()).setPrimaryShow(true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt index 0718d0d32812..83fd4c258082 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt @@ -264,6 +264,29 @@ class KeyguardInteractorTest : SysuiTestCase() { } @Test + fun dismissAlpha_doesNotEmitWhenNotDismissible() = + testScope.runTest { + val dismissAlpha by collectValues(underTest.dismissAlpha) + assertThat(dismissAlpha[0]).isEqualTo(1f) + assertThat(dismissAlpha.size).isEqualTo(1) + + keyguardTransitionRepository.sendTransitionSteps(from = AOD, to = LOCKSCREEN, testScope) + + // User begins to swipe up when not dimissible, which would show bouncer + repository.setStatusBarState(StatusBarState.KEYGUARD) + repository.setKeyguardDismissible(false) + shadeRepository.setLegacyShadeExpansion(0.98f) + + assertThat(dismissAlpha[0]).isEqualTo(1f) + assertThat(dismissAlpha.size).isEqualTo(1) + + // Shade reset should not affect dismiss alpha when not dismissible + shadeRepository.setLegacyShadeExpansion(0f) + assertThat(dismissAlpha[0]).isEqualTo(1f) + assertThat(dismissAlpha.size).isEqualTo(1) + } + + @Test fun dismissAlpha_onGlanceableHub_doesNotEmitWhenShadeResets() = testScope.runTest { val dismissAlpha by collectValues(underTest.dismissAlpha) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt index 6704d63395ad..582666561be2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt @@ -267,12 +267,20 @@ class KeyguardKeyEventInteractorTest : SysuiTestCase() { // action down: does NOT collapse the shade val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode) assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse() - verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any()) + verify(statusBarKeyguardViewManager, never()) + .showPrimaryBouncer( + any(), + eq("KeyguardKeyEventInteractor#collapseShadeLockedOrShowPrimaryBouncer"), + ) // action up: collapses the shade val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode) assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue() - verify(statusBarKeyguardViewManager).showPrimaryBouncer(eq(true)) + verify(statusBarKeyguardViewManager) + .showPrimaryBouncer( + eq(true), + eq("KeyguardKeyEventInteractor#collapseShadeLockedOrShowPrimaryBouncer"), + ) } private fun verifyActionsDoNothing(keycode: Int) { @@ -280,12 +288,20 @@ class KeyguardKeyEventInteractorTest : SysuiTestCase() { val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode) assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse() verify(shadeController, never()).animateCollapseShadeForced() - verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any()) + verify(statusBarKeyguardViewManager, never()) + .showPrimaryBouncer( + any(), + eq("KeyguardKeyEventInteractor#collapseShadeLockedOrShowPrimaryBouncer"), + ) // action up: doesNothing val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode) assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isFalse() verify(shadeController, never()).animateCollapseShadeForced() - verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any()) + verify(statusBarKeyguardViewManager, never()) + .showPrimaryBouncer( + any(), + eq("KeyguardKeyEventInteractor#collapseShadeLockedOrShowPrimaryBouncer"), + ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt index f0eedee48e57..4f351143c793 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt @@ -340,6 +340,22 @@ class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() { @Test @EnableSceneContainer + fun surfaceBehindVisibility_whileSceneContainerNotVisible_alwaysTrue() = + testScope.runTest { + val isSurfaceBehindVisible by collectLastValue(underTest.value.surfaceBehindVisibility) + val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene) + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + assertThat(isSurfaceBehindVisible).isFalse() + + kosmos.sceneInteractor.setVisible(false, "test") + runCurrent() + + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + assertThat(isSurfaceBehindVisible).isTrue() + } + + @Test + @EnableSceneContainer fun surfaceBehindVisibility_idleWhileLocked_alwaysFalse() = testScope.runTest { val isSurfaceBehindVisible by collectLastValue(underTest.value.surfaceBehindVisibility) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt index e1323c166f6b..9aee4c97f214 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt @@ -35,6 +35,7 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest @@ -56,7 +57,8 @@ class AlternateBouncerViewModelTest : SysuiTestCase() { fun onTapped() = testScope.runTest { underTest.onTapped() - verify(statusBarKeyguardViewManager).showPrimaryBouncer(any()) + verify(statusBarKeyguardViewManager) + .showPrimaryBouncer(any(), eq("AlternateBouncerViewModel#onTapped")) } @Test @@ -154,7 +156,7 @@ class AlternateBouncerViewModelTest : SysuiTestCase() { private fun stepToAlternateBouncer( value: Float, - state: TransitionState = TransitionState.RUNNING + state: TransitionState = TransitionState.RUNNING, ): TransitionStep { return step( from = KeyguardState.LOCKSCREEN, @@ -166,7 +168,7 @@ class AlternateBouncerViewModelTest : SysuiTestCase() { private fun stepFromAlternateBouncer( value: Float, - state: TransitionState = TransitionState.RUNNING + state: TransitionState = TransitionState.RUNNING, ): TransitionStep { return step( from = KeyguardState.ALTERNATE_BOUNCER, @@ -180,14 +182,14 @@ class AlternateBouncerViewModelTest : SysuiTestCase() { from: KeyguardState, to: KeyguardState, value: Float, - transitionState: TransitionState + transitionState: TransitionState, ): TransitionStep { return TransitionStep( from = from, to = to, value = value, transitionState = transitionState, - ownerName = "AlternateBouncerViewModelTest" + ownerName = "AlternateBouncerViewModelTest", ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt index d1b552906fbb..5d4de02f9aaa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateExtTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.model -import android.view.Display import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -35,7 +34,7 @@ class SysUiStateExtTest : SysuiTestCase() { @Test fun updateFlags() { - underTest.updateFlags(Display.DEFAULT_DISPLAY, 1L to true, 2L to false, 4L to true) + underTest.updateFlags(1L to true, 2L to false, 4L to true) assertThat(underTest.flags and 1L).isNotEqualTo(0L) assertThat(underTest.flags and 2L).isEqualTo(0L) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt index 8b9ae9a0606d..2dd2f7c0f562 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt @@ -23,6 +23,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.shared.model.SizedTileImpl +import com.android.systemui.qs.panels.ui.compose.selection.PlacementEvent import com.android.systemui.qs.panels.ui.model.GridCell import com.android.systemui.qs.panels.ui.model.TileGridCell import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel @@ -108,6 +109,76 @@ class EditTileListStateTest : SysuiTestCase() { assertThat(underTest.tiles.toStrings()).doesNotContain(TestEditTiles[0].tile.tileSpec.spec) } + @Test + fun targetIndexForPlacementToTileSpec_returnsCorrectIndex() { + val placementEvent = + PlacementEvent.PlaceToTileSpec( + movingSpec = TestEditTiles[0].tile.tileSpec, + targetSpec = TestEditTiles[3].tile.tileSpec, + ) + val index = underTest.targetIndexForPlacement(placementEvent) + + assertThat(index).isEqualTo(3) + } + + @Test + fun targetIndexForPlacementToIndex_indexOutOfBounds_returnsCorrectIndex() { + val placementEventTooLow = + PlacementEvent.PlaceToIndex( + movingSpec = TestEditTiles[0].tile.tileSpec, + targetIndex = -1, + ) + val index1 = underTest.targetIndexForPlacement(placementEventTooLow) + + assertThat(index1).isEqualTo(0) + + val placementEventTooHigh = + PlacementEvent.PlaceToIndex( + movingSpec = TestEditTiles[0].tile.tileSpec, + targetIndex = 10, + ) + val index2 = underTest.targetIndexForPlacement(placementEventTooHigh) + assertThat(index2).isEqualTo(TestEditTiles.size) + } + + @Test + fun targetIndexForPlacementToIndex_movingBack_returnsCorrectIndex() { + /** + * With the grid: [ a ] [ b ] [ c ] [ Large D ] [ e ] [ f ] + * + * Moving 'e' to the spacer at index 3 will result in the tilespec order: a, b, c, e, d, f + * + * 'e' is now at index 3 + */ + val placementEvent = + PlacementEvent.PlaceToIndex( + movingSpec = TestEditTiles[4].tile.tileSpec, + targetIndex = 3, + ) + val index = underTest.targetIndexForPlacement(placementEvent) + + assertThat(index).isEqualTo(3) + } + + @Test + fun targetIndexForPlacementToIndex_movingForward_returnsCorrectIndex() { + /** + * With the grid: [ a ] [ b ] [ c ] [ Large D ] [ e ] [ f ] + * + * Moving '1' to the spacer at index 3 will result in the tilespec order: b, c, a, d, e, f + * + * 'a' is now at index 2 + */ + val placementEvent = + PlacementEvent.PlaceToIndex( + movingSpec = TestEditTiles[0].tile.tileSpec, + targetIndex = 3, + ) + val index = underTest.targetIndexForPlacement(placementEvent) + + assertThat(index).isEqualTo(2) + } + private fun List<GridCell>.toStrings(): List<String> { return map { if (it is TileGridCell) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt index ab217a3f50ef..33ee3379c0d6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt @@ -19,8 +19,14 @@ package com.android.systemui.qs.panels.ui.compose.selection import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.qs.panels.ui.compose.selection.TileState.GreyedOut +import com.android.systemui.qs.panels.ui.compose.selection.TileState.None +import com.android.systemui.qs.panels.ui.compose.selection.TileState.Placeable +import com.android.systemui.qs.panels.ui.compose.selection.TileState.Removable +import com.android.systemui.qs.panels.ui.compose.selection.TileState.Selected import com.android.systemui.qs.pipeline.shared.TileSpec import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -45,7 +51,104 @@ class MutableSelectionStateTest : SysuiTestCase() { assertThat(underTest.selection).isEqualTo(newSpec) } + @Test + fun placementModeEnabled_tapOnIndex_sendsCorrectPlacementEvent() { + // Tap while in placement mode + underTest.enterPlacementMode(TEST_SPEC) + underTest.onTap(2) + + assertThat(underTest.placementEnabled).isFalse() + val event = underTest.placementEvent as PlacementEvent.PlaceToIndex + assertThat(event.movingSpec).isEqualTo(TEST_SPEC) + assertThat(event.targetIndex).isEqualTo(2) + } + + @Test + fun placementModeDisabled_tapOnIndex_doesNotSendPlacementEvent() { + // Tap while placement mode is disabled + underTest.onTap(2) + + assertThat(underTest.placementEnabled).isFalse() + assertThat(underTest.placementEvent).isNull() + } + + @Test + fun placementModeEnabled_tapOnSelection_exitPlacementMode() { + // Tap while in placement mode + underTest.enterPlacementMode(TEST_SPEC) + underTest.onTap(TEST_SPEC) + + assertThat(underTest.placementEnabled).isFalse() + assertThat(underTest.placementEvent).isNull() + } + + @Test + fun placementModeEnabled_tapOnTileSpec_sendsCorrectPlacementEvent() { + // Tap while in placement mode + underTest.enterPlacementMode(TEST_SPEC) + underTest.onTap(TEST_SPEC_2) + + assertThat(underTest.placementEnabled).isFalse() + val event = underTest.placementEvent as PlacementEvent.PlaceToTileSpec + assertThat(event.movingSpec).isEqualTo(TEST_SPEC) + assertThat(event.targetSpec).isEqualTo(TEST_SPEC_2) + } + + @Test + fun placementModeDisabled_tapOnSelection_unselect() { + // Select the tile and tap on it + underTest.select(TEST_SPEC) + underTest.onTap(TEST_SPEC) + + assertThat(underTest.placementEnabled).isFalse() + assertThat(underTest.selected).isFalse() + } + + @Test + fun placementModeDisabled_tapOnTile_selects() { + // Select a tile but tap a second one + underTest.select(TEST_SPEC) + underTest.onTap(TEST_SPEC_2) + + assertThat(underTest.placementEnabled).isFalse() + assertThat(underTest.selection).isEqualTo(TEST_SPEC_2) + } + + @Test + fun tileStateFor_selectedTile_returnsSingleSelection() = runTest { + underTest.select(TEST_SPEC) + + assertThat(underTest.tileStateFor(TEST_SPEC, None, canShowRemovalBadge = true)) + .isEqualTo(Selected) + assertThat(underTest.tileStateFor(TEST_SPEC_2, None, canShowRemovalBadge = true)) + .isEqualTo(Removable) + assertThat(underTest.tileStateFor(TEST_SPEC_3, None, canShowRemovalBadge = true)) + .isEqualTo(Removable) + } + + @Test + fun tileStateFor_placementMode_returnsSinglePlaceable() = runTest { + underTest.enterPlacementMode(TEST_SPEC) + + assertThat(underTest.tileStateFor(TEST_SPEC, None, canShowRemovalBadge = true)) + .isEqualTo(Placeable) + assertThat(underTest.tileStateFor(TEST_SPEC_2, None, canShowRemovalBadge = true)) + .isEqualTo(GreyedOut) + assertThat(underTest.tileStateFor(TEST_SPEC_3, None, canShowRemovalBadge = true)) + .isEqualTo(GreyedOut) + } + + @Test + fun tileStateFor_nonRemovableTile_returnsNoneState() = runTest { + assertThat(underTest.tileStateFor(TEST_SPEC, None, canShowRemovalBadge = true)) + .isEqualTo(Removable) + assertThat(underTest.tileStateFor(TEST_SPEC_2, None, canShowRemovalBadge = false)) + .isEqualTo(None) + } + companion object { private val TEST_SPEC = TileSpec.create("testSpec") + private val TEST_SPEC_2 = TileSpec.create("testSpec2") + private val TEST_SPEC_3 = TileSpec.create("testSpec3") } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java index 765c5749cd4b..d880aa604849 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java @@ -50,6 +50,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.tiles.dialog.CastDetailsViewModel; import com.android.systemui.shade.domain.interactor.FakeShadeDialogContextInteractor; import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor; import com.android.systemui.statusbar.connectivity.IconState; @@ -63,6 +64,7 @@ import com.android.systemui.statusbar.policy.HotspotController; import com.android.systemui.statusbar.policy.KeyguardStateController; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -104,6 +106,8 @@ public class CastTileTest extends SysuiTestCase { private DialogTransitionAnimator mDialogTransitionAnimator; @Mock private QsEventLogger mUiEventLogger; + @Mock + private CastDetailsViewModel.Factory mCastDetailsViewModelFactory; private final TileJavaAdapter mJavaAdapter = new TileJavaAdapter(); private final FakeConnectivityRepository mConnectivityRepository = @@ -517,6 +521,29 @@ public class CastTileTest extends SysuiTestCase { assertTrue(mCastTile.getState().forceExpandIcon); } + @Test + public void testDetailsViewUnavailableState_returnsNull() { + createAndStartTileNewImpl(); + mTestableLooper.processAllMessages(); + + assertEquals(Tile.STATE_UNAVAILABLE, mCastTile.getState().state); + mCastTile.getDetailsViewModel(Assert::assertNull); + } + + @Test + public void testDetailsViewAvailableState_returnsNotNull() { + createAndStartTileNewImpl(); + CastDevice device = createConnectedCastDevice(); + List<CastDevice> devices = new ArrayList<>(); + devices.add(device); + when(mController.getCastDevices()).thenReturn(devices); + mConnectivityRepository.setWifiConnected(true); + mTestableLooper.processAllMessages(); + + assertEquals(Tile.STATE_ACTIVE, mCastTile.getState().state); + mCastTile.getDetailsViewModel(Assert::assertNotNull); + } + /** * For simplicity, let this method still set the field even though that's kind of gross */ @@ -540,7 +567,8 @@ public class CastTileTest extends SysuiTestCase { mConnectivityRepository, mJavaAdapter, mFeatureFlags, - mShadeDialogContextInteractor + mShadeDialogContextInteractor, + mCastDetailsViewModelFactory ); mCastTile.initialize(); @@ -584,7 +612,8 @@ public class CastTileTest extends SysuiTestCase { mConnectivityRepository, mJavaAdapter, mFeatureFlags, - mShadeDialogContextInteractor + mShadeDialogContextInteractor, + mCastDetailsViewModelFactory ); mCastTile.initialize(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModelTest.kt new file mode 100644 index 000000000000..468c3dc3be93 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModelTest.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.qs.tiles.dialog + +import android.content.Context +import android.media.MediaRouter +import android.provider.Settings +import android.testing.TestableLooper.RunWithLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.app.MediaRouteDialogPresenter +import com.android.systemui.SysuiTestCase +import com.android.systemui.qs.tiles.base.domain.actions.FakeQSTileIntentUserInputHandler +import com.android.systemui.qs.tiles.base.domain.actions.intentInputs +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub + +@SmallTest +@RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidJUnit4::class) +class CastDetailsViewModelTest : SysuiTestCase() { + var inputHandler: FakeQSTileIntentUserInputHandler = FakeQSTileIntentUserInputHandler() + private var context: Context = mock() + private var mediaRouter: MediaRouter = mock() + private var selectedRoute: MediaRouter.RouteInfo = mock() + + @Test + fun testClickOnSettingsButton() { + var viewModel = CastDetailsViewModel(inputHandler, context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) + + viewModel.clickOnSettingsButton() + + assertThat(inputHandler.handledInputs).hasSize(1) + val intentInput = inputHandler.intentInputs.last() + assertThat(intentInput.expandable).isNull() + assertThat(intentInput.intent.action).isEqualTo(Settings.ACTION_CAST_SETTINGS) + } + + @Test + fun testShouldShowChooserDialog() { + context.stub { + on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter + } + mediaRouter.stub { + on { selectedRoute } doReturn selectedRoute + } + + var viewModel = + CastDetailsViewModel(inputHandler, context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) + + assertThat(viewModel.shouldShowChooserDialog()) + .isEqualTo( + MediaRouteDialogPresenter.shouldShowChooserDialog( + context, + MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, + ) + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 1743e056b65c..6d4fffdefb1b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -22,7 +22,6 @@ import android.os.PowerManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.provider.Settings -import android.view.Display import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState @@ -1213,15 +1212,15 @@ class SceneContainerStartableTest : SysuiTestCase() { fakeSceneDataSource.pause() sceneInteractor.changeScene(sceneKey, "reason") runCurrent() - verify(sysUiState, times(index)).commitUpdate(Display.DEFAULT_DISPLAY) + verify(sysUiState, times(index)).commitUpdate() fakeSceneDataSource.unpause(expectedScene = sceneKey) runCurrent() - verify(sysUiState, times(index)).commitUpdate(Display.DEFAULT_DISPLAY) + verify(sysUiState, times(index)).commitUpdate() transitionStateFlow.value = ObservableTransitionState.Idle(sceneKey) runCurrent() - verify(sysUiState, times(index + 1)).commitUpdate(Display.DEFAULT_DISPLAY) + verify(sysUiState, times(index + 1)).commitUpdate() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt index 32eec56af8ea..b376558371f3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt @@ -21,6 +21,7 @@ import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags @@ -30,8 +31,10 @@ import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticati import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.enableDualShade import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.CommandQueue import com.android.systemui.testKosmos @@ -177,6 +180,40 @@ class ShadeControllerSceneImplTest : SysuiTestCase() { verify(testRunnable, times(1)).run() } + @Test + fun instantCollapseShade_notificationShadeHidden() = + testScope.runTest { + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + // GIVEN the dual shade configuration with the notification shade overlay visible + kosmos.enableDualShade() + runCurrent() + sceneInteractor.showOverlay(Overlays.NotificationsShade, "test") + assertThat(currentOverlays).isEqualTo(setOf(Overlays.NotificationsShade)) + + // WHEN shade instantly collapses + underTest.instantCollapseShade() + + // THEN overlay was hidden + assertThat(currentOverlays).isEmpty() + } + + @Test + fun instantCollapseShade_qsShadeHidden() = + testScope.runTest { + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + // GIVEN the dual shade configuration with the QS shade overlay visible + kosmos.enableDualShade() + runCurrent() + sceneInteractor.showOverlay(Overlays.QuickSettingsShade, "test") + assertThat(currentOverlays).isEqualTo(setOf(Overlays.QuickSettingsShade)) + + // WHEN shade instantly collapses + underTest.instantCollapseShade() + + // THEN overlay was hidden + assertThat(currentOverlays).isEmpty() + } + private fun setScene(key: SceneKey) { sceneInteractor.changeScene(key, "test") sceneInteractor.setTransitionState( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java index cd55bb23c1fc..6f1150e5fd96 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java @@ -46,6 +46,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.keyguard.CarrierTextManager; +import com.android.systemui.kairos.KairosNetwork; import com.android.systemui.log.core.FakeLogBuffer; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.connectivity.IconState; @@ -55,6 +56,7 @@ import com.android.systemui.statusbar.connectivity.SignalCallback; import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider; import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos; import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger; import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel; @@ -63,6 +65,7 @@ import com.android.systemui.util.kotlin.FlowProviderKt; import com.android.systemui.utils.leaks.LeakCheckedTest; import com.android.systemui.utils.os.FakeHandler; +import kotlinx.coroutines.CoroutineScope; import kotlinx.coroutines.flow.MutableStateFlow; import org.junit.Before; @@ -178,8 +181,10 @@ public class ShadeCarrierGroupControllerTest extends LeakCheckedTest { mSlotIndexResolver, mMobileUiAdapter, mMobileContextProvider, - mStatusBarPipelineFlags - ) + mStatusBarPipelineFlags, + mock(CoroutineScope.class), + mock(KairosNetwork.class), + () -> mock(MobileUiAdapterKairos.class)) .setShadeCarrierGroup(mShadeCarrierGroup) .build(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java index 70df82d95008..c26f18f5ab6d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java @@ -36,7 +36,9 @@ import android.hardware.biometrics.IBiometricSysuiReceiver; import android.hardware.biometrics.PromptInfo; import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback; import android.os.Bundle; +import android.os.RemoteException; import android.platform.test.annotations.EnableFlags; +import android.util.Pair; import android.view.KeyEvent; import android.view.WindowInsets; import android.view.WindowInsets.Type.InsetsType; @@ -46,6 +48,7 @@ import android.view.WindowInsetsController.Behavior; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.internal.statusbar.DisableStates; import com.android.internal.statusbar.LetterboxDetails; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.view.AppearanceRegion; @@ -58,11 +61,14 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.HashMap; +import java.util.Map; + @SmallTest @RunWith(AndroidJUnit4.class) public class CommandQueueTest extends SysuiTestCase { - private static final LetterboxDetails[] TEST_LETTERBOX_DETAILS = new LetterboxDetails[] { + private static final LetterboxDetails[] TEST_LETTERBOX_DETAILS = new LetterboxDetails[]{ new LetterboxDetails( /* letterboxInnerBounds= */ new Rect(100, 0, 200, 500), /* letterboxFullBounds= */ new Rect(0, 0, 500, 100), @@ -119,6 +125,27 @@ public class CommandQueueTest extends SysuiTestCase { } @Test + public void testDisableForAllDisplays() throws RemoteException { + int state1 = 14; + int state2 = 42; + int secondaryDisplayState1 = 16; + int secondaryDisplayState2 = 44; + Map<Integer, Pair<Integer, Integer>> displaysWithStates = new HashMap<>(); + displaysWithStates.put(DEFAULT_DISPLAY, new Pair<>(state1, state2)); // Example values + displaysWithStates.put(SECONDARY_DISPLAY, + new Pair<>(secondaryDisplayState1, secondaryDisplayState2)); // Example values + DisableStates expectedDisableStates = new DisableStates(displaysWithStates, true); + + mCommandQueue.disableForAllDisplays(expectedDisableStates); + waitForIdleSync(); + + verify(mCallbacks).disable(eq(DEFAULT_DISPLAY), eq(state1), eq(state2), eq(true)); + verify(mCallbacks).disable(eq(SECONDARY_DISPLAY), eq(secondaryDisplayState1), + eq(secondaryDisplayState2), eq(true)); + } + + + @Test public void testExpandNotifications() { mCommandQueue.animateExpandNotificationsPanel(); waitForIdleSync(); @@ -475,7 +502,8 @@ public class CommandQueueTest extends SysuiTestCase { final long requestId = 10; mCommandQueue.showAuthenticationDialog(promptInfo, receiver, sensorIds, - credentialAllowed, requireConfirmation, userId, operationId, packageName, requestId); + credentialAllowed, requireConfirmation, userId, operationId, packageName, + requestId); waitForIdleSync(); verify(mCallbacks).showAuthenticationDialog(eq(promptInfo), eq(receiver), eq(sensorIds), eq(credentialAllowed), eq(requireConfirmation), eq(userId), eq(operationId), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt index e04162bf990a..18f25cd4838b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt @@ -16,24 +16,35 @@ package com.android.systemui.statusbar +import android.app.Notification +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.res.R +import com.android.systemui.statusbar.notification.collection.BundleEntry +import com.android.systemui.statusbar.notification.collection.EntryAdapterFactory +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationTestHelper +import com.android.systemui.statusbar.notification.row.entryAdapterFactory import com.android.systemui.statusbar.notification.shared.NotificationBundleUi +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 +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class NotificationGroupingUtilTest(flags: FlagsParameterization) : SysuiTestCase() { - + private val kosmos = testKosmos() private lateinit var underTest: NotificationGroupingUtil + private val factory: EntryAdapterFactory = kosmos.entryAdapterFactory private lateinit var testHelper: NotificationTestHelper companion object { @@ -60,4 +71,55 @@ class NotificationGroupingUtilTest(flags: FlagsParameterization) : SysuiTestCase underTest = NotificationGroupingUtil(row) assertThat(underTest.showsTime(row)).isTrue() } + + @Test + fun iconExtractor_extractsSbn_notification() { + val row = testHelper.createRow() + + underTest = NotificationGroupingUtil(row) + + assertThat(NotificationGroupingUtil.ICON_EXTRACTOR.extractData(row)).isInstanceOf( + Notification::class.java) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun iconExtractor_noException_bundle() { + val row = mock(ExpandableNotificationRow::class.java) + val be = BundleEntry("promotions") + `when`(row.entryAdapter).thenReturn(factory.create(be)) + + underTest = NotificationGroupingUtil(row) + + assertThat(NotificationGroupingUtil.ICON_EXTRACTOR.extractData(row)).isNull() + } + + @Test + fun iconComparator_sameNotificationIcon() { + val n1 = NotificationGroupingUtil.ICON_EXTRACTOR.extractData(testHelper.createRow()) + val n2 = NotificationGroupingUtil.ICON_EXTRACTOR.extractData(testHelper.createRow()) + + assertThat(NotificationGroupingUtil.IconComparator().hasSameIcon(n1, n2)).isTrue() + } + + @Test + fun iconComparator_differentNotificationIcon() { + val notif = Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_menu).build() + val n1 = NotificationGroupingUtil.ICON_EXTRACTOR.extractData(testHelper.createRow(notif)) + val n2 = NotificationGroupingUtil.ICON_EXTRACTOR.extractData(testHelper.createRow()) + + assertThat(NotificationGroupingUtil.IconComparator().hasSameIcon(n1, n2)).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun iconComparator_bundleNotification() { + assertThat(NotificationGroupingUtil.IconComparator().hasSameIcon(null, + NotificationGroupingUtil.ICON_EXTRACTOR.extractData(testHelper.createRow()))).isFalse() + } + + @Test + fun iconComparator_twoBundles() { + assertThat(NotificationGroupingUtil.IconComparator().hasSameIcon(null, null)).isFalse() + } }
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt index 3ecf302204bc..67af7a54988e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt @@ -33,6 +33,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionChangeEvent +import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.DozeParameters @@ -45,6 +46,8 @@ import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor import com.android.wm.shell.appzoomout.AppZoomOut import com.google.common.truth.Truth.assertThat +import java.util.Optional +import java.util.function.Consumer import org.junit.Before import org.junit.Rule import org.junit.Test @@ -65,8 +68,6 @@ import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnit -import java.util.Optional -import java.util.function.Consumer @RunWith(AndroidJUnit4::class) @RunWithLooper @@ -75,6 +76,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { private val kosmos = testKosmos() private val applicationScope = kosmos.testScope.backgroundScope + private val shadeDisplayRepository = kosmos.fakeShadeDisplaysRepository @Mock private lateinit var statusBarStateController: StatusBarStateController @Mock private lateinit var blurUtils: BlurUtils @Mock private lateinit var biometricUnlockController: BiometricUnlockController @@ -135,7 +137,8 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { windowRootViewBlurInteractor, appZoomOutOptional, applicationScope, - dumpManager + dumpManager, + { shadeDisplayRepository }, ) notificationShadeDepthController.shadeAnimation = shadeAnimation notificationShadeDepthController.brightnessMirrorSpring = brightnessSpring @@ -355,6 +358,36 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) + fun updateBlurCallback_shadeInExternalDisplay_doesSetZeroZoom() { + notificationShadeDepthController.onPanelExpansionChanged( + ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false) + ) + notificationShadeDepthController.addListener(listener) + shadeDisplayRepository.setDisplayId(1) // not default display. + + notificationShadeDepthController.updateBlurCallback.doFrame(0) + + verify(wallpaperController).setNotificationShadeZoom(eq(0f)) + verify(listener).onWallpaperZoomOutChanged(eq(0f)) + } + + @Test + @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) + fun updateBlurCallback_shadeInDefaultDisplay_doesNotSetZeroZoom() { + notificationShadeDepthController.onPanelExpansionChanged( + ShadeExpansionChangeEvent(fraction = 1f, expanded = true, tracking = false) + ) + notificationShadeDepthController.addListener(listener) + shadeDisplayRepository.setDisplayId(0) // shade is in default display + + notificationShadeDepthController.updateBlurCallback.doFrame(0) + + verify(wallpaperController).setNotificationShadeZoom(floatThat { it != 0f }) + verify(listener).onWallpaperZoomOutChanged(floatThat { it != 0f }) + } + + @Test @DisableFlags(Flags.FLAG_NOTIFICATION_SHADE_BLUR) fun updateBlurCallback_setsOpaque_whenScrim() { scrimVisibilityCaptor.value.accept(ScrimController.OPAQUE) @@ -488,10 +521,10 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { } private fun enableSplitShade() { - `when` (shadeModeInteractor.isSplitShade).thenReturn(true) + `when`(shadeModeInteractor.isSplitShade).thenReturn(true) } private fun disableSplitShade() { - `when` (shadeModeInteractor.isSplitShade).thenReturn(false) + `when`(shadeModeInteractor.isSplitShade).thenReturn(false) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index 8120c2d9f816..5ec9b601bcca 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt @@ -44,6 +44,7 @@ import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.core.StatusBarRootModernization +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization @@ -106,6 +107,7 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse() + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isFalse() } @Test @@ -117,6 +119,7 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse() + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isFalse() } @Test @@ -128,6 +131,7 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse() + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isFalse() } @Test @@ -152,6 +156,7 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue() + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isFalse() } @Test @@ -176,6 +181,7 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue() + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isFalse() } @Test @@ -200,6 +206,7 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue() + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isFalse() } @Test @@ -793,8 +800,8 @@ class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } private val PROMOTED_CONTENT_WITH_COLOR = - PromotedNotificationContentModel.Builder("notif") - .apply { + PromotedNotificationContentBuilder("notif") + .applyToShared { this.colors = PromotedNotificationContentModel.Colors( backgroundColor = PROMOTED_BACKGROUND_COLOR, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt index d921ab3b284d..a60e7c19d8bd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt @@ -135,6 +135,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { ) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) @@ -157,6 +158,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { ) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) @@ -177,6 +179,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) @@ -215,6 +218,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { ) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) @@ -245,6 +249,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { // Only the projection info will show a timer assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) 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 7f8f5f43e775..9ce43a0792c2 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 @@ -30,7 +30,7 @@ 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 +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat @@ -420,7 +420,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { // WHEN the notif gets a new UID that starts as visible activityManagerRepository.fake.startingIsAppVisibleValue = true val newPromotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.shortCriticalText = "Arrived" } val newPromotedContent = newPromotedContentBuilder.build() @@ -452,6 +452,6 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { companion object { private const val UID = 885 - private val PROMOTED_CONTENT = PromotedNotificationContentModel.Builder("notif1").build() + private val PROMOTED_CONTENT = PromotedNotificationContentBuilder("notif1").build() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt index 0b9b297130a2..202d5cff95d7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt @@ -36,7 +36,7 @@ import com.android.systemui.statusbar.notification.data.repository.ActiveNotific import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.addNotif import com.android.systemui.statusbar.notification.data.repository.removeNotif -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.notification.shared.CallType import com.android.systemui.testKosmos @@ -65,7 +65,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = mock<StatusBarIconView>(), - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -96,7 +96,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = null, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -115,7 +115,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = null, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -135,7 +135,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = icon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -158,12 +158,12 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif1", statusBarChipIcon = firstIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), + promotedContent = PromotedNotificationContentBuilder("notif1").build(), ), activeNotificationModel( key = "notif2", statusBarChipIcon = secondIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif2").build(), + promotedContent = PromotedNotificationContentBuilder("notif2").build(), ), activeNotificationModel( key = "notif3", @@ -195,7 +195,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { key = "notif", uid = uid, statusBarChipIcon = mock<StatusBarIconView>(), - promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), + promotedContent = PromotedNotificationContentBuilder("notif1").build(), ) ) ) @@ -223,14 +223,14 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { key = "promotedNormal", statusBarChipIcon = mock(), promotedContent = - PromotedNotificationContentModel.Builder("promotedNormal").build(), + PromotedNotificationContentBuilder("promotedNormal").build(), callType = CallType.None, ), activeNotificationModel( key = "promotedCall", statusBarChipIcon = mock(), promotedContent = - PromotedNotificationContentModel.Builder("promotedCall").build(), + PromotedNotificationContentBuilder("promotedCall").build(), callType = CallType.Ongoing, ), ) @@ -256,7 +256,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = firstIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -269,7 +269,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = secondIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -282,7 +282,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = thirdIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -302,7 +302,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = mock(), - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -325,7 +325,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = mock(), - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -348,7 +348,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif1", statusBarChipIcon = firstIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), + promotedContent = PromotedNotificationContentBuilder("notif1").build(), ) setNotifs(listOf(notif1)) assertThat(latest!!.map { it.key }).containsExactly("notif1").inOrder() @@ -359,7 +359,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif2", statusBarChipIcon = secondIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif2").build(), + promotedContent = PromotedNotificationContentBuilder("notif2").build(), ) setNotifs(listOf(notif1, notif2)) @@ -380,7 +380,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { // WHEN notif1 gets an update val notif1NewPromotedContent = - PromotedNotificationContentModel.Builder("notif1").apply { + PromotedNotificationContentBuilder("notif1").applyToShared { this.shortCriticalText = "Arrived" } setNotifs( @@ -426,8 +426,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { key = notif1Info.key, uid = notif1Info.uid, statusBarChipIcon = notif1Info.icon, - promotedContent = - PromotedNotificationContentModel.Builder(notif1Info.key).build(), + promotedContent = PromotedNotificationContentBuilder(notif1Info.key).build(), ) ) activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = false) @@ -443,8 +442,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { key = notif2Info.key, uid = notif2Info.uid, statusBarChipIcon = notif2Info.icon, - promotedContent = - PromotedNotificationContentModel.Builder(notif2Info.key).build(), + promotedContent = PromotedNotificationContentBuilder(notif2Info.key).build(), ) ) activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false) @@ -482,16 +480,14 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { key = notif1Info.key, uid = notif1Info.uid, statusBarChipIcon = notif1Info.icon, - promotedContent = - PromotedNotificationContentModel.Builder(notif1Info.key).build(), + promotedContent = PromotedNotificationContentBuilder(notif1Info.key).build(), ) val notif2 = activeNotificationModel( key = notif2Info.key, uid = notif2Info.uid, statusBarChipIcon = notif2Info.icon, - promotedContent = - PromotedNotificationContentModel.Builder(notif2Info.key).build(), + promotedContent = PromotedNotificationContentBuilder(notif2Info.key).build(), ) setNotifs(listOf(notif1, notif2)) assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder() @@ -537,16 +533,14 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { key = notif1Info.key, uid = notif1Info.uid, statusBarChipIcon = notif1Info.icon, - promotedContent = - PromotedNotificationContentModel.Builder(notif1Info.key).build(), + promotedContent = PromotedNotificationContentBuilder(notif1Info.key).build(), ) val notif2 = activeNotificationModel( key = notif2Info.key, uid = notif2Info.uid, statusBarChipIcon = notif2Info.icon, - promotedContent = - PromotedNotificationContentModel.Builder(notif2Info.key).build(), + promotedContent = PromotedNotificationContentBuilder(notif2Info.key).build(), ) setNotifs(listOf(notif1, notif2)) assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder() @@ -567,8 +561,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { key = notif3Info.key, uid = notif3Info.uid, statusBarChipIcon = notif3Info.icon, - promotedContent = - PromotedNotificationContentModel.Builder(notif3Info.key).build(), + promotedContent = PromotedNotificationContentBuilder(notif3Info.key).build(), ) setNotifs(listOf(notif1, notif2, notif3)) @@ -597,8 +590,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { key = notif1Info.key, uid = notif1Info.uid, statusBarChipIcon = notif1Info.icon, - promotedContent = - PromotedNotificationContentModel.Builder(notif1Info.key).build(), + promotedContent = PromotedNotificationContentBuilder(notif1Info.key).build(), ) setNotifs(listOf(notif1)) @@ -609,8 +601,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { key = notif2Info.key, uid = notif2Info.uid, statusBarChipIcon = notif2Info.icon, - promotedContent = - PromotedNotificationContentModel.Builder(notif2Info.key).build(), + promotedContent = PromotedNotificationContentBuilder(notif2Info.key).build(), ) setNotifs(listOf(notif1, notif2)) @@ -637,7 +628,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { // WHEN notif2 gets an update val notif2NewPromotedContent = - PromotedNotificationContentModel.Builder("notif2").apply { + PromotedNotificationContentBuilder("notif2").applyToShared { this.shortCriticalText = "Arrived" } setNotifs( @@ -662,8 +653,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { key = notif3Info.key, uid = notif3Info.uid, statusBarChipIcon = notif3Info.icon, - promotedContent = - PromotedNotificationContentModel.Builder(notif3Info.key).build(), + promotedContent = PromotedNotificationContentBuilder(notif3Info.key).build(), ) setNotifs(listOf(notif1, notif2, notif3)) @@ -710,8 +700,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif|uid1", statusBarChipIcon = firstIcon, - promotedContent = - PromotedNotificationContentModel.Builder("notif|uid1").build(), + promotedContent = PromotedNotificationContentBuilder("notif|uid1").build(), ) ) ) @@ -725,8 +714,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif|uid2", statusBarChipIcon = secondIcon, - promotedContent = - PromotedNotificationContentModel.Builder("notif|uid2").build(), + promotedContent = PromotedNotificationContentBuilder("notif|uid2").build(), ) ) ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index b5cfc7e9080d..eecdbbfd408b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -43,6 +43,7 @@ import com.android.systemui.statusbar.notification.data.repository.ActiveNotific import com.android.systemui.statusbar.notification.data.repository.UnconfinedFakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.headsup.PinnedStatus +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel @@ -101,7 +102,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = null, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -121,7 +122,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = null, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -142,7 +143,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { key = "notif", appName = "Fake App Name", statusBarChipIcon = icon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -172,7 +173,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { key = notifKey, appName = "Fake App Name", statusBarChipIcon = null, - promotedContent = PromotedNotificationContentModel.Builder(notifKey).build(), + promotedContent = PromotedNotificationContentBuilder(notifKey).build(), ) ) ) @@ -195,7 +196,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.chips) val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.colors = PromotedNotificationContentModel.Colors( backgroundColor = 56, @@ -229,12 +230,12 @@ class NotifChipsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif1", statusBarChipIcon = firstIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), + promotedContent = PromotedNotificationContentBuilder("notif1").build(), ), activeNotificationModel( key = "notif2", statusBarChipIcon = secondIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif2").build(), + promotedContent = PromotedNotificationContentBuilder("notif2").build(), ), activeNotificationModel( key = "notif3", @@ -264,13 +265,12 @@ class NotifChipsViewModelTest : SysuiTestCase() { activeNotificationModel( key = firstKey, statusBarChipIcon = null, - promotedContent = PromotedNotificationContentModel.Builder(firstKey).build(), + promotedContent = PromotedNotificationContentBuilder(firstKey).build(), ), activeNotificationModel( key = secondKey, statusBarChipIcon = null, - promotedContent = - PromotedNotificationContentModel.Builder(secondKey).build(), + promotedContent = PromotedNotificationContentBuilder(secondKey).build(), ), activeNotificationModel( key = thirdKey, @@ -294,7 +294,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.shortCriticalText = "Arrived" this.time = When.Time(currentTime + 30.minutes.inWholeMilliseconds) } @@ -321,7 +321,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.chips) val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { this.time = null } + PromotedNotificationContentBuilder("notif").applyToShared { this.time = null } setNotifs( listOf( activeNotificationModel( @@ -346,7 +346,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.wasPromotedAutomatically = true this.time = When.Time(currentTime + 30.minutes.inWholeMilliseconds) } @@ -374,7 +374,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.wasPromotedAutomatically = false this.time = When.Time(currentTime + 30.minutes.inWholeMilliseconds) } @@ -402,7 +402,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.time = When.Time(currentTime + 13.minutes.inWholeMilliseconds) } @@ -430,7 +430,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.time = When.Time(currentTime + 500) } @@ -458,7 +458,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.time = When.Time(currentTime) } @@ -486,7 +486,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.time = When.Time(currentTime - 2.minutes.inWholeMilliseconds) } @@ -515,7 +515,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.time = When.Time(currentTime + 3.minutes.inWholeMilliseconds) } @@ -555,7 +555,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { val whenElapsed = currentElapsed - 1.minutes.inWholeMilliseconds val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.time = When.Chronometer(elapsedRealtimeMillis = whenElapsed, isCountDown = false) } @@ -592,7 +592,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { val whenElapsed = currentElapsed + 10.minutes.inWholeMilliseconds val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.time = When.Chronometer(elapsedRealtimeMillis = whenElapsed, isCountDown = true) } @@ -623,7 +623,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds) } setNotifs( @@ -653,7 +653,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds) } setNotifs( @@ -690,11 +690,11 @@ class NotifChipsViewModelTest : SysuiTestCase() { fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds) } val otherPromotedContentBuilder = - PromotedNotificationContentModel.Builder("other notif").apply { + PromotedNotificationContentBuilder("other notif").applyToShared { this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds) } val icon = createStatusBarIconViewOrNull() @@ -738,7 +738,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.time = When.Time(currentTime + 10.minutes.inWholeMilliseconds) } setNotifs( @@ -781,7 +781,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { activeNotificationModel( key, statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = PromotedNotificationContentModel.Builder(key).build(), + promotedContent = PromotedNotificationContentBuilder(key).build(), ) ) ) @@ -809,7 +809,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { activeNotificationModel( key, statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = PromotedNotificationContentModel.Builder(key).build(), + promotedContent = PromotedNotificationContentBuilder(key).build(), ) ) ) @@ -842,6 +842,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { expectedContentDescriptionSubstrings: List<String> = emptyList(), ) { val active = latest as OngoingActivityChipModel.Active + assertThat(active.isImportantForPrivacy).isFalse() if (StatusBarConnectedDisplays.isEnabled) { assertThat(active.icon) .isInstanceOf( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt index 538247e26a6b..f8c57655005e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt @@ -87,7 +87,7 @@ class ScreenRecordChipInteractorTest : SysuiTestCase() { screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting - assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = null)) + assertThat((latest as ScreenRecordChipModel.Recording).recordedTask).isNull() } @Test @@ -99,7 +99,7 @@ class ScreenRecordChipInteractorTest : SysuiTestCase() { mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen("host.package") - assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = null)) + assertThat((latest as ScreenRecordChipModel.Recording).recordedTask).isNull() } @Test @@ -116,7 +116,48 @@ class ScreenRecordChipInteractorTest : SysuiTestCase() { task, ) - assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task)) + assertThat((latest as ScreenRecordChipModel.Recording).recordedTask).isEqualTo(task) + } + + @Test + fun screenRecordState_projectionIsNotProjecting_hostPackageNull() = + testScope.runTest { + val latest by collectLastValue(underTest.screenRecordState) + + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording + mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting + + assertThat((latest as ScreenRecordChipModel.Recording).hostPackage).isNull() + } + + @Test + fun screenRecordState_projectionIsEntireScreen_hostPackageMatches() = + testScope.runTest { + val latest by collectLastValue(underTest.screenRecordState) + + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(hostPackage = "host.package") + + assertThat((latest as ScreenRecordChipModel.Recording).hostPackage) + .isEqualTo("host.package") + } + + @Test + fun screenRecordState_projectionIsSingleTask_hostPackageMatches() = + testScope.runTest { + val latest by collectLastValue(underTest.screenRecordState) + + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.SingleTask( + hostPackage = "host.package", + hostDeviceName = null, + task = createTask(taskId = 1), + ) + + assertThat((latest as ScreenRecordChipModel.Recording).hostPackage) + .isEqualTo("host.package") } @Test @@ -150,7 +191,7 @@ class ScreenRecordChipInteractorTest : SysuiTestCase() { advanceTimeBy(901) // THEN we automatically update to the recording state - assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = null)) + assertThat(latest).isInstanceOf(ScreenRecordChipModel.Recording::class.java) } @Test @@ -175,13 +216,14 @@ class ScreenRecordChipInteractorTest : SysuiTestCase() { ) // THEN we immediately switch to Recording, and we have the task - assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task)) + assertThat(latest).isInstanceOf(ScreenRecordChipModel.Recording::class.java) + assertThat((latest as ScreenRecordChipModel.Recording).recordedTask).isEqualTo(task) // WHEN more than 900ms has elapsed advanceTimeBy(200) // THEN we still stay in the Recording state and we have the task - assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task)) + assertThat((latest as ScreenRecordChipModel.Recording).recordedTask).isEqualTo(task) } @Test @@ -247,7 +289,7 @@ class ScreenRecordChipInteractorTest : SysuiTestCase() { // THEN we *do* auto-start 400ms later advanceTimeBy(401) - assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = null)) + assertThat(latest).isInstanceOf(ScreenRecordChipModel.Recording::class.java) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt index 005af366a6c0..91942213ce75 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt @@ -110,6 +110,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(400) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Countdown::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() assertThat((latest as OngoingActivityChipModel.Active).icon).isNull() assertThat((latest as OngoingActivityChipModel.Active).onClickListenerLegacy).isNull() } @@ -156,6 +157,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) @@ -261,6 +263,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() assertThat((latest as OngoingActivityChipModel.Active.Timer).startTimeMs) .isEqualTo(1234) @@ -275,6 +278,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { // THEN the start time is still the old start time assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() assertThat((latest as OngoingActivityChipModel.Active.Timer).startTimeMs) .isEqualTo(1234) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt index d6b10a89726e..99e378c78ee2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt @@ -384,6 +384,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { MediaProjectionState.Projecting.NoScreen(NORMAL_PACKAGE, hostDeviceName = null) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) @@ -407,6 +408,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { ) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) @@ -424,6 +426,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + assertThat((latest as OngoingActivityChipModel.Active).isImportantForPrivacy).isTrue() val icon = (((latest as OngoingActivityChipModel.Active).icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt index 7135cf01394a..83b3c9cae3c1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt @@ -63,7 +63,7 @@ import com.android.systemui.statusbar.notification.data.repository.activeNotific import com.android.systemui.statusbar.notification.data.repository.addNotif import com.android.systemui.statusbar.notification.data.repository.addNotifs import com.android.systemui.statusbar.notification.data.repository.removeNotif -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization @@ -358,7 +358,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { addOngoingCallState(key = "call") val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.shortCriticalText = "Some text here" } activeNotificationListRepository.addNotif( @@ -741,7 +741,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = icon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -765,7 +765,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = icon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -791,14 +791,12 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "firstNotif", statusBarChipIcon = firstIcon, - promotedContent = - PromotedNotificationContentModel.Builder("firstNotif").build(), + promotedContent = PromotedNotificationContentBuilder("firstNotif").build(), ), activeNotificationModel( key = "secondNotif", statusBarChipIcon = secondIcon, - promotedContent = - PromotedNotificationContentModel.Builder("secondNotif").build(), + promotedContent = PromotedNotificationContentBuilder("secondNotif").build(), ), ) ) @@ -822,14 +820,12 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "firstNotif", statusBarChipIcon = firstIcon, - promotedContent = - PromotedNotificationContentModel.Builder("firstNotif").build(), + promotedContent = PromotedNotificationContentBuilder("firstNotif").build(), ), activeNotificationModel( key = "secondNotif", statusBarChipIcon = secondIcon, - promotedContent = - PromotedNotificationContentModel.Builder("secondNotif").build(), + promotedContent = PromotedNotificationContentBuilder("secondNotif").build(), ), ) ) @@ -857,20 +853,17 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "firstNotif", statusBarChipIcon = firstIcon, - promotedContent = - PromotedNotificationContentModel.Builder("firstNotif").build(), + promotedContent = PromotedNotificationContentBuilder("firstNotif").build(), ), activeNotificationModel( key = "secondNotif", statusBarChipIcon = secondIcon, - promotedContent = - PromotedNotificationContentModel.Builder("secondNotif").build(), + promotedContent = PromotedNotificationContentBuilder("secondNotif").build(), ), activeNotificationModel( key = "thirdNotif", statusBarChipIcon = thirdIcon, - promotedContent = - PromotedNotificationContentModel.Builder("thirdNotif").build(), + promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(), ), ) ) @@ -896,26 +889,22 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "firstNotif", statusBarChipIcon = firstIcon, - promotedContent = - PromotedNotificationContentModel.Builder("firstNotif").build(), + promotedContent = PromotedNotificationContentBuilder("firstNotif").build(), ), activeNotificationModel( key = "secondNotif", statusBarChipIcon = secondIcon, - promotedContent = - PromotedNotificationContentModel.Builder("secondNotif").build(), + promotedContent = PromotedNotificationContentBuilder("secondNotif").build(), ), activeNotificationModel( key = "thirdNotif", statusBarChipIcon = thirdIcon, - promotedContent = - PromotedNotificationContentModel.Builder("thirdNotif").build(), + promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(), ), activeNotificationModel( key = "fourthNotif", statusBarChipIcon = fourthIcon, - promotedContent = - PromotedNotificationContentModel.Builder("fourthNotif").build(), + promotedContent = PromotedNotificationContentBuilder("fourthNotif").build(), ), ) ) @@ -941,20 +930,17 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "firstNotif", statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = - PromotedNotificationContentModel.Builder("firstNotif").build(), + promotedContent = PromotedNotificationContentBuilder("firstNotif").build(), ), activeNotificationModel( key = "secondNotif", statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = - PromotedNotificationContentModel.Builder("secondNotif").build(), + promotedContent = PromotedNotificationContentBuilder("secondNotif").build(), ), activeNotificationModel( key = "thirdNotif", statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = - PromotedNotificationContentModel.Builder("thirdNotif").build(), + promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(), ), ) ) @@ -973,26 +959,22 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "firstNotif", statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = - PromotedNotificationContentModel.Builder("firstNotif").build(), + promotedContent = PromotedNotificationContentBuilder("firstNotif").build(), ), activeNotificationModel( key = "secondNotif", statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = - PromotedNotificationContentModel.Builder("secondNotif").build(), + promotedContent = PromotedNotificationContentBuilder("secondNotif").build(), ), activeNotificationModel( key = "thirdNotif", statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = - PromotedNotificationContentModel.Builder("thirdNotif").build(), + promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(), ), activeNotificationModel( key = "fourthNotif", statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = - PromotedNotificationContentModel.Builder("fourthNotif").build(), + promotedContent = PromotedNotificationContentBuilder("fourthNotif").build(), ), ) ) @@ -1016,14 +998,12 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "firstNotif", statusBarChipIcon = firstIcon, - promotedContent = - PromotedNotificationContentModel.Builder("firstNotif").build(), + promotedContent = PromotedNotificationContentBuilder("firstNotif").build(), ), activeNotificationModel( key = "secondNotif", statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = - PromotedNotificationContentModel.Builder("secondNotif").build(), + promotedContent = PromotedNotificationContentBuilder("secondNotif").build(), ), ) ) @@ -1050,20 +1030,17 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "firstNotif", statusBarChipIcon = firstIcon, - promotedContent = - PromotedNotificationContentModel.Builder("firstNotif").build(), + promotedContent = PromotedNotificationContentBuilder("firstNotif").build(), ), activeNotificationModel( key = "secondNotif", statusBarChipIcon = secondIcon, - promotedContent = - PromotedNotificationContentModel.Builder("secondNotif").build(), + promotedContent = PromotedNotificationContentBuilder("secondNotif").build(), ), activeNotificationModel( key = "thirdNotif", statusBarChipIcon = thirdIcon, - promotedContent = - PromotedNotificationContentModel.Builder("thirdNotif").build(), + promotedContent = PromotedNotificationContentBuilder("thirdNotif").build(), ), ) ) @@ -1092,7 +1069,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) @@ -1114,14 +1091,14 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif1", statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), + promotedContent = PromotedNotificationContentBuilder("notif1").build(), ) ) activeNotificationListRepository.addNotif( activeNotificationModel( key = "notif2", statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = PromotedNotificationContentModel.Builder("notif2").build(), + promotedContent = PromotedNotificationContentBuilder("notif2").build(), ) ) @@ -1143,14 +1120,14 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif1", statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), + promotedContent = PromotedNotificationContentBuilder("notif1").build(), ) ) activeNotificationListRepository.addNotif( activeNotificationModel( key = "notif2", statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = PromotedNotificationContentModel.Builder("notif2").build(), + promotedContent = PromotedNotificationContentBuilder("notif2").build(), ) ) @@ -1159,6 +1136,11 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .inOrder() } + // The ranking between different chips should stay consistent between + // OngoingActivityChipsViewModel and PromotedNotificationsInteractor. + // Make sure to also change + // PromotedNotificationsInteractorTest#orderedChipNotificationKeys_rankingIsCorrect + // if you change this test. @EnableChipsModernization @Test fun chips_screenRecordAndCallAndPromotedNotifs_secondNotifInOverflow() = @@ -1173,7 +1155,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) addOngoingCallState(key = callNotificationKey) @@ -1184,7 +1166,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif2", statusBarChipIcon = notifIcon2, - promotedContent = PromotedNotificationContentModel.Builder("notif2").build(), + promotedContent = PromotedNotificationContentBuilder("notif2").build(), ) ) @@ -1209,7 +1191,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -1260,7 +1242,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) @@ -1299,7 +1281,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) // And everything else hidden @@ -1377,7 +1359,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif1", statusBarChipIcon = notif1Icon, - promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), + promotedContent = PromotedNotificationContentBuilder("notif1").build(), ) ) ) @@ -1431,7 +1413,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif2", statusBarChipIcon = notif2Icon, - promotedContent = PromotedNotificationContentModel.Builder("notif2").build(), + promotedContent = PromotedNotificationContentBuilder("notif2").build(), ) ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt index ba2d40ba6a0d..3d7ced747450 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.app.Notification +import android.app.Notification.FLAG_FOREGROUND_SERVICE import android.app.NotificationManager import android.app.PendingIntent import android.app.Person @@ -31,6 +32,10 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.mediaprojection.data.model.MediaProjectionState +import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository +import com.android.systemui.screenrecord.data.model.ScreenRecordModel +import com.android.systemui.screenrecord.data.repository.screenRecordRepository import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.core.StatusBarRootModernization @@ -169,6 +174,87 @@ class ColorizedFgsCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME) + fun testIncludeScreenRecordNotifInSection_importanceDefault() = + kosmos.runTest { + // GIVEN a screen record event + screen record notif that has a status bar chip + screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg") + val screenRecordEntry = + buildNotificationEntry(tag = "screenRecord", promoted = false) { + setImportance(NotificationManager.IMPORTANCE_DEFAULT) + setFlag(context, FLAG_FOREGROUND_SERVICE, true) + } + + renderNotificationListInteractor.setRenderedList(listOf(screenRecordEntry)) + + val orderedChipNotificationKeys by + collectLastValue(promotedNotificationsInteractor.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|screenRecord|0") + .inOrder() + + // THEN the entry is in the fgs section + assertTrue(sectioner.isInSection(screenRecordEntry)) + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME) + fun testDiscludeScreenRecordNotifInSection_importanceMin() = + kosmos.runTest { + // GIVEN a screen record event + screen record notif that has a status bar chip + screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg") + val screenRecordEntry = + buildNotificationEntry(tag = "screenRecord", promoted = false) { + setImportance(NotificationManager.IMPORTANCE_MIN) + setFlag(context, FLAG_FOREGROUND_SERVICE, true) + } + + renderNotificationListInteractor.setRenderedList(listOf(screenRecordEntry)) + + val orderedChipNotificationKeys by + collectLastValue(promotedNotificationsInteractor.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|screenRecord|0") + .inOrder() + + // THEN the entry is NOT in the fgs section + assertFalse(sectioner.isInSection(screenRecordEntry)) + } + + @Test + @DisableFlags(PromotedNotificationUi.FLAG_NAME) + fun testDiscludeScreenRecordNotifInSection_flagDisabled() = + kosmos.runTest { + // GIVEN a screen record event + screen record notif that has a status bar chip + screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg") + val screenRecordEntry = + buildNotificationEntry(tag = "screenRecord", promoted = false) { + setImportance(NotificationManager.IMPORTANCE_DEFAULT) + setFlag(context, FLAG_FOREGROUND_SERVICE, true) + } + + renderNotificationListInteractor.setRenderedList(listOf(screenRecordEntry)) + + val orderedChipNotificationKeys by + collectLastValue(promotedNotificationsInteractor.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|screenRecord|0") + .inOrder() + + // THEN the entry is NOT in the fgs section + assertFalse(sectioner.isInSection(screenRecordEntry)) + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME) fun promoterSelectsPromotedOngoing_flagEnabled() { val promoter: NotifPromoter = withArgCaptor { verify(notifPipeline).addPromoter(capture()) } @@ -234,8 +320,4 @@ class ColorizedFgsCoordinatorTest : SysuiTestCase() { val person = Person.Builder().setName("person").build() return Notification.CallStyle.forOngoingCall(person, pendingIntent) } - - companion object { - private const val NOTIF_USER_ID = 0 - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt index d3befa921e9e..29bb29f25797 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt @@ -29,7 +29,7 @@ import com.android.systemui.statusbar.notification.data.model.activeNotification import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder import com.android.systemui.statusbar.notification.shared.CallType import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -170,7 +170,7 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { val promoted1 = activeNotificationModel( key = "notif1", - promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), + promotedContent = PromotedNotificationContentBuilder("notif1").build(), ) val notPromoted2 = activeNotificationModel(key = "notif2", promotedContent = null) @@ -208,14 +208,14 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { val promoted1 = activeNotificationModel( key = "notif1", - promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), + promotedContent = PromotedNotificationContentBuilder("notif1").build(), ) val notPromoted2 = activeNotificationModel(key = "notif2", promotedContent = null) val notPromoted3 = activeNotificationModel(key = "notif3", promotedContent = null) val promoted4 = activeNotificationModel( key = "notif4", - promotedContent = PromotedNotificationContentModel.Builder("notif4").build(), + promotedContent = PromotedNotificationContentBuilder("notif4").build(), ) activeNotificationListRepository.activeNotifications.value = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt index 35b19c19d5ce..5c749e6e35d6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt @@ -28,7 +28,8 @@ import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels import com.android.systemui.statusbar.notification.shared.byKey import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock @@ -130,7 +131,7 @@ class RenderNotificationsListInteractorTest : SysuiTestCase() { val promoted2 = mockNotificationEntry( "key2", - promotedContent = PromotedNotificationContentModel.Builder("key2").build(), + promotedContent = PromotedNotificationContentBuilder("key2").build(), ) underTest.setRenderedList(listOf(notPromoted1, promoted2)) @@ -149,7 +150,7 @@ class RenderNotificationsListInteractorTest : SysuiTestCase() { private fun mockNotificationEntry( key: String, rank: Int = 0, - promotedContent: PromotedNotificationContentModel? = null, + promotedContent: PromotedNotificationContentModels? = null, ): NotificationEntry { val nBuilder = Notification.Builder(context, "a") val notification = nBuilder.build() @@ -165,7 +166,7 @@ class RenderNotificationsListInteractorTest : SysuiTestCase() { whenever(this.representativeEntry).thenReturn(this) whenever(this.ranking).thenReturn(RankingBuilder().setRank(rank).build()) whenever(this.sbn).thenReturn(mockSbn) - whenever(this.promotedNotificationContentModel).thenReturn(promotedContent) + whenever(this.promotedNotificationContentModels).thenReturn(promotedContent) } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt index ee698ae20adb..41120a16e4ea 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt @@ -33,8 +33,10 @@ import com.android.systemui.statusbar.notification.data.repository.notifications import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.headsUpNotificationIconInteractor import com.android.systemui.statusbar.notification.promoted.domain.interactor.aodPromotedNotificationInteractor +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style.Base +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels import com.android.systemui.statusbar.notification.shared.byIsAmbient import com.android.systemui.statusbar.notification.shared.byIsLastMessageFromReply import com.android.systemui.statusbar.notification.shared.byIsPromoted @@ -354,6 +356,6 @@ private val testIcons = private fun promotedContent( key: String, style: PromotedNotificationContentModel.Style, -): PromotedNotificationContentModel { - return PromotedNotificationContentModel.Builder(key).apply { this.style = style }.build() +): PromotedNotificationContentModels { + return PromotedNotificationContentBuilder(key).applyToShared { this.style = style }.build() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt index 0ac944a43de6..cc016b9768b7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt @@ -33,18 +33,21 @@ import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator.Companion.EXTRA_WAS_AUTOMATICALLY_PROMOTED -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels import com.android.systemui.statusbar.notification.row.RowImageInflater import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.android.systemui.util.time.systemClock import com.google.common.truth.Truth.assertThat +import kotlin.test.assertNotNull import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import org.junit.Test @@ -112,12 +115,43 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { setContentText(TEST_CONTENT_TEXT) } - val content = extractContent(entry) + val content = requireContent(entry) - assertThat(content).isNotNull() - assertThat(content?.subText).isEqualTo(TEST_SUB_TEXT) - assertThat(content?.title).isEqualTo(TEST_CONTENT_TITLE) - assertThat(content?.text).isEqualTo(TEST_CONTENT_TEXT) + content.privateVersion.apply { + assertThat(subText).isEqualTo(TEST_SUB_TEXT) + assertThat(title).isEqualTo(TEST_CONTENT_TITLE) + assertThat(text).isEqualTo(TEST_CONTENT_TEXT) + } + + content.publicVersion.apply { + assertThat(subText).isNull() + assertThat(title).isNull() + assertThat(text).isNull() + } + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun extractsContent_commonFields_noRedaction() { + val entry = createEntry { + setSubText(TEST_SUB_TEXT) + setContentTitle(TEST_CONTENT_TITLE) + setContentText(TEST_CONTENT_TEXT) + } + + val content = requireContent(entry, redactionType = REDACTION_TYPE_NONE) + + content.privateVersion.apply { + assertThat(subText).isEqualTo(TEST_SUB_TEXT) + assertThat(title).isEqualTo(TEST_CONTENT_TITLE) + assertThat(text).isEqualTo(TEST_CONTENT_TEXT) + } + + content.publicVersion.apply { + assertThat(subText).isEqualTo(TEST_SUB_TEXT) + assertThat(title).isEqualTo(TEST_CONTENT_TITLE) + assertThat(text).isEqualTo(TEST_CONTENT_TEXT) + } } @Test @@ -125,9 +159,9 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { fun extractContent_wasPromotedAutomatically_false() { val entry = createEntry { extras.putBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, false) } - val content = extractContent(entry) + val content = requireContent(entry).privateVersion - assertThat(content!!.wasPromotedAutomatically).isFalse() + assertThat(content.wasPromotedAutomatically).isFalse() } @Test @@ -135,9 +169,9 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { fun extractContent_wasPromotedAutomatically_true() { val entry = createEntry { extras.putBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, true) } - val content = extractContent(entry) + val content = requireContent(entry).privateVersion - assertThat(content!!.wasPromotedAutomatically).isTrue() + assertThat(content.wasPromotedAutomatically).isTrue() } @Test @@ -146,10 +180,9 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { fun extractContent_apiFlagOff_shortCriticalTextNotExtracted() { val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) } - val content = extractContent(entry) + val content = requireContent(entry).privateVersion - assertThat(content).isNotNull() - assertThat(content?.text).isNull() + assertThat(content.text).isNull() } @Test @@ -161,10 +194,9 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { fun extractContent_apiFlagOn_shortCriticalTextExtracted() { val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) } - val content = extractContent(entry) + val content = requireContent(entry).privateVersion - assertThat(content).isNotNull() - assertThat(content?.shortCriticalText).isEqualTo(TEST_SHORT_CRITICAL_TEXT) + assertThat(content.shortCriticalText).isEqualTo(TEST_SHORT_CRITICAL_TEXT) } @Test @@ -176,10 +208,9 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { fun extractContent_noShortCriticalTextSet_textIsNull() { val entry = createEntry { setShortCriticalText(null) } - val content = extractContent(entry) + val content = requireContent(entry).privateVersion - assertThat(content).isNotNull() - assertThat(content?.shortCriticalText).isNull() + assertThat(content.shortCriticalText).isNull() } @Test @@ -379,17 +410,14 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { setWhen(providedCurrentTime) } - val content = extractContent(entry) - - assertThat(content).isNotNull() + val content = requireContent(entry).privateVersion when (expected) { - ExpectedTime.Null -> assertThat(content?.time).isNull() + ExpectedTime.Null -> assertThat(content.time).isNull() ExpectedTime.Time -> { - val actual = content?.time as? When.Time - assertThat(actual).isNotNull() - assertThat(actual?.currentTimeMillis).isEqualTo(expectedCurrentTime) + val actual = assertNotNull(content.time as? When.Time) + assertThat(actual.currentTimeMillis).isEqualTo(expectedCurrentTime) } ExpectedTime.CountDown, @@ -398,23 +426,24 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { expectedCurrentTime + systemClock.elapsedRealtime() - systemClock.currentTimeMillis() - val actual = content?.time as? When.Chronometer - assertThat(actual).isNotNull() - assertThat(actual?.elapsedRealtimeMillis).isEqualTo(expectedElapsedRealtime) - assertThat(actual?.isCountDown).isEqualTo(expected == ExpectedTime.CountDown) + val actual = assertNotNull(content.time as? When.Chronometer) + assertThat(actual.elapsedRealtimeMillis).isEqualTo(expectedElapsedRealtime) + assertThat(actual.isCountDown).isEqualTo(expected == ExpectedTime.CountDown) } } } + // TODO: Add tests for the style of the publicVersion once we implement that + @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) fun extractContent_fromBaseStyle() { val entry = createEntry { setStyle(null) } - val content = extractContent(entry) + val content = requireContent(entry) - assertThat(content).isNotNull() - assertThat(content?.style).isEqualTo(Style.Base) + assertThat(content.privateVersion.style).isEqualTo(Style.Base) + assertThat(content.publicVersion.style).isEqualTo(Style.Base) } @Test @@ -422,10 +451,10 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { fun extractContent_fromBigPictureStyle() { val entry = createEntry { setStyle(BigPictureStyle()) } - val content = extractContent(entry) + val content = requireContent(entry) - assertThat(content).isNotNull() - assertThat(content?.style).isEqualTo(Style.BigPicture) + assertThat(content.privateVersion.style).isEqualTo(Style.BigPicture) + assertThat(content.publicVersion.style).isEqualTo(Style.Base) } @Test @@ -442,12 +471,15 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { ) } - val content = extractContent(entry) + val content = requireContent(entry) - assertThat(content).isNotNull() - assertThat(content?.style).isEqualTo(Style.BigText) - assertThat(content?.title).isEqualTo(TEST_BIG_CONTENT_TITLE) - assertThat(content?.text).isEqualTo(TEST_BIG_TEXT) + assertThat(content.privateVersion.style).isEqualTo(Style.BigText) + assertThat(content.privateVersion.title).isEqualTo(TEST_BIG_CONTENT_TITLE) + assertThat(content.privateVersion.text).isEqualTo(TEST_BIG_TEXT) + + assertThat(content.publicVersion.style).isEqualTo(Style.Base) + assertThat(content.publicVersion.title).isNull() + assertThat(content.publicVersion.text).isNull() } @Test @@ -464,12 +496,15 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { ) } - val content = extractContent(entry) + val content = requireContent(entry) - assertThat(content).isNotNull() - assertThat(content?.style).isEqualTo(Style.BigText) - assertThat(content?.title).isEqualTo(TEST_CONTENT_TITLE) - assertThat(content?.text).isEqualTo(TEST_BIG_TEXT) + assertThat(content.privateVersion.style).isEqualTo(Style.BigText) + assertThat(content.privateVersion.title).isEqualTo(TEST_CONTENT_TITLE) + assertThat(content.privateVersion.text).isEqualTo(TEST_BIG_TEXT) + + assertThat(content.publicVersion.style).isEqualTo(Style.Base) + assertThat(content.publicVersion.title).isNull() + assertThat(content.publicVersion.text).isNull() } @Test @@ -486,12 +521,15 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { ) } - val content = extractContent(entry) + val content = requireContent(entry) - assertThat(content).isNotNull() - assertThat(content?.style).isEqualTo(Style.BigText) - assertThat(content?.title).isEqualTo(TEST_BIG_CONTENT_TITLE) - assertThat(content?.text).isEqualTo(TEST_CONTENT_TEXT) + assertThat(content.privateVersion.style).isEqualTo(Style.BigText) + assertThat(content.privateVersion.title).isEqualTo(TEST_BIG_CONTENT_TITLE) + assertThat(content.privateVersion.text).isEqualTo(TEST_CONTENT_TEXT) + + assertThat(content.publicVersion.style).isEqualTo(Style.Base) + assertThat(content.publicVersion.title).isNull() + assertThat(content.publicVersion.text).isNull() } @Test @@ -506,11 +544,14 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { ) val entry = createEntry { setStyle(CallStyle.forOngoingCall(TEST_PERSON, hangUpIntent)) } - val content = extractContent(entry) + val content = requireContent(entry) - assertThat(content).isNotNull() - assertThat(content?.style).isEqualTo(Style.Call) - assertThat(content?.title).isEqualTo(TEST_PERSON_NAME) + assertThat(content.privateVersion.style).isEqualTo(Style.Call) + assertThat(content.privateVersion.title).isEqualTo(TEST_PERSON_NAME) + + assertThat(content.publicVersion.style).isEqualTo(Style.Base) + assertThat(content.publicVersion.title).isNull() + assertThat(content.publicVersion.text).isNull() } @Test @@ -524,13 +565,17 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { setStyle(ProgressStyle().addProgressSegment(Segment(100)).setProgress(75)) } - val content = extractContent(entry) + val content = requireContent(entry) - assertThat(content).isNotNull() - assertThat(content?.style).isEqualTo(Style.Progress) - assertThat(content?.newProgress).isNotNull() - assertThat(content?.newProgress?.progress).isEqualTo(75) - assertThat(content?.newProgress?.progressMax).isEqualTo(100) + assertThat(content.privateVersion.style).isEqualTo(Style.Progress) + val newProgress = assertNotNull(content.privateVersion.newProgress) + assertThat(newProgress.progress).isEqualTo(75) + assertThat(newProgress.progressMax).isEqualTo(100) + + assertThat(content.publicVersion.style).isEqualTo(Style.Base) + assertThat(content.publicVersion.title).isNull() + assertThat(content.publicVersion.text).isNull() + assertThat(content.publicVersion.newProgress).isNull() } @Test @@ -540,10 +585,11 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { setStyle(MessagingStyle(TEST_PERSON).addMessage("message text", 0L, TEST_PERSON)) } - val content = extractContent(entry) + val content = requireContent(entry) - assertThat(content).isNotNull() - assertThat(content?.style).isEqualTo(Style.Ineligible) + assertThat(content.privateVersion.style).isEqualTo(Style.Ineligible) + + assertThat(content.publicVersion.style).isEqualTo(Style.Ineligible) } @Test @@ -553,18 +599,13 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ false) } - val content = extractContent(entry) - - assertThat(content).isNotNull() + val content = requireContent(entry) - val oldProgress = content?.oldProgress - assertThat(oldProgress).isNotNull() + val oldProgress = assertNotNull(content.privateVersion.oldProgress) - assertThat(content).isNotNull() - assertThat(content?.oldProgress).isNotNull() - assertThat(content?.oldProgress?.progress).isEqualTo(TEST_PROGRESS) - assertThat(content?.oldProgress?.max).isEqualTo(TEST_PROGRESS_MAX) - assertThat(content?.oldProgress?.isIndeterminate).isFalse() + assertThat(oldProgress.progress).isEqualTo(TEST_PROGRESS) + assertThat(oldProgress.max).isEqualTo(TEST_PROGRESS_MAX) + assertThat(oldProgress.isIndeterminate).isFalse() } @Test @@ -574,18 +615,25 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ true) } - val content = extractContent(entry) + val content = requireContent(entry) + val oldProgress = assertNotNull(content.privateVersion.oldProgress) - assertThat(content).isNotNull() - assertThat(content?.oldProgress).isNotNull() - assertThat(content?.oldProgress?.progress).isEqualTo(TEST_PROGRESS) - assertThat(content?.oldProgress?.max).isEqualTo(TEST_PROGRESS_MAX) - assertThat(content?.oldProgress?.isIndeterminate).isTrue() + assertThat(oldProgress.progress).isEqualTo(TEST_PROGRESS) + assertThat(oldProgress.max).isEqualTo(TEST_PROGRESS_MAX) + assertThat(oldProgress.isIndeterminate).isTrue() } - private fun extractContent(entry: NotificationEntry): PromotedNotificationContentModel? { + private fun requireContent( + entry: NotificationEntry, + redactionType: Int = REDACTION_TYPE_PUBLIC, + ): PromotedNotificationContentModels = assertNotNull(extractContent(entry, redactionType)) + + private fun extractContent( + entry: NotificationEntry, + redactionType: Int = REDACTION_TYPE_PUBLIC, + ): PromotedNotificationContentModels? { val recoveredBuilder = Notification.Builder(context, entry.sbn.notification) - return underTest.extractContent(entry, recoveredBuilder, imageModelProvider) + return underTest.extractContent(entry, recoveredBuilder, redactionType, imageModelProvider) } private fun createEntry( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt index 6192399c522b..42c3f6603ad8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.promoted.domain.interactor +import android.app.Notification.FLAG_FOREGROUND_SERVICE +import android.app.Notification.FLAG_ONGOING_EVENT import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -25,15 +27,26 @@ import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.mediaprojection.data.model.MediaProjectionState +import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository +import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask +import com.android.systemui.screenrecord.data.model.ScreenRecordModel +import com.android.systemui.screenrecord.data.repository.screenRecordRepository +import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModelTest.Companion.createStatusBarIconViewOrNull import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.notification.collection.buildNotificationEntry import com.android.systemui.statusbar.notification.collection.buildOngoingCallEntry import com.android.systemui.statusbar.notification.collection.buildPromotedOngoingEntry +import com.android.systemui.statusbar.notification.data.model.activeNotificationModel +import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository +import com.android.systemui.statusbar.notification.data.repository.addNotif import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -76,6 +89,7 @@ class PromotedNotificationsInteractorTest : SysuiTestCase() { // THEN the order of the notification keys should be the call then the RON assertThat(orderedChipNotificationKeys) .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0") + .inOrder() } @Test @@ -96,6 +110,521 @@ class PromotedNotificationsInteractorTest : SysuiTestCase() { // THEN the order of the notification keys should be the call then the RON assertThat(orderedChipNotificationKeys) .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0") + .inOrder() + } + + @Test + fun orderedChipNotificationKeys_noScreenRecordNotif_isEmpty() = + kosmos.runTest { + screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg") + + renderNotificationListInteractor.setRenderedList(emptyList()) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys).isEmpty() + } + + @Test + fun orderedChipNotificationKeys_nullHostPackageForScreenRecord_isEmpty() = + kosmos.runTest { + screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + // hostPackage would be provided through mediaProjectionState + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.NotProjecting + + val entry = buildNotificationEntry(tag = "record", promoted = false) + renderNotificationListInteractor.setRenderedList(listOf(entry)) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys).isEmpty() + } + + @Test + fun orderedChipNotificationKeys_containsPromotedScreenRecordNotif() = + kosmos.runTest { + screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg") + + val screenRecordEntry = buildNotificationEntry(tag = "record", promoted = true) + renderNotificationListInteractor.setRenderedList(listOf(screenRecordEntry)) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|record|0") + .inOrder() + } + + @Test + fun orderedChipNotificationKeys_containsNotPromotedScreenRecordNotif_ifOngoing() = + kosmos.runTest { + screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg") + + val screenRecordEntry = + buildNotificationEntry(tag = "record", promoted = false) { + setFlag(context, FLAG_ONGOING_EVENT, true) + } + renderNotificationListInteractor.setRenderedList(listOf(screenRecordEntry)) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|record|0") + .inOrder() + } + + @Test + fun orderedChipNotificationKeys_containsNotPromotedScreenRecordNotif_ifFgs() = + kosmos.runTest { + screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg") + + val screenRecordEntry = + buildNotificationEntry(tag = "record", promoted = false) { + setFlag(context, FLAG_FOREGROUND_SERVICE, true) + } + renderNotificationListInteractor.setRenderedList(listOf(screenRecordEntry)) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|record|0") + .inOrder() + } + + @Test + fun orderedChipNotificationKeys_doesNotContainScreenRecordNotif_ifNotOngoingOrFgs() = + kosmos.runTest { + screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg") + + val screenRecordEntry = + buildNotificationEntry(tag = "record", promoted = false) { + setFlag(context, FLAG_ONGOING_EVENT, false) + setFlag(context, FLAG_FOREGROUND_SERVICE, false) + } + renderNotificationListInteractor.setRenderedList(listOf(screenRecordEntry)) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys).isEmpty() + } + + @Test + fun orderedChipNotificationKeys_containsFgsScreenRecordNotif_whenNonFgsNotifExists() = + kosmos.runTest { + screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg") + + val fgsEntry = + buildNotificationEntry(tag = "recordFgs", promoted = false) { + setFlag(context, FLAG_FOREGROUND_SERVICE, true) + } + val notFgsEntry = + buildNotificationEntry(tag = "recordNotFgs", promoted = false) { + setFlag(context, FLAG_FOREGROUND_SERVICE, false) + } + renderNotificationListInteractor.setRenderedList(listOf(fgsEntry, notFgsEntry)) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|recordFgs|0") + .inOrder() + } + + @Test + fun orderedChipNotificationKeys_containsOngoingScreenRecordNotif_whenNonOngoingNotifExists() = + kosmos.runTest { + screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg") + + val ongoingEntry = + buildNotificationEntry(tag = "recordOngoing", promoted = false) { + setFlag(context, FLAG_ONGOING_EVENT, true) + } + val notOngoingEntry = + buildNotificationEntry(tag = "recordNotOngoing", promoted = false) { + setFlag(context, FLAG_ONGOING_EVENT, false) + } + renderNotificationListInteractor.setRenderedList(listOf(notOngoingEntry, ongoingEntry)) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|recordOngoing|0") + .inOrder() + } + + @Test + fun orderedChipNotificationKeys_containsFgsOngoingScreenRecordNotif_whenNonFgsOngoingNotifExists() = + kosmos.runTest { + screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg") + + val ongoingAndFgsEntry = + buildNotificationEntry(tag = "recordBoth", promoted = false) { + setFlag(context, FLAG_FOREGROUND_SERVICE, true) + setFlag(context, FLAG_ONGOING_EVENT, true) + } + val ongoingButNotFgsEntry = + buildNotificationEntry(tag = "recordOngoing", promoted = false) { + setFlag(context, FLAG_ONGOING_EVENT, true) + setFlag(context, FLAG_FOREGROUND_SERVICE, false) + } + val fgsButNotOngoingEntry = + buildNotificationEntry(tag = "recordFgs", promoted = false) { + setFlag(context, FLAG_FOREGROUND_SERVICE, true) + setFlag(context, FLAG_ONGOING_EVENT, false) + } + renderNotificationListInteractor.setRenderedList( + listOf(fgsButNotOngoingEntry, ongoingButNotFgsEntry, ongoingAndFgsEntry) + ) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|recordBoth|0") + .inOrder() + } + + @Test + fun orderedChipNotificationKeys_twoEquivalentNotifsForScreenRecord_isEmpty() = + kosmos.runTest { + screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(hostPackage = "test_pkg") + + val entry1 = + buildNotificationEntry(tag = "entry1", promoted = false) { + setFlag(context, FLAG_FOREGROUND_SERVICE, true) + } + val entry2 = + buildNotificationEntry(tag = "entry2", promoted = false) { + setFlag(context, FLAG_FOREGROUND_SERVICE, true) + } + renderNotificationListInteractor.setRenderedList(listOf(entry1, entry2)) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys).isEmpty() + } + + @Test + fun orderedChipNotificationKeys_noMediProjNotif_isEmpty() = + kosmos.runTest { + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.SingleTask( + hostPackage = "test_pkg", + hostDeviceName = null, + createTask(taskId = 1), + ) + + renderNotificationListInteractor.setRenderedList(emptyList()) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys).isEmpty() + } + + @Test + fun orderedChipNotificationKeys_containsPromotedMediaProjNotif() = + kosmos.runTest { + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.SingleTask( + hostPackage = "test_pkg", + hostDeviceName = null, + createTask(taskId = 1), + ) + + val mediaProjEntry = buildNotificationEntry(tag = "proj", promoted = true) + renderNotificationListInteractor.setRenderedList(listOf(mediaProjEntry)) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys).containsExactly("0|test_pkg|0|proj|0").inOrder() + } + + @Test + fun orderedChipNotificationKeys_containsNotPromotedMediaProjNotif_ifOngoing() = + kosmos.runTest { + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.SingleTask( + hostPackage = "test_pkg", + hostDeviceName = null, + createTask(taskId = 1), + ) + + val mediaProjEntry = + buildNotificationEntry(tag = "proj", promoted = false) { + setFlag(context, FLAG_ONGOING_EVENT, true) + } + renderNotificationListInteractor.setRenderedList(listOf(mediaProjEntry)) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys).containsExactly("0|test_pkg|0|proj|0").inOrder() + } + + @Test + fun orderedChipNotificationKeys_containsNotPromotedMediaProjNotif_ifFgs() = + kosmos.runTest { + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.SingleTask( + hostPackage = "test_pkg", + hostDeviceName = null, + createTask(taskId = 1), + ) + + val mediaProjEntry = + buildNotificationEntry(tag = "proj", promoted = false) { + setFlag(context, FLAG_FOREGROUND_SERVICE, true) + } + renderNotificationListInteractor.setRenderedList(listOf(mediaProjEntry)) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys).containsExactly("0|test_pkg|0|proj|0").inOrder() + } + + @Test + fun orderedChipNotificationKeys_doesNotContainMediaProjNotif_ifNotOngoingOrFgs() = + kosmos.runTest { + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.SingleTask( + hostPackage = "test_pkg", + hostDeviceName = null, + createTask(taskId = 1), + ) + + val mediaProjEntry = + buildNotificationEntry(tag = "proj", promoted = false) { + setFlag(context, FLAG_ONGOING_EVENT, false) + setFlag(context, FLAG_FOREGROUND_SERVICE, false) + } + renderNotificationListInteractor.setRenderedList(listOf(mediaProjEntry)) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys).isEmpty() + } + + @Test + fun orderedChipNotificationKeys_containsFgsMediaProjNotif_whenNonFgsNotifExists() = + kosmos.runTest { + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.SingleTask( + hostPackage = "test_pkg", + hostDeviceName = null, + createTask(taskId = 1), + ) + + val fgsEntry = + buildNotificationEntry(tag = "projFgs", promoted = false) { + setFlag(context, FLAG_FOREGROUND_SERVICE, true) + } + val notFgsEntry = + buildNotificationEntry(tag = "projNotFgs", promoted = false) { + setFlag(context, FLAG_FOREGROUND_SERVICE, false) + } + renderNotificationListInteractor.setRenderedList(listOf(fgsEntry, notFgsEntry)) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|projFgs|0") + .inOrder() + } + + @Test + fun orderedChipNotificationKeys_containsOngoingMediaProjNotif_whenNonOngoingNotifExists() = + kosmos.runTest { + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.SingleTask( + hostPackage = "test_pkg", + hostDeviceName = null, + createTask(taskId = 1), + ) + + val ongoingEntry = + buildNotificationEntry(tag = "projOngoing", promoted = false) { + setFlag(context, FLAG_ONGOING_EVENT, true) + } + val notOngoingEntry = + buildNotificationEntry(tag = "projNotOngoing", promoted = false) { + setFlag(context, FLAG_ONGOING_EVENT, false) + } + renderNotificationListInteractor.setRenderedList(listOf(notOngoingEntry, ongoingEntry)) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|projOngoing|0") + .inOrder() + } + + @Test + fun orderedChipNotificationKeys_containsFgsOngoingMediaProjNotif_whenNonFgsOngoingNotifExists() = + kosmos.runTest { + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.SingleTask( + hostPackage = "test_pkg", + hostDeviceName = null, + createTask(taskId = 1), + ) + + val ongoingAndFgsEntry = + buildNotificationEntry(tag = "projBoth", promoted = false) { + setFlag(context, FLAG_FOREGROUND_SERVICE, true) + setFlag(context, FLAG_ONGOING_EVENT, true) + } + val ongoingButNotFgsEntry = + buildNotificationEntry(tag = "projOngoing", promoted = false) { + setFlag(context, FLAG_ONGOING_EVENT, true) + setFlag(context, FLAG_FOREGROUND_SERVICE, false) + } + val fgsButNotOngoingEntry = + buildNotificationEntry(tag = "projFgs", promoted = false) { + setFlag(context, FLAG_FOREGROUND_SERVICE, true) + setFlag(context, FLAG_ONGOING_EVENT, false) + } + renderNotificationListInteractor.setRenderedList( + listOf(fgsButNotOngoingEntry, ongoingButNotFgsEntry, ongoingAndFgsEntry) + ) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|projBoth|0") + .inOrder() + } + + @Test + fun orderedChipNotificationKeys_twoEquivalentNotifsForMediaProj_isEmpty() = + kosmos.runTest { + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.SingleTask( + hostPackage = "test_pkg", + hostDeviceName = null, + createTask(taskId = 1), + ) + + val entry1 = + buildNotificationEntry(tag = "entry1", promoted = false) { + setFlag(context, FLAG_FOREGROUND_SERVICE, true) + } + val entry2 = + buildNotificationEntry(tag = "entry2", promoted = false) { + setFlag(context, FLAG_FOREGROUND_SERVICE, true) + } + renderNotificationListInteractor.setRenderedList(listOf(entry1, entry2)) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys).isEmpty() + } + + @Test + fun orderedChipNotificationKeys_maintainsPromotedNotifOrder() = + kosmos.runTest { + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif1", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = PromotedNotificationContentBuilder("notif1").build(), + ) + ) + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif2", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = PromotedNotificationContentBuilder("notif2").build(), + ) + ) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys).containsExactly("notif1", "notif2").inOrder() + } + + // The ranking between different chips should stay consistent between + // PromotedNotificationsInteractor and OngoingActivityChipsViewModel. + // See OngoingActivityChipsWithNotifsViewModelTest#chips_screenRecordAndCallAndPromotedNotifs + // test for the right ranking. + @Test + fun orderedChipNotificationKeys_rankingIsCorrect() = + kosmos.runTest { + // Screen record + screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + fakeMediaProjectionRepository.mediaProjectionState.value = + MediaProjectionState.Projecting.SingleTask( + hostPackage = "screen.record.package", + hostDeviceName = null, + createTask(taskId = 1), + ) + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "screenRecordKey", + packageName = "screen.record.package", + isOngoingEvent = true, + ) + ) + // Call + addOngoingCallState(key = "callKey") + // Other promoted notifs + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif1", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = PromotedNotificationContentBuilder("notif1").build(), + ) + ) + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif2", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = PromotedNotificationContentBuilder("notif2").build(), + ) + ) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + assertThat(orderedChipNotificationKeys) + .containsExactly("screenRecordKey", "callKey", "notif1", "notif2") + .inOrder() } @Test @@ -114,8 +643,7 @@ class PromotedNotificationsInteractorTest : SysuiTestCase() { collectLastValue(underTest.aodPromotedNotification) // THEN the ron is first because the call has no content - assertThat(topPromotedNotificationContent?.identity?.key) - .isEqualTo("0|test_pkg|0|ron|0") + assertThat(topPromotedNotificationContent?.key).isEqualTo("0|test_pkg|0|ron|0") } @Test @@ -134,8 +662,7 @@ class PromotedNotificationsInteractorTest : SysuiTestCase() { collectLastValue(underTest.aodPromotedNotification) // THEN the call is the top notification - assertThat(topPromotedNotificationContent?.identity?.key) - .isEqualTo("0|test_pkg|0|call|0") + assertThat(topPromotedNotificationContent?.key).isEqualTo("0|test_pkg|0|call|0") } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index 99f2596dbf1d..19b1046f1931 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -67,11 +67,11 @@ import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; -import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder; +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; @@ -389,8 +389,8 @@ public class NotificationContentInflaterTest extends SysuiTestCase { @Test @DisableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME}) public void testExtractsPromotedContent_notWhenBothFlagsDisabled() throws Exception { - final PromotedNotificationContentModel content = - new PromotedNotificationContentModel.Builder("key").build(); + final PromotedNotificationContentModels content = + new PromotedNotificationContentBuilder("key").build(); mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content); inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); @@ -401,43 +401,43 @@ public class NotificationContentInflaterTest extends SysuiTestCase { @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME) @DisableFlags(StatusBarNotifChips.FLAG_NAME) - public void testExtractsPromotedContent_whenPromotedNotificationUiFlagEnabled() + public void testExtractsPromotedContent_whePromotedNotificationUiFlagEnabled() throws Exception { - final PromotedNotificationContentModel content = - new PromotedNotificationContentModel.Builder("key").build(); + final PromotedNotificationContentModels content = + new PromotedNotificationContentBuilder("key").build(); mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content); inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); mPromotedNotificationContentExtractor.verifyOneExtractCall(); - assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel()); + assertEquals(content, mRow.getEntry().getPromotedNotificationContentModels()); } @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) @DisableFlags(PromotedNotificationUi.FLAG_NAME) public void testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() throws Exception { - final PromotedNotificationContentModel content = - new PromotedNotificationContentModel.Builder("key").build(); + final PromotedNotificationContentModels content = + new PromotedNotificationContentBuilder("key").build(); mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content); inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); mPromotedNotificationContentExtractor.verifyOneExtractCall(); - assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel()); + assertEquals(content, mRow.getEntry().getPromotedNotificationContentModels()); } @Test @EnableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME}) public void testExtractsPromotedContent_whenBothFlagsEnabled() throws Exception { - final PromotedNotificationContentModel content = - new PromotedNotificationContentModel.Builder("key").build(); + final PromotedNotificationContentModels content = + new PromotedNotificationContentBuilder("key").build(); mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content); inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); mPromotedNotificationContentExtractor.verifyOneExtractCall(); - assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel()); + assertEquals(content, mRow.getEntry().getPromotedNotificationContentModels()); } @Test @@ -448,7 +448,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); mPromotedNotificationContentExtractor.verifyOneExtractCall(); - assertNull(mRow.getEntry().getPromotedNotificationContentModel()); + assertNull(mRow.getEntry().getPromotedNotificationContentModels()); } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt index 0ac5fe95957c..16663def16a4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt @@ -67,8 +67,8 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB import com.android.systemui.statusbar.notification.promoted.domain.interactor.PackageDemotionInteractor import com.android.systemui.statusbar.notification.row.icon.AppIconProvider import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider -import com.android.systemui.statusbar.notification.row.icon.appIconProvider -import com.android.systemui.statusbar.notification.row.icon.notificationIconStyleProvider +import com.android.systemui.statusbar.notification.row.icon.mockAppIconProvider +import com.android.systemui.statusbar.notification.row.icon.mockNotificationIconStyleProvider import com.android.systemui.testKosmos import com.android.telecom.telecomManager import com.google.common.truth.Truth.assertThat @@ -80,6 +80,7 @@ import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock @@ -101,6 +102,8 @@ class NotificationInfoTest : SysuiTestCase() { private lateinit var entry: NotificationEntry private val mockPackageManager = kosmos.mockPackageManager + private val mockAppIconProvider = kosmos.mockAppIconProvider + private val mockIconStyleProvider = kosmos.mockNotificationIconStyleProvider private val uiEventLogger = kosmos.uiEventLoggerFake private val testableLooper by lazy { kosmos.testableLooper } @@ -202,7 +205,8 @@ class NotificationInfoTest : SysuiTestCase() { } @Test - fun testBindNotification_SetsPackageIcon() { + @DisableFlags(com.android.systemui.Flags.FLAG_NOTIFICATIONS_REDESIGN_GUTS) + fun testBindNotification_SetsPackageIcon_flagOff() { val iconDrawable = mock<Drawable>() whenever(mockPackageManager.getApplicationIcon(any<ApplicationInfo>())) .thenReturn(iconDrawable) @@ -212,6 +216,26 @@ class NotificationInfoTest : SysuiTestCase() { } @Test + @EnableFlags(com.android.systemui.Flags.FLAG_NOTIFICATIONS_REDESIGN_GUTS) + fun testBindNotification_SetsPackageIcon_flagOn() { + val iconDrawable = mock<Drawable>() + whenever(mockIconStyleProvider.shouldShowWorkProfileBadge(anyOrNull(), anyOrNull())) + .thenReturn(false) + whenever( + mockAppIconProvider.getOrFetchAppIcon( + anyOrNull(), + anyOrNull(), + anyBoolean(), + anyBoolean(), + ) + ) + .thenReturn(iconDrawable) + bindNotification() + val iconView = underTest.findViewById<ImageView>(R.id.pkg_icon) + assertThat(iconView.drawable).isEqualTo(iconDrawable) + } + + @Test fun testBindNotification_noDelegate() { bindNotification() val nameView = underTest.findViewById<TextView>(R.id.delegate_name) @@ -894,8 +918,8 @@ class NotificationInfoTest : SysuiTestCase() { private fun bindNotification( pm: PackageManager = this.mockPackageManager, iNotificationManager: INotificationManager = this.mockINotificationManager, - appIconProvider: AppIconProvider = kosmos.appIconProvider, - iconStyleProvider: NotificationIconStyleProvider = kosmos.notificationIconStyleProvider, + appIconProvider: AppIconProvider = this.mockAppIconProvider, + iconStyleProvider: NotificationIconStyleProvider = this.mockIconStyleProvider, onUserInteractionCallback: OnUserInteractionCallback = this.onUserInteractionCallback, channelEditorDialogController: ChannelEditorDialogController = this.channelEditorDialogController, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java index 95366568a37a..5ad4a4fab056 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java @@ -103,46 +103,6 @@ public class NotificationMenuRowTest extends LeakCheckedTest { row.resetMenu(); } - - @Test - public void testNoAppOpsInSlowSwipe() { - when(mRow.getShowSnooze()).thenReturn(false); - Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0); - - NotificationMenuRow row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier); - row.createMenu(mRow); - - ViewGroup container = (ViewGroup) row.getMenuView(); - // noti blocking - assertEquals(1, container.getChildCount()); - } - - @Test - public void testNoSnoozeInSlowSwipe() { - when(mRow.getShowSnooze()).thenReturn(false); - Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0); - - NotificationMenuRow row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier); - row.createMenu(mRow); - - ViewGroup container = (ViewGroup) row.getMenuView(); - // just for noti blocking - assertEquals(1, container.getChildCount()); - } - - @Test - public void testSnoozeInSlowSwipe() { - when(mRow.getShowSnooze()).thenReturn(true); - Settings.Global.putInt(mContext.getContentResolver(), SHOW_NEW_NOTIF_DISMISS, 0); - - NotificationMenuRow row = new NotificationMenuRow(mContext, mPeopleNotificationIdentifier); - row.createMenu(mRow); - - ViewGroup container = (ViewGroup) row.getMenuView(); - // one for snooze and one for noti blocking - assertEquals(2, container.getChildCount()); - } - @Test public void testSlowSwipe_newDismiss() { when(mRow.getShowSnooze()).thenReturn(true); @@ -237,6 +197,7 @@ public class NotificationMenuRowTest extends LeakCheckedTest { new NotificationMenuRow(mContext, mPeopleNotificationIdentifier)); doReturn(30f).when(row).getSnapBackThreshold(); doReturn(50f).when(row).getDismissThreshold(); + doReturn(70).when(row).getSpaceForMenu(); when(row.isMenuOnLeft()).thenReturn(true); when(row.getTranslation()).thenReturn(40f); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt index 063a04ab9f37..dcba3e447dda 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt @@ -44,7 +44,7 @@ import com.android.systemui.statusbar.notification.ConversationNotificationProce import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED @@ -456,7 +456,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @Test @DisableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) fun testExtractsPromotedContent_notWhenBothFlagsDisabled() { - val content = PromotedNotificationContentModel.Builder("key").build() + val content = PromotedNotificationContentBuilder("key").build() promotedNotificationContentExtractor.resetForEntry(row.entry, content) inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) @@ -468,38 +468,38 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @EnableFlags(PromotedNotificationUi.FLAG_NAME) @DisableFlags(StatusBarNotifChips.FLAG_NAME) fun testExtractsPromotedContent_whenPromotedNotificationUiFlagEnabled() { - val content = PromotedNotificationContentModel.Builder("key").build() + val content = PromotedNotificationContentBuilder("key").build() promotedNotificationContentExtractor.resetForEntry(row.entry, content) inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) promotedNotificationContentExtractor.verifyOneExtractCall() - Assert.assertEquals(content, row.entry.promotedNotificationContentModel) + Assert.assertEquals(content, row.entry.promotedNotificationContentModels) } @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) @DisableFlags(PromotedNotificationUi.FLAG_NAME) fun testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() { - val content = PromotedNotificationContentModel.Builder("key").build() + val content = PromotedNotificationContentBuilder("key").build() promotedNotificationContentExtractor.resetForEntry(row.entry, content) inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) promotedNotificationContentExtractor.verifyOneExtractCall() - Assert.assertEquals(content, row.entry.promotedNotificationContentModel) + Assert.assertEquals(content, row.entry.promotedNotificationContentModels) } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) fun testExtractsPromotedContent_whenBothFlagsEnabled() { - val content = PromotedNotificationContentModel.Builder("key").build() + val content = PromotedNotificationContentBuilder("key").build() promotedNotificationContentExtractor.resetForEntry(row.entry, content) inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) promotedNotificationContentExtractor.verifyOneExtractCall() - Assert.assertEquals(content, row.entry.promotedNotificationContentModel) + Assert.assertEquals(content, row.entry.promotedNotificationContentModels) } @Test @@ -510,7 +510,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) promotedNotificationContentExtractor.verifyOneExtractCall() - Assert.assertNull(row.entry.promotedNotificationContentModel) + Assert.assertNull(row.entry.promotedNotificationContentModels) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt index 936b971c889b..f52f96efb9d1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt @@ -139,7 +139,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { underTest.setMagneticRowTranslation(swipedRow, translation = 100f) // WHEN setting a translation that will fall below the threshold - val translation = threshold / underTest.swipedRowMultiplier - 50f + val translation = 50f underTest.setMagneticRowTranslation(swipedRow, translation) // THEN the targets continue to be pulled and translations are set @@ -162,7 +162,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { underTest.setMagneticRowTranslation(swipedRow, translation = 100f) // WHEN setting a translation that will fall below the threshold - val translation = threshold / underTest.swipedRowMultiplier - 50f + val translation = 50f underTest.setMagneticRowTranslation(swipedRow, translation) // THEN the targets continue to be pulled and reduced translations are set @@ -185,7 +185,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { underTest.setMagneticRowTranslation(swipedRow, translation = 100f) // WHEN setting a translation that will fall above the threshold - val translation = threshold / underTest.swipedRowMultiplier + 50f + val translation = 150f underTest.setMagneticRowTranslation(swipedRow, translation) // THEN the swiped view detaches and the correct detach haptics play @@ -208,7 +208,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { underTest.setMagneticRowTranslation(swipedRow, translation = 100f) // WHEN setting a translation that will fall above the threshold - val translation = threshold / underTest.swipedRowMultiplier + 50f + val translation = 150f underTest.setMagneticRowTranslation(swipedRow, translation) // THEN the swiped view does not detach and the reduced translation is set @@ -355,7 +355,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { underTest.setMagneticRowTranslation(swipedRow, translation = 100f) // Set a translation that will fall above the threshold - val translation = threshold / underTest.swipedRowMultiplier + 50f + val translation = 150f underTest.setMagneticRowTranslation(swipedRow, translation) assertThat(underTest.currentState).isEqualTo(State.DETACHED) 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 1ea41de63e64..716353945be2 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 @@ -186,7 +186,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { .thenReturn(false); mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT, BiometricSourceType.FINGERPRINT, true /* isStrongBiometric */); - verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean()); + verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean(), + eq("BiometricUnlockController#MODE_SHOW_BOUNCER")); verify(mStatusBarKeyguardViewManager, never()).notifyKeyguardAuthenticated(anyBoolean()); assertThat(mBiometricUnlockController.getMode()) .isEqualTo(BiometricUnlockController.MODE_SHOW_BOUNCER); @@ -198,7 +199,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { .thenReturn(false); mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT, BiometricSourceType.FINGERPRINT, false /* isStrongBiometric */); - verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean()); + verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean(), + eq("BiometricUnlockController#MODE_SHOW_BOUNCER")); assertThat(mBiometricUnlockController.getMode()) .isEqualTo(BiometricUnlockController.MODE_SHOW_BOUNCER); assertThat(mBiometricUnlockController.getBiometricType()) @@ -248,7 +250,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT, BiometricSourceType.FINGERPRINT, true /* isStrongBiometric */); - verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean()); + verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean(), + eq("BiometricUnlockController#MODE_SHOW_BOUNCER")); verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(eq(false)); assertThat(mBiometricUnlockController.getMode()) .isEqualTo(BiometricUnlockController.MODE_UNLOCK_COLLAPSING); @@ -327,7 +330,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT, BiometricSourceType.FACE, true /* isStrongBiometric */); - verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean()); + verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean(), + eq("BiometricUnlockController#MODE_SHOW_BOUNCER")); assertThat(mBiometricUnlockController.getMode()) .isEqualTo(BiometricUnlockController.MODE_SHOW_BOUNCER); } @@ -359,7 +363,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT, BiometricSourceType.FACE, true /* isStrongBiometric */); - verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean()); + verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean(), + eq("BiometricUnlockController#MODE_SHOW_BOUNCER")); assertThat(mBiometricUnlockController.getMode()) .isEqualTo(BiometricUnlockController.MODE_NONE); } @@ -438,17 +443,20 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { // WHEN udfps fails once - then don't show the bouncer yet mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); - verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean()); + verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean(), + eq("BiometricUnlockController#MODE_SHOW_BOUNCER")); // WHEN udfps fails the second time - then don't show the bouncer yet mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); - verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean()); + verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean(), + eq("BiometricUnlockController#MODE_SHOW_BOUNCER")); // WHEN udpfs fails the third time mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); // THEN show the bouncer - verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true); + verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true, + "BiometricUnlockController#MODE_SHOW_BOUNCER"); } @Test @@ -460,14 +468,16 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); - verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean()); + verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean(), + eq("BiometricUnlockController#MODE_SHOW_BOUNCER")); // WHEN lockout is received mBiometricUnlockController.onBiometricError(FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, "Lockout", BiometricSourceType.FINGERPRINT); // THEN show bouncer - verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true); + verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true, + "BiometricUnlockController#MODE_SHOW_BOUNCER"); } @Test @@ -544,7 +554,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { BiometricSourceType.FINGERPRINT, true /* isStrongBiometric */); // THEN shows primary bouncer - verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean()); + verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean(), + eq("BiometricUnlockController#MODE_SHOW_BOUNCER")); } @Test @@ -554,7 +565,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { BiometricSourceType.FACE, false /* isStrongBiometric */); // THEN shows primary bouncer - verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean()); + verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean(), + eq("BiometricUnlockController#MODE_SHOW_BOUNCER")); } private void givenFingerprintModeUnlockCollapsing() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java index 1cc291199531..d9e256228428 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java @@ -124,7 +124,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { mRemoteInputCallback.onLockedRemoteInput( mock(ExpandableNotificationRow.class), mock(View.class)); - verify(mStatusBarKeyguardViewManager).showBouncer(true); + verify(mStatusBarKeyguardViewManager).showBouncer(true, + "StatusBarRemoteInputCallback#onLockedRemoteInput"); } @Test @DisableFlags(ExpandHeadsUpOnInlineReply.FLAG_NAME) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt index c58b4bc9953c..18074d53e87b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt @@ -40,7 +40,7 @@ import com.android.systemui.statusbar.notification.data.model.activeNotification import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.notification.shared.CallType import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository @@ -170,7 +170,7 @@ class OngoingCallControllerTest : SysuiTestCase() { @Test fun interactorHasOngoingCallNotif_repoHasPromotedContent() = testScope.runTest { - val promotedContent = PromotedNotificationContentModel.Builder("ongoingNotif").build() + val promotedContent = PromotedNotificationContentBuilder("ongoingNotif").build() setNotifOnRepo( activeNotificationModel( key = "ongoingNotif", diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt index 84f1d5cd4895..c071327ae398 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt @@ -29,7 +29,7 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository import com.android.systemui.statusbar.gesture.swipeStatusBarAwayGestureHandler -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentBuilder import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState @@ -75,7 +75,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { val startTimeMs = 1000L val testIconView: StatusBarIconView = mock() val testIntent: PendingIntent = mock() - val testPromotedContent = PromotedNotificationContentModel.Builder(key).build() + val testPromotedContent = PromotedNotificationContentBuilder(key).build() addOngoingCallState( key = key, startTimeMs = startTimeMs, @@ -106,7 +106,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { val startTimeMs = 1000L val testIconView: StatusBarIconView = mock() val testIntent: PendingIntent = mock() - val testPromotedContent = PromotedNotificationContentModel.Builder(key).build() + val testPromotedContent = PromotedNotificationContentBuilder(key).build() addOngoingCallState( key = key, startTimeMs = startTimeMs, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/IconManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/IconManagerTest.kt index 90732d0183d2..318eb87f500b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/IconManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/IconManagerTest.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalKairosApi::class) + package com.android.systemui.statusbar.phone.ui import android.app.Flags @@ -28,13 +30,17 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.statusbar.StatusBarIcon import com.android.systemui.SysuiTestCase +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.KairosNetwork import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter import com.android.systemui.util.Assert import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -58,7 +64,10 @@ class IconManagerTest : SysuiTestCase() { StatusBarLocation.HOME, mock<WifiUiAdapter>(defaultAnswer = RETURNS_DEEP_STUBS), mock<MobileUiAdapter>(defaultAnswer = RETURNS_DEEP_STUBS), + { mock<MobileUiAdapterKairos>(defaultAnswer = RETURNS_DEEP_STUBS) }, mock<MobileContextProvider>(defaultAnswer = RETURNS_DEEP_STUBS), + mock<KairosNetwork>(defaultAnswer = RETURNS_DEEP_STUBS), + mock<CoroutineScope>(defaultAnswer = RETURNS_DEEP_STUBS), ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerTest.java index 891ff38764fe..8e3117f47f86 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerTest.java @@ -35,6 +35,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.statusbar.StatusBarIcon; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; +import com.android.systemui.kairos.KairosNetwork; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarIconView; @@ -45,12 +46,15 @@ import com.android.systemui.statusbar.phone.StatusBarLocation; import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry; import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos; import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.tuner.TunerService; import com.android.systemui.utils.leaks.LeakCheckedTest; +import kotlinx.coroutines.CoroutineScope; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -76,7 +80,9 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { public void testSetCalledOnAdd_IconManager() { LinearLayout layout = new LinearLayout(mContext); TestIconManager manager = - new TestIconManager(layout, mMobileUiAdapter, mMobileContextProvider); + new TestIconManager(layout, mMobileUiAdapter, mMobileContextProvider, + mock(MobileUiAdapterKairos.class), mock( + KairosNetwork.class), mock(CoroutineScope.class)); testCallOnAdd_forManager(manager); } @@ -89,7 +95,9 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { mock(WifiUiAdapter.class), mMobileUiAdapter, mMobileContextProvider, - mock(DarkIconDispatcher.class)); + mock(DarkIconDispatcher.class), + mock(MobileUiAdapterKairos.class), mock(KairosNetwork.class), + mock(CoroutineScope.class)); testCallOnAdd_forManager(manager); } @@ -139,12 +147,18 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { WifiUiAdapter wifiUiAdapter, MobileUiAdapter mobileUiAdapter, MobileContextProvider contextProvider, - DarkIconDispatcher darkIconDispatcher) { + DarkIconDispatcher darkIconDispatcher, + MobileUiAdapterKairos mobileUiAdapterKairos, + KairosNetwork kairosNetwork, + CoroutineScope appScope) { super(group, location, wifiUiAdapter, mobileUiAdapter, + () -> mobileUiAdapterKairos, contextProvider, + kairosNetwork, + appScope, darkIconDispatcher); } @@ -167,13 +181,19 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { TestIconManager( ViewGroup group, MobileUiAdapter adapter, - MobileContextProvider contextProvider + MobileContextProvider contextProvider, + MobileUiAdapterKairos adapterKairos, + KairosNetwork kairosNetwork, + CoroutineScope appScope ) { super(group, StatusBarLocation.HOME, mock(WifiUiAdapter.class), adapter, - contextProvider); + () -> adapterKairos, + contextProvider, + kairosNetwork, + appScope); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt index 9e914ad0a660..9e914ad0a660 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt index 18a124cf362e..033503f9ad8e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt @@ -52,6 +52,7 @@ import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABL import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_HALF_OPEN import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.SCREEN_EVENT_TIMEOUT import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent +import com.android.systemui.unfold.data.repository.ScreenTimeoutPolicyRepository import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor import com.android.systemui.unfoldedDeviceState @@ -97,6 +98,8 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { private val animationStatusRepository = kosmos.fakeAnimationStatusRepository private val keyguardInteractor = mock<KeyguardInteractor>() private val displaySwitchLatencyLogger = mock<DisplaySwitchLatencyLogger>() + private val screenTimeoutPolicyRepository = mock<ScreenTimeoutPolicyRepository>() + private val screenTimeoutActive = MutableStateFlow(true) private val latencyTracker = mock<LatencyTracker>() private val deviceStateManager = kosmos.deviceStateManager @@ -136,6 +139,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { whenever(resources.getIntArray(R.array.config_foldedDeviceStates)) .thenReturn(nonEmptyClosedDeviceStatesArray) whenever(keyguardInteractor.isAodAvailable).thenReturn(isAodAvailable) + whenever(screenTimeoutPolicyRepository.screenTimeoutActive).thenReturn(screenTimeoutActive) animationStatusRepository.onAnimationStatusChanged(true) powerInteractor.setAwakeForTest() powerInteractor.setScreenPowerState(SCREEN_ON) @@ -144,6 +148,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { mockContext, foldStateRepository, powerInteractor, + screenTimeoutPolicyRepository, unfoldTransitionInteractor, animationStatusRepository, keyguardInteractor, @@ -196,6 +201,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { mockContext, foldStateRepository, powerInteractor, + screenTimeoutPolicyRepository, unfoldTransitionInteractorWithEmptyProgressProvider, animationStatusRepository, keyguardInteractor, @@ -625,6 +631,44 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { } } + @Test + fun displaySwitch_screenTimeoutActive_logsNoScreenWakelocks() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + screenTimeoutActive.value = true + + startUnfolding() + advanceTimeBy(100.milliseconds) + finishUnfolding() + + val event = capturedLogEvent() + assertThat(event.screenWakelockStatus) + .isEqualTo( + SysUiStatsLog + .DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_NO_WAKELOCKS + ) + } + } + + @Test + fun displaySwitch_screenTimeoutNotActive_logsHasScreenWakelocks() { + testScope.runTest { + startInFoldedState(displaySwitchLatencyTracker) + screenTimeoutActive.value = false + + startUnfolding() + advanceTimeBy(100.milliseconds) + finishUnfolding() + + val event = capturedLogEvent() + assertThat(event.screenWakelockStatus) + .isEqualTo( + SysUiStatsLog + .DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_HAS_SCREEN_WAKELOCKS + ) + } + } + private fun capturedLogEvent(): DisplaySwitchLatencyEvent { verify(displaySwitchLatencyLogger).log(capture(loggerArgumentCaptor)) return loggerArgumentCaptor.value @@ -662,6 +706,9 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() { fromFoldableDeviceState = fromFoldableDeviceState, toFoldableDeviceState = toFoldableDeviceState, toState = toState, + screenWakelockStatus = + SysUiStatsLog + .DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_NO_WAKELOCKS, trackingResult = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TRACKING_RESULT__SUCCESS, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt index 3eada258f616..07706414393b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt @@ -27,6 +27,8 @@ import android.graphics.drawable.Drawable import android.os.Process import android.os.UserHandle import android.os.UserManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -34,6 +36,7 @@ import com.android.internal.logging.UiEventLogger import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.Flags as AConfigFlags +import com.android.systemui.Flags.FLAG_USER_SWITCHER_ADD_SIGN_OUT_OPTION import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.SysuiTestCase @@ -68,6 +71,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertNotNull +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runCurrent @@ -101,10 +105,13 @@ class UserSwitcherInteractorTest : SysuiTestCase() { @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var userLogoutInteractor: UserLogoutInteractor private val kosmos = testKosmos() + private val logoutEnabledStateFlow = MutableStateFlow<Boolean>(false) private val testScope = kosmos.testScope private lateinit var spyContext: Context + private lateinit var userRepository: FakeUserRepository private lateinit var keyguardReply: KeyguardInteractorFactory.WithDependencies private lateinit var keyguardRepository: FakeKeyguardRepository @@ -118,6 +125,8 @@ class UserSwitcherInteractorTest : SysuiTestCase() { whenever(manager.getUserIcon(anyInt())).thenReturn(ICON) whenever(manager.canAddMoreUsers(any())).thenReturn(true) + whenever(userLogoutInteractor.isLogoutEnabled).thenReturn(logoutEnabledStateFlow) + overrideResource(com.android.settingslib.R.drawable.ic_account_circle, GUEST_ICON) overrideResource(R.dimen.max_avatar_size, 10) overrideResource( @@ -493,6 +502,42 @@ class UserSwitcherInteractorTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_USER_SWITCHER_ADD_SIGN_OUT_OPTION) + fun actions_logoutEnabled_flagDisabled_signOutIsNotShown() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 1, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false)) + keyguardRepository.setKeyguardShowing(true) + logoutEnabledStateFlow.value = true + + val value = collectLastValue(underTest.actions) + + assertThat(value()).isEqualTo(emptyList<UserActionModel>()) + } + } + + @Test + @EnableFlags(FLAG_USER_SWITCHER_ADD_SIGN_OUT_OPTION) + fun actions_logoutEnabled_flagEnabled_signOutIsShown() { + createUserInteractor() + testScope.runTest { + val userInfos = createUserInfos(count = 1, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false)) + keyguardRepository.setKeyguardShowing(true) + logoutEnabledStateFlow.value = true + + val value = collectLastValue(underTest.actions) + + assertThat(value()).isEqualTo(listOf(UserActionModel.SIGN_OUT)) + } + } + + @Test fun executeAction_addUser_dismissesDialogAndStartsActivity() { createUserInteractor() testScope.runTest { @@ -569,14 +614,23 @@ class UserSwitcherInteractorTest : SysuiTestCase() { verify(uiEventLogger, times(1)) .log(MultiUserActionsEvent.CREATE_GUEST_FROM_USER_SWITCHER) assertThat(dialogRequests) - .contains( - ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true), - ) + .contains(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true)) verify(activityManager).switchUser(guestUserInfo.id) } } @Test + fun executeAction_signOut() { + createUserInteractor() + testScope.runTest { + underTest.executeAction(UserActionModel.SIGN_OUT) + runCurrent() + + verify(userLogoutInteractor).logOut() + } + } + + @Test fun selectUser_alreadySelectedGuestReSelected_exitGuestDialog() { createUserInteractor() testScope.runTest { @@ -739,7 +793,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( spyContext, - Intent(Intent.ACTION_LOCALE_CHANGED) + Intent(Intent.ACTION_LOCALE_CHANGED), ) runCurrent() @@ -972,7 +1026,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { 50, "Work Profile", /* iconPath= */ "", - /* flags= */ UserInfo.FLAG_MANAGED_PROFILE + /* flags= */ UserInfo.FLAG_MANAGED_PROFILE, ) ) userRepository.setUserInfos(userInfos) @@ -1010,7 +1064,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { userRepository.setSettings( UserSwitcherSettingsModel( isUserSwitcherEnabled = true, - isAddUsersFromLockscreen = true + isAddUsersFromLockscreen = true, ) ) @@ -1034,7 +1088,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { userRepository.setSettings( UserSwitcherSettingsModel( isUserSwitcherEnabled = true, - isAddUsersFromLockscreen = true + isAddUsersFromLockscreen = true, ) ) @@ -1068,7 +1122,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { whenever( manager.hasUserRestrictionForUser( UserManager.DISALLOW_ADD_USER, - UserHandle.of(id) + UserHandle.of(id), ) ) .thenReturn(true) @@ -1170,7 +1224,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { whenever( manager.hasUserRestrictionForUser( UserManager.DISALLOW_ADD_USER, - UserHandle.of(0) + UserHandle.of(0), ) ) .thenReturn(true) @@ -1195,7 +1249,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { model = model, id = index, isSelected = index == selectedIndex, - isGuest = includeGuest && index == count - 1 + isGuest = includeGuest && index == count - 1, ) } } @@ -1263,14 +1317,12 @@ class UserSwitcherInteractorTest : SysuiTestCase() { assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled) } - private fun assertRecordForAction( - record: UserRecord, - type: UserActionModel, - ) { + private fun assertRecordForAction(record: UserRecord, type: UserActionModel) { assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE) assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER) assertThat(record.isAddSupervisedUser) .isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER) + assertThat(record.isSignOut).isEqualTo(type === UserActionModel.SIGN_OUT) } private fun createUserInteractor(startAsProcessUser: Boolean = true) { @@ -1317,13 +1369,11 @@ class UserSwitcherInteractorTest : SysuiTestCase() { featureFlags = kosmos.fakeFeatureFlagsClassic, userRestrictionChecker = mock(), processWrapper = kosmos.processWrapper, + userLogoutInteractor = userLogoutInteractor, ) } - private fun createUserInfos( - count: Int, - includeGuest: Boolean, - ): List<UserInfo> { + private fun createUserInfos(count: Int, includeGuest: Boolean): List<UserInfo> { return (0 until count).map { index -> val isGuest = includeGuest && index == count - 1 createUserInfo( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt index 5d51c6d16c5a..d51e66d6f3b0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt @@ -44,9 +44,12 @@ import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.domain.interactor.GuestUserInteractor import com.android.systemui.user.domain.interactor.HeadlessSystemUserMode import com.android.systemui.user.domain.interactor.RefreshUsersScheduler +import com.android.systemui.user.domain.interactor.UserLogoutInteractor import com.android.systemui.user.domain.interactor.UserSwitcherInteractor 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.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.toList @@ -78,13 +81,13 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var userLogoutInteractor: UserLogoutInteractor private lateinit var underTest: StatusBarUserChipViewModel private val userRepository = FakeUserRepository() private lateinit var guestUserInteractor: GuestUserInteractor private lateinit var refreshUsersScheduler: RefreshUsersScheduler - private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -92,6 +95,9 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) + val logoutEnabledStateFlow = MutableStateFlow<Boolean>(false) + whenever(userLogoutInteractor.isLogoutEnabled).thenReturn(logoutEnabledStateFlow) + doAnswer { invocation -> val userId = invocation.arguments[0] as Int when (userId) { @@ -251,9 +257,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { headlessSystemUserMode = headlessSystemUserMode, applicationScope = testScope.backgroundScope, telephonyInteractor = - TelephonyInteractor( - repository = FakeTelephonyRepository(), - ), + TelephonyInteractor(repository = FakeTelephonyRepository()), broadcastDispatcher = fakeBroadcastDispatcher, keyguardUpdateMonitor = keyguardUpdateMonitor, backgroundDispatcher = testDispatcher, @@ -263,7 +267,8 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { guestUserInteractor = guestUserInteractor, uiEventLogger = uiEventLogger, userRestrictionChecker = mock(), - processWrapper = ProcessWrapperFake(activityManager) + processWrapper = ProcessWrapperFake(activityManager), + userLogoutInteractor = userLogoutInteractor, ) ) } @@ -293,7 +298,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { USER_NAME_0.text!!, /* iconPath */ "", /* flags */ UserInfo.FLAG_FULL, - /* userType */ UserManager.USER_TYPE_FULL_SYSTEM + /* userType */ UserManager.USER_TYPE_FULL_SYSTEM, ) private val USER_1 = @@ -302,7 +307,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { USER_NAME_1.text!!, /* iconPath */ "", /* flags */ UserInfo.FLAG_FULL, - /* userType */ UserManager.USER_TYPE_FULL_SYSTEM + /* userType */ UserManager.USER_TYPE_FULL_SYSTEM, ) private val USER_2 = @@ -311,7 +316,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { USER_NAME_2.text!!, /* iconPath */ "", /* flags */ UserInfo.FLAG_FULL, - /* userType */ UserManager.USER_TYPE_FULL_SYSTEM + /* userType */ UserManager.USER_TYPE_FULL_SYSTEM, ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index 8ff088f5d29b..087ccb83afe5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -44,6 +44,7 @@ import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.domain.interactor.GuestUserInteractor import com.android.systemui.user.domain.interactor.HeadlessSystemUserMode import com.android.systemui.user.domain.interactor.RefreshUsersScheduler +import com.android.systemui.user.domain.interactor.UserLogoutInteractor import com.android.systemui.user.domain.interactor.UserSwitcherInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.shared.model.UserActionModel @@ -51,6 +52,7 @@ 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.MutableStateFlow import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -79,6 +81,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var userLogoutInteractor: UserLogoutInteractor private lateinit var underTest: UserSwitcherViewModel @@ -94,6 +97,10 @@ class UserSwitcherViewModelTest : SysuiTestCase() { whenever(manager.canAddMoreUsers(any())).thenReturn(true) whenever(manager.getUserSwitchability(any())) .thenReturn(UserManager.SWITCHABILITY_STATUS_OK) + + val logoutEnabledStateFlow = MutableStateFlow<Boolean>(false) + whenever(userLogoutInteractor.isLogoutEnabled).thenReturn(logoutEnabledStateFlow) + overrideResource( com.android.internal.R.string.config_supervisedUserCreationPackage, SUPERVISED_USER_CREATION_PACKAGE, @@ -113,15 +120,11 @@ class UserSwitcherViewModelTest : SysuiTestCase() { UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL, UserManager.USER_TYPE_FULL_SYSTEM, - ), + ) ) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) - userRepository.setSettings( - UserSwitcherSettingsModel( - isUserSwitcherEnabled = true, - ) - ) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) } val refreshUsersScheduler = @@ -163,9 +166,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { headlessSystemUserMode = headlessSystemUserMode, applicationScope = testScope.backgroundScope, telephonyInteractor = - TelephonyInteractor( - repository = FakeTelephonyRepository(), - ), + TelephonyInteractor(repository = FakeTelephonyRepository()), broadcastDispatcher = fakeBroadcastDispatcher, keyguardUpdateMonitor = keyguardUpdateMonitor, backgroundDispatcher = testDispatcher, @@ -175,7 +176,8 @@ class UserSwitcherViewModelTest : SysuiTestCase() { guestUserInteractor = guestUserInteractor, uiEventLogger = uiEventLogger, userRestrictionChecker = mock(), - processWrapper = ProcessWrapperFake(activityManager) + processWrapper = ProcessWrapperFake(activityManager), + userLogoutInteractor = userLogoutInteractor, ), guestUserInteractor = guestUserInteractor, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt index 3da4f29a6fcb..dc344aa1a66f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt @@ -73,7 +73,7 @@ class WindowRootViewModelTest : SysuiTestCase() { assertThat(blurRadius).isEqualTo(0f) - kosmos.windowRootViewBlurRepository.blurRadius.value = 60 + kosmos.windowRootViewBlurRepository.blurRequestedByShade.value = 60 runCurrent() assertThat(blurRadius).isEqualTo(0f) diff --git a/packages/SystemUI/res/drawable/unpin_icon.xml b/packages/SystemUI/res/drawable/unpin_icon.xml new file mode 100644 index 000000000000..4e2e15893884 --- /dev/null +++ b/packages/SystemUI/res/drawable/unpin_icon.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M680,120L680,200L640,200L640,527L560,447L560,200L400,200L400,287L313,200L280,167L280,167L280,120L680,120ZM480,920L440,880L440,640L240,640L240,560L320,480L320,434L56,168L112,112L848,848L790,904L526,640L520,640L520,880L480,920ZM354,560L446,560L402,516L400,514L354,560ZM480,367L480,367L480,367L480,367ZM402,516L402,516L402,516L402,516Z"/> +</vector> diff --git a/packages/SystemUI/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml b/packages/SystemUI/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml index aec204f45aa7..7f6dc49505bb 100644 --- a/packages/SystemUI/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml +++ b/packages/SystemUI/res/drawable/vector_drawable_progress_indeterminate_horizontal_trimmed.xml @@ -38,7 +38,7 @@ <path android:name="rect" android:pathData="M -144.0,-5.0 l 288.0,0 l 0,10.0 l -288.0,0 Z" - android:fillColor="?androidprv:attr/colorAccentPrimaryVariant" /> + android:fillColor="@androidprv:color/materialColorPrimary" /> </group> </group> </vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/activity_rear_display_enabled.xml b/packages/SystemUI/res/layout/activity_rear_display_enabled.xml index f900626b4da6..6b633e03f1f2 100644 --- a/packages/SystemUI/res/layout/activity_rear_display_enabled.xml +++ b/packages/SystemUI/res/layout/activity_rear_display_enabled.xml @@ -56,6 +56,7 @@ android:gravity="center_horizontal" /> <TextView + android:id="@+id/seekbar_instructions" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/rear_display_unfolded_front_screen_on_slide_to_cancel" @@ -73,4 +74,13 @@ android:background="@null" android:gravity="center_horizontal" /> + <Button + android:id="@+id/cancel_button" + android:text="@string/cancel" + android:layout_width="@dimen/rear_display_animation_width_opened" + android:layout_height="wrap_content" + android:gravity="center_horizontal" + android:visibility="gone" + style="@style/Widget.Dialog.Button.BorderButton"/> + </LinearLayout> diff --git a/packages/SystemUI/res/layout/promoted_permission_guts.xml b/packages/SystemUI/res/layout/promoted_permission_guts.xml new file mode 100644 index 000000000000..50e5ae3c05ed --- /dev/null +++ b/packages/SystemUI/res/layout/promoted_permission_guts.xml @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2017, The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<com.android.systemui.statusbar.notification.row.PromotedPermissionGutsContent + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingTop="2dp" + android:paddingBottom="2dp" + android:background="@androidprv:color/materialColorSurfaceContainerHigh" + android:theme="@style/Theme.SystemUI" + > + + <RelativeLayout + android:id="@+id/promoted_guts" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="@dimen/notification_2025_min_height"> + + <ImageView + android:id="@+id/unpin_icon" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:src="@drawable/unpin_icon" + android:layout_alignParentTop="true" + android:layout_centerHorizontal="true" + android:padding="@dimen/notification_importance_button_padding" + /> + + <TextView + android:id="@+id/demote_explain" + android:layout_width="400dp" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_below="@id/unpin_icon" + android:layout_toLeftOf="@id/undo" + android:padding="@*android:dimen/notification_content_margin_end" + android:textColor="@androidprv:color/materialColorOnSurface" + android:minWidth="@dimen/min_clickable_item_size" + android:minHeight="@dimen/min_clickable_item_size" + style="@style/TextAppearance.NotificationInfo.Button" /> + + <TextView + android:id="@+id/undo" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/unpin_icon" + android:layout_alignParentRight="true" + android:padding="@*android:dimen/notification_content_margin_end" + android:textColor="@androidprv:color/materialColorOnSurface" + android:minWidth="@dimen/min_clickable_item_size" + android:minHeight="@dimen/min_clickable_item_size" + android:text="@string/snooze_undo" + style="@style/TextAppearance.NotificationInfo.Button" /> + </RelativeLayout> + +</com.android.systemui.statusbar.notification.row.PromotedPermissionGutsContent> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index d0ae307b6919..7d983068f34e 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -989,7 +989,7 @@ <dimen name="keyguard_security_container_padding_top">20dp</dimen> - <dimen name="keyguard_translate_distance_on_swipe_up">-200dp</dimen> + <dimen name="keyguard_translate_distance_on_swipe_up">-180dp</dimen> <dimen name="keyguard_indication_margin_bottom">32dp</dimen> <dimen name="ambient_indication_margin_bottom">71dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index b627bdf22a6c..681bd53f1a40 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2551,6 +2551,9 @@ <!-- Label for header of customize QS [CHAR LIMIT=60] --> <string name="drag_to_rearrange_tiles">Hold and drag to rearrange tiles</string> + <!-- Label for placing tiles in edit mode for QS [CHAR LIMIT=60] --> + <string name="tap_to_position_tile">Tap to position tile</string> + <!-- Label for area where tiles can be dragged in to [CHAR LIMIT=60] --> <string name="drag_to_remove_tiles">Drag here to remove</string> @@ -2592,6 +2595,12 @@ <!-- Accessibility description of action to remove QS tile on click. It will read as "Double-tap to remove tile" in screen readers [CHAR LIMIT=NONE] --> <string name="accessibility_qs_edit_remove_tile_action">remove tile</string> + <!-- Accessibility description of action to select the QS tile to place on click. It will read as "Double-tap to toggle placement mode" in screen readers [CHAR LIMIT=NONE] --> + <string name="accessibility_qs_edit_toggle_placement_mode">toggle placement mode</string> + + <!-- Accessibility description of action to toggle the QS tile selection. It will read as "Double-tap to toggle selection" in screen readers [CHAR LIMIT=NONE] --> + <string name="accessibility_qs_edit_toggle_selection">toggle selection</string> + <!-- Accessibility action of action to add QS tile to end. It will read as "Double-tap to add tile to the last position" in screen readers [CHAR LIMIT=NONE] --> <string name="accessibility_qs_edit_tile_add_action">add tile to the last position</string> @@ -4200,6 +4209,12 @@ All Quick Settings tiles will reset to the device’s original settings </string> + + <!-- Content of the Reset Tiles dialog in QS Edit mode. [CHAR LIMIT=NONE] --> + <string name="demote_explain_text"> + <xliff:g id="application" example= "Superfast Food Delivery">%1$s</xliff:g> will no longer show Live Updates here. You can change this any time in Settings. + </string> + <!-- Template that joins disabled message with the label for the voice over. [CHAR LIMIT=NONE] --> <string name="volume_slider_disabled_message_template"><xliff:g example="Notification" id="stream_name">%1$s</xliff:g>, <xliff:g example="Disabled because ring is muted" id="disabled_message">%2$s</xliff:g></string> </resources> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java index ea7321627322..b8cd5bec2cbe 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java @@ -208,4 +208,19 @@ public class PreviewPositionHelper { } mMatrix.postTranslate(translateX, translateY); } + + /** + * A factory that returns a new instance of the {@link PreviewPositionHelper}. + * <p>{@link PreviewPositionHelper} is a stateful helper, and hence when using it in distinct + * scenarios, prefer fetching an object using this factory</p> + * <p>Additionally, helpful for injecting mocks in tests</p> + */ + public static class PreviewPositionHelperFactory { + /** + * Returns a new {@link PreviewPositionHelper} for use in a distinct scenario. + */ + public PreviewPositionHelper create() { + return new PreviewPositionHelper(); + } + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java index 892851cd7056..8a307145023d 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java @@ -160,7 +160,7 @@ public interface KeyguardViewController { /** * Shows the primary bouncer. */ - void showPrimaryBouncer(boolean scrimmed); + void showPrimaryBouncer(boolean scrimmed, String reason); /** * When the primary bouncer is fully visible or is showing but animation didn't finish yet. diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/scrim/BouncerScrimController.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/scrim/BouncerScrimController.java index 6f2dd799c409..633c13e9e94d 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/scrim/BouncerScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/scrim/BouncerScrimController.java @@ -34,7 +34,7 @@ public class BouncerScrimController implements ScrimController { @Override public void show(boolean scrimmed) { - mStatusBarKeyguardViewManager.showPrimaryBouncer(scrimmed); + mStatusBarKeyguardViewManager.showPrimaryBouncer(scrimmed, "BouncerScrimController#show"); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java index da1c1bc49d23..2d44d401b0b0 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java @@ -130,6 +130,8 @@ public class AssistManager { AssistUtils.INVOCATION_TYPE_POWER_BUTTON_LONG_PRESS; public static final int INVOCATION_TYPE_NAV_HANDLE_LONG_PRESS = AssistUtils.INVOCATION_TYPE_NAV_HANDLE_LONG_PRESS; + public static final int INVOCATION_TYPE_LAUNCHER_SYSTEM_SHORTCUT = + AssistUtils.INVOCATION_TYPE_LAUNCHER_SYSTEM_SHORTCUT; public static final int DISMISS_REASON_INVOCATION_CANCELLED = 1; public static final int DISMISS_REASON_TAP = 2; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index dfe8eb28b2a6..659d3b46fea9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -880,7 +880,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { Log.v(TAG, "aod lock icon long-press rejected by the falsing manager."); return; } - mKeyguardViewManager.showPrimaryBouncer(true); + mKeyguardViewManager.showPrimaryBouncer(true, "UdfpsController#onAodInterrupt"); // play the same haptic as the DeviceEntryIcon longpress if (mOverlay != null && mOverlay.getTouchOverlay() != null) { diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt index 0c6d7920d7f3..48e08fcd90c5 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt @@ -135,7 +135,7 @@ constructor( // TODO(b/243695312): Encapsulate all of the show logic for the bouncer. /** Show the bouncer if necessary and set the relevant states. */ @JvmOverloads - fun show(isScrimmed: Boolean): Boolean { + fun show(isScrimmed: Boolean, reason: String): Boolean { // When the scene container framework is enabled, instead of calling this, call // SceneInteractor#changeScene(Scenes.Bouncer, ...). SceneContainerFlag.assertInLegacyMode() @@ -176,6 +176,7 @@ constructor( return false } + Log.i(TAG, "Show primary bouncer requested, reason: $reason") repository.setPrimaryShowingSoon(true) if (usePrimaryBouncerPassiveAuthDelay()) { Log.d(TAG, "delay bouncer, passive auth may succeed") diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/compose/gestures/EagerTap.kt b/packages/SystemUI/src/com/android/systemui/common/ui/compose/gestures/EagerTap.kt new file mode 100644 index 000000000000..078ea569a63c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/ui/compose/gestures/EagerTap.kt @@ -0,0 +1,91 @@ +/* + * 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.common.ui.compose.gestures + +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerInputScope +import kotlinx.coroutines.coroutineScope + +/** + * Detects taps and double taps without waiting for the double tap minimum delay in between + * + * Using [detectTapGestures] with both a single tap and a double tap defined will send only one of + * these event per user interaction. This variant will send the single tap at all times, with the + * optional double tap if the user pressed a second time in a short period of time. + * + * Warning: Use this only if you know that reporting a single tap followed by a double tap won't be + * a problem in your use case. + * + * @param doubleTapEnabled whether this should listen for double tap events. This value is captured + * at the first down movement. + * @param onDoubleTap the double tap callback + * @param onTap the single tap callback + */ +suspend fun PointerInputScope.detectEagerTapGestures( + doubleTapEnabled: () -> Boolean, + onDoubleTap: (Offset) -> Unit, + onTap: () -> Unit, +) = coroutineScope { + awaitEachGesture { + val down = awaitFirstDown() + down.consume() + + // Capture whether double tap is enabled on first down as this state can change following + // the first tap + val isDoubleTapEnabled = doubleTapEnabled() + + // wait for first tap up or long press + val upOrCancel = waitForUpOrCancellation() + + if (upOrCancel != null) { + // tap was successful. + upOrCancel.consume() + onTap.invoke() + + if (isDoubleTapEnabled) { + // check for second tap + val secondDown = + withTimeoutOrNull(viewConfiguration.doubleTapTimeoutMillis) { + val minUptime = + upOrCancel.uptimeMillis + viewConfiguration.doubleTapMinTimeMillis + var change: PointerInputChange + // The second tap doesn't count if it happens before DoubleTapMinTime of the + // first tap + do { + change = awaitFirstDown() + } while (change.uptimeMillis < minUptime) + change + } + + if (secondDown != null) { + // Second tap down detected + + // Might have a long second press as the second tap + val secondUp = waitForUpOrCancellation() + if (secondUp != null) { + secondUp.consume() + onDoubleTap(secondUp.position) + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalLockIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalLockIconViewModel.kt index 19eeabd98c88..931639c8b247 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalLockIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalLockIconViewModel.kt @@ -130,7 +130,9 @@ constructor( if (SceneContainerFlag.isEnabled) { deviceEntryInteractor.attemptDeviceEntry() } else { - keyguardViewController.get().showPrimaryBouncer(/* scrim */ true) + keyguardViewController + .get() + .showPrimaryBouncer(/* scrim */ true, "CommunalLockIconViewModel#onUserInteraction") } deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 6db2ebc0df2c..099a7f067482 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -140,6 +140,7 @@ import com.android.systemui.animation.TransitionAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor; +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor; import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; @@ -364,6 +365,7 @@ public class KeyguardViewMediator implements CoreStartable, private final Lazy<NotificationShadeDepthController> mNotificationShadeDepthController; private final Lazy<ShadeController> mShadeController; private final Lazy<CommunalSceneInteractor> mCommunalSceneInteractor; + private final Lazy<CommunalSettingsInteractor> mCommunalSettingsInteractor; /* * Records the user id on request to go away, for validation when WM calls back to start the * exit animation. @@ -1567,6 +1569,7 @@ public class KeyguardViewMediator implements CoreStartable, KeyguardInteractor keyguardInteractor, KeyguardTransitionBootInteractor transitionBootInteractor, Lazy<CommunalSceneInteractor> communalSceneInteractor, + Lazy<CommunalSettingsInteractor> communalSettingsInteractor, WindowManagerOcclusionManager wmOcclusionManager) { mContext = context; mUserTracker = userTracker; @@ -1609,6 +1612,7 @@ public class KeyguardViewMediator implements CoreStartable, mKeyguardInteractor = keyguardInteractor; mTransitionBootInteractor = transitionBootInteractor; mCommunalSceneInteractor = communalSceneInteractor; + mCommunalSettingsInteractor = communalSettingsInteractor; mStatusBarStateController = statusBarStateController; statusBarStateController.addCallback(this); @@ -2429,9 +2433,18 @@ public class KeyguardViewMediator implements CoreStartable, private void doKeyguardLocked(Bundle options) { // If the power button behavior requests to open the glanceable hub. if (options != null && options.getBoolean(EXTRA_TRIGGER_HUB)) { - // Set the hub to show immediately when the SysUI window shows, then continue to lock - // the device. - mCommunalSceneInteractor.get().showHubFromPowerButton(); + if (mCommunalSettingsInteractor.get().getAutoOpenEnabled().getValue()) { + // Set the hub to show immediately when the SysUI window shows, then continue to + // lock the device. + mCommunalSceneInteractor.get().showHubFromPowerButton(); + } else { + // If the hub is not available, go to sleep instead of locking. This can happen + // because the power button behavior does not check all possible reasons the hub + // might be disabled. + mPM.goToSleep(android.os.SystemClock.uptimeMillis(), + PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, 0); + return; + } } int currentUserId = mSelectedUserInteractor.getSelectedUserId(); @@ -3765,13 +3778,7 @@ public class KeyguardViewMediator implements CoreStartable, Log.d(TAG, "Status bar manager is disabled for visible background users"); } } else { - try { - mStatusBarService.disableForUser(flags, mStatusBarDisableToken, - mContext.getPackageName(), - mSelectedUserInteractor.getSelectedUserId()); - } catch (RemoteException e) { - Log.d(TAG, "Failed to force clear flags", e); - } + statusBarServiceDisableForUser(flags, "Failed to force clear flags"); } } @@ -3807,18 +3814,29 @@ public class KeyguardViewMediator implements CoreStartable, // Handled in StatusBarDisableFlagsInteractor. if (!KeyguardWmStateRefactor.isEnabled()) { - try { - mStatusBarService.disableForUser(flags, mStatusBarDisableToken, - mContext.getPackageName(), - mSelectedUserInteractor.getSelectedUserId()); - } catch (RemoteException e) { - Log.d(TAG, "Failed to set disable flags: " + flags, e); - } + statusBarServiceDisableForUser(flags, "Failed to set disable flags: "); } } } } + private void statusBarServiceDisableForUser(int flags, String loggingContext) { + Runnable runnable = () -> { + try { + mStatusBarService.disableForUser(flags, mStatusBarDisableToken, + mContext.getPackageName(), + mSelectedUserInteractor.getSelectedUserId()); + } catch (RemoteException e) { + Log.d(TAG, loggingContext + " " + flags, e); + } + }; + if (com.android.systemui.Flags.bouncerUiRevamp()) { + mUiBgExecutor.execute(runnable); + } else { + runnable.run(); + } + } + /** * Handle message sent by {@link #resetStateLocked} * @see #RESET @@ -4099,12 +4117,23 @@ public class KeyguardViewMediator implements CoreStartable, || aodShowing != mAodShowing || forceCallbacks; mShowing = showing; mAodShowing = aodShowing; - if (notifyDefaultDisplayCallbacks) { - notifyDefaultDisplayCallbacks(showing); - } - if (updateActivityLockScreenState) { - updateActivityLockScreenState(showing, aodShowing, reason); + + if (KeyguardWmReorderAtmsCalls.isEnabled()) { + if (updateActivityLockScreenState) { + updateActivityLockScreenState(showing, aodShowing, reason); + } + if (notifyDefaultDisplayCallbacks) { + notifyDefaultDisplayCallbacks(showing); + } + } else { + if (notifyDefaultDisplayCallbacks) { + notifyDefaultDisplayCallbacks(showing); + } + if (updateActivityLockScreenState) { + updateActivityLockScreenState(showing, aodShowing, reason); + } } + } private void notifyDefaultDisplayCallbacks(boolean showing) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmReorderAtmsCalls.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmReorderAtmsCalls.kt new file mode 100644 index 000000000000..7ac52813ff71 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmReorderAtmsCalls.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the keyguard wm state refactor flag state. */ +@Suppress("NOTHING_TO_INLINE") +object KeyguardWmReorderAtmsCalls { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_KEYGUARD_WM_REORDER_ATMS_CALLS + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.keyguardWmReorderAtmsCalls() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt index ddccc5d9e96d..41d14b9e727f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt @@ -20,7 +20,16 @@ import com.android.systemui.Flags import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils -/** Helper for reading or using the keyguard wm state refactor flag state. */ +/** + * Helper for reading or using the keyguard_wm_state_refactor flag state. + * + * keyguard_wm_state_refactor works both with and without flexiglass (scene_container), but + * flexiglass requires keyguard_wm_state_refactor. For this reason, this class will return isEnabled + * if either keyguard_wm_state_refactor OR scene_container are enabled. This enables us to roll out + * keyguard_wm_state_refactor independently of scene_container, while also ensuring that + * scene_container rolling out ahead of keyguard_wm_state_refactor causes code gated by + * KeyguardWmStateRefactor to be enabled as well. + */ @Suppress("NOTHING_TO_INLINE") object KeyguardWmStateRefactor { /** The aconfig flag name */ @@ -30,10 +39,9 @@ object KeyguardWmStateRefactor { val token: FlagToken get() = FlagToken(FLAG_NAME, isEnabled) - /** Is the refactor enabled */ @JvmStatic inline val isEnabled - get() = Flags.keyguardWmStateRefactor() + get() = Flags.keyguardWmStateRefactor() || Flags.sceneContainer() /** * Called to ensure code is only run when the flag is enabled. This protects users from the diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 6b1248b6983e..1fe6eb9ce7c8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -42,6 +42,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.classifier.FalsingModule; import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor; +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor; import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; @@ -182,6 +183,7 @@ public interface KeyguardModule { KeyguardInteractor keyguardInteractor, KeyguardTransitionBootInteractor transitionBootInteractor, Lazy<CommunalSceneInteractor> communalSceneInteractor, + Lazy<CommunalSettingsInteractor> communalSettingsInteractor, WindowManagerOcclusionManager windowManagerOcclusionManager) { return new KeyguardViewMediator( context, @@ -234,6 +236,7 @@ public interface KeyguardModule { keyguardInteractor, transitionBootInteractor, communalSceneInteractor, + communalSettingsInteractor, windowManagerOcclusionManager); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index f8c7a86687dd..f4e804ac5abf 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -24,6 +24,7 @@ import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dagger.SysUISingleton +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.keyguard.KeyguardWmStateRefactor @@ -62,6 +63,7 @@ constructor( override val internalTransitionInteractor: InternalKeyguardTransitionInteractor, transitionInteractor: KeyguardTransitionInteractor, @Background private val scope: CoroutineScope, + @Application private val applicationScope: CoroutineScope, @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, keyguardInteractor: KeyguardInteractor, @@ -175,7 +177,7 @@ constructor( private fun listenForLockscreenToPrimaryBouncerDragging() { if (SceneContainerFlag.isEnabled) return var transitionId: UUID? = null - scope.launch("$TAG#listenForLockscreenToPrimaryBouncerDragging") { + applicationScope.launch("$TAG#listenForLockscreenToPrimaryBouncerDragging") { shadeRepository.legacyShadeExpansion.collect { shadeExpansion -> val statusBarState = keyguardInteractor.statusBarState.value val isKeyguardUnlocked = keyguardInteractor.isKeyguardDismissible.value @@ -204,7 +206,7 @@ constructor( id, // This maps the shadeExpansion to a much faster curve, to match // the existing logic - 1f - MathUtils.constrainedMap(0f, 1f, 0.95f, 1f, shadeExpansion), + 1f - MathUtils.constrainedMap(0f, 1f, 0.88f, 1f, shadeExpansion), nextState, ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt index 0a4022ad4de8..e6f8406726f0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractor.kt @@ -159,7 +159,10 @@ constructor( if (alternateBouncerInteractor.canShowAlternateBouncer.value) { alternateBouncerInteractor.forceShow() } else { - primaryBouncerInteractor.show(true) + primaryBouncerInteractor.show( + true, + "KeyguardDismissInteractor#dismissKeyguardWithCallback", + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 7977000ed5c8..2d5ff61a5015 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -19,7 +19,6 @@ import android.app.StatusBarManager import android.graphics.Point import android.util.Log import android.util.MathUtils -import com.android.app.animation.Interpolators import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor @@ -371,9 +370,11 @@ constructor( currentKeyguardState == LOCKSCREEN && legacyShadeExpansion != 1f ) { - emit(MathUtils.constrainedMap(0f, 1f, 0.95f, 1f, legacyShadeExpansion)) + emit(MathUtils.constrainedMap(0f, 1f, 0.82f, 1f, legacyShadeExpansion)) } else if ( - (legacyShadeExpansion == 0f || legacyShadeExpansion == 1f) && !onGlanceableHub + !onGlanceableHub && + isKeyguardDismissible && + (legacyShadeExpansion == 0f || legacyShadeExpansion == 1f) ) { // Resets alpha state emit(1f) @@ -401,15 +402,7 @@ constructor( // 0f and 1f need to be ignored in the legacy shade expansion. These can // flip arbitrarily as the legacy shade is reset, and would cause the // translation value to jump around unexpectedly. - emit( - MathUtils.lerp( - translationDistance, - 0, - Interpolators.FAST_OUT_LINEAR_IN.getInterpolation( - legacyShadeExpansion - ), - ) - ) + emit(MathUtils.lerp(translationDistance, 0, legacyShadeExpansion)) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt index 6d9b276031e9..ced96e93d87d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt @@ -136,7 +136,10 @@ constructor( return true } StatusBarState.KEYGUARD -> { - statusBarKeyguardViewManager.showPrimaryBouncer(true) + statusBarKeyguardViewManager.showPrimaryBouncer( + true, + "KeyguardKeyEventInteractor#collapseShadeLockedOrShowPrimaryBouncer", + ) return true } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index af58d10f3066..df58b215167a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -20,6 +20,8 @@ package com.android.systemui.keyguard.domain.interactor import android.annotation.SuppressLint import android.util.Log import com.android.app.tracing.coroutines.flow.filterTraced +import com.android.app.tracing.coroutines.flow.shareInTraced +import com.android.app.tracing.coroutines.flow.stateInTraced import com.android.app.tracing.coroutines.flow.traceAs import com.android.app.tracing.coroutines.launchTraced as launch import com.android.app.tracing.coroutines.traceCoroutine @@ -64,8 +66,6 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.shareIn -import kotlinx.coroutines.flow.stateIn /** Encapsulates business-logic related to the keyguard transitions. */ @SysUISingleton @@ -102,12 +102,18 @@ constructor( val transitions = repository.transitions val transitionState: StateFlow<TransitionStep> = - transitions.stateIn(scope, SharingStarted.Eagerly, TransitionStep()) + transitions.stateInTraced( + "KTF-transitionState", + scope, + SharingStarted.Eagerly, + TransitionStep(), + ) private val sceneTransitionPair = sceneInteractor.transitionState .pairwise() - .stateIn( + .stateInTraced( + "KTF-sceneTransitionPair", scope, SharingStarted.Eagerly, WithPrev( @@ -130,11 +136,16 @@ constructor( repository.transitions .pairwise() .filter { it.newValue.transitionState == TransitionState.STARTED } - .shareIn(scope, SharingStarted.Eagerly, replay = 1) + .shareInTraced( + "KTF-startedStepWithPrecedingStep", + scope, + SharingStarted.Eagerly, + replay = 1, + ) init { // Collect non-canceled steps and emit transition values. - scope.launch { + scope.launch("KTF-update-non-canceled") { repository.transitions .filter { it.transitionState != TransitionState.CANCELED } .collect { step -> @@ -145,7 +156,7 @@ constructor( } } - scope.launch { + scope.launch("KTF-update-transitionMap") { repository.transitions.collect { // FROM->TO transitionMap[Edge.create(it.from, it.to)]?.emit(it) @@ -160,7 +171,7 @@ constructor( // need to ensure we emit transitionValue(A) = 0f, since no further steps will be emitted // where the from or to states are A. This would leave transitionValue(A) stuck at an // arbitrary non-zero value. - scope.launch { + scope.launch("KTF-update-canceled") { startedStepWithPrecedingStep.collect { (prevStep, startedStep) -> if ( prevStep.transitionState == TransitionState.CANCELED && @@ -180,7 +191,7 @@ constructor( // Safety: When any transition is FINISHED, ensure all other transitionValue flows other // than the FINISHED state are reset to a value of 0f. There have been rare but severe // bugs that get the device stuck in a bad state when these are not properly reset. - scope.launch { + scope.launch("KTF-update-finished") { repository.transitions .filter { it.transitionState == TransitionState.FINISHED } .collect { @@ -201,7 +212,7 @@ constructor( * If the screen is turning off, finish the current transition immediately. Further * frames won't be visible anyway. */ - scope.launch { + scope.launch("KTF-force-finish") { powerInteractor.screenPowerState .filter { it == ScreenPowerState.SCREEN_TURNING_OFF } .collect { repository.forceFinishCurrentTransition() } @@ -347,6 +358,7 @@ constructor( } } } + .traceAs("KTF-transition-simulator") /** * This function is similar to flatMapLatest but it will additionally emit a FINISHED @@ -372,7 +384,7 @@ constructor( traceCoroutine("cancelAndJoin") { job?.cancelAndJoin() } job = - launch("inner") { + launch("KTF-flatMapLatestWithFinished") { val innerFlow = transform(value) try { innerFlow.collect { step -> @@ -398,7 +410,6 @@ constructor( } } } - .traceAs("flatMapLatestWithFinished") /** * Converts old KTF states to UNDEFINED when [SceneContainerFlag] is enabled. @@ -451,7 +462,12 @@ constructor( val startedKeyguardTransitionStep: StateFlow<TransitionStep> = repository.transitions .filter { step -> step.transitionState == TransitionState.STARTED } - .stateIn(scope, SharingStarted.Eagerly, TransitionStep()) + .stateInTraced( + "KTF-startedKeyguardTransitionStep", + scope, + SharingStarted.Eagerly, + TransitionStep(), + ) /** * The [KeyguardState] we're currently in. @@ -517,7 +533,7 @@ constructor( it.from } } - .stateIn(scope, SharingStarted.Eagerly, OFF) + .stateInTraced("KTF-currentKeyguardState", scope, SharingStarted.Eagerly, OFF) val isInTransition = combine(isInTransitionWhere({ true }, { true }), sceneInteractor.transitionState) { @@ -629,7 +645,7 @@ constructor( repository.transitions .filter { it.transitionState == TransitionState.FINISHED } .map { it.to } - .stateIn(scope, SharingStarted.Eagerly, OFF) + .stateInTraced("KTF-finishedKeyguardState", scope, SharingStarted.Eagerly, OFF) companion object { private val TAG = KeyguardTransitionInteractor::class.simpleName diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt index 68d595ebf0b6..b4e9d8296a74 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt @@ -196,39 +196,50 @@ constructor( .distinctUntilChanged() } - private val lockscreenVisibilityWithScenes = - combine( - sceneInteractor.get().transitionState.flatMapLatestConflated { - when (it) { - is Idle -> { - when (it.currentScene) { - in keyguardContent -> flowOf(true) - in nonKeyguardContent -> flowOf(false) - in keyguardAgnosticContent -> isDeviceNotEnteredDirectly - else -> - throw IllegalStateException("Unknown scene: ${it.currentScene}") - } - } - is Transition -> { - when { - it.isTransitioningSets(from = keyguardContent) -> flowOf(true) - it.isTransitioningSets(from = nonKeyguardContent) -> flowOf(false) - it.isTransitioningSets(from = keyguardAgnosticContent) -> - isDeviceNotEnteredDirectly - else -> - throw IllegalStateException( - "Unknown content: ${it.fromContent}" - ) + private val lockscreenVisibilityWithScenes: Flow<Boolean> = + // The scene container visibility into account as that will be forced to false when the + // device isn't yet provisioned (e.g. still in the setup wizard). + sceneInteractor.get().isVisible.flatMapLatestConflated { isVisible -> + if (isVisible) { + combine( + sceneInteractor.get().transitionState.flatMapLatestConflated { + when (it) { + is Idle -> + when (it.currentScene) { + in keyguardContent -> flowOf(true) + in nonKeyguardContent -> flowOf(false) + in keyguardAgnosticContent -> isDeviceNotEnteredDirectly + else -> + throw IllegalStateException( + "Unknown scene: ${it.currentScene}" + ) + } + is Transition -> + when { + it.isTransitioningSets(from = keyguardContent) -> + flowOf(true) + it.isTransitioningSets(from = nonKeyguardContent) -> + flowOf(false) + it.isTransitioningSets(from = keyguardAgnosticContent) -> + isDeviceNotEnteredDirectly + else -> + throw IllegalStateException( + "Unknown content: ${it.fromContent}" + ) + } } - } + }, + wakeToGoneInteractor.canWakeDirectlyToGone, + ::Pair, + ) + .map { (lockscreenVisibilityByTransitionState, canWakeDirectlyToGone) -> + lockscreenVisibilityByTransitionState && !canWakeDirectlyToGone } - }, - wakeToGoneInteractor.canWakeDirectlyToGone, - ::Pair, - ) - .map { (lockscreenVisibilityByTransitionState, canWakeDirectlyToGone) -> - lockscreenVisibilityByTransitionState && !canWakeDirectlyToGone + } else { + // Lockscreen is never visible when the scene container is invisible. + flowOf(false) } + } private val lockscreenVisibilityLegacy = combine( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt index 70a827d5e45b..1ea47ec670af 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt @@ -39,6 +39,7 @@ import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.util.kotlin.DisposableHandles +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DisposableHandle @@ -56,6 +57,7 @@ object DeviceEntryIconViewBinder { @JvmStatic fun bind( applicationScope: CoroutineScope, + mainImmediateDispatcher: CoroutineDispatcher, view: DeviceEntryIconView, viewModel: DeviceEntryIconViewModel, fgViewModel: DeviceEntryForegroundViewModel, @@ -96,6 +98,32 @@ object DeviceEntryIconViewBinder { } disposables += + view.repeatWhenAttached(mainImmediateDispatcher) { + repeatOnLifecycle(Lifecycle.State.CREATED) { + launch("$TAG#viewModel.useBackgroundProtection") { + viewModel.useBackgroundProtection.collect { useBackgroundProtection -> + if (useBackgroundProtection) { + bgView.visibility = View.VISIBLE + } else { + bgView.visibility = View.GONE + } + } + } + launch("$TAG#viewModel.burnInOffsets") { + viewModel.burnInOffsets.collect { burnInOffsets -> + view.translationX = burnInOffsets.x.toFloat() + view.translationY = burnInOffsets.y.toFloat() + view.aodFpDrawable.progress = burnInOffsets.progress + } + } + + launch("$TAG#viewModel.deviceEntryViewAlpha") { + viewModel.deviceEntryViewAlpha.collect { alpha -> view.alpha = alpha } + } + } + } + + disposables += view.repeatWhenAttached { // Repeat on CREATED so that the view will always observe the entire // GONE => AOD transition (even though the view may not be visible until the middle @@ -152,26 +180,6 @@ object DeviceEntryIconViewBinder { } } } - launch("$TAG#viewModel.useBackgroundProtection") { - viewModel.useBackgroundProtection.collect { useBackgroundProtection -> - if (useBackgroundProtection) { - bgView.visibility = View.VISIBLE - } else { - bgView.visibility = View.GONE - } - } - } - launch("$TAG#viewModel.burnInOffsets") { - viewModel.burnInOffsets.collect { burnInOffsets -> - view.translationX = burnInOffsets.x.toFloat() - view.translationY = burnInOffsets.y.toFloat() - view.aodFpDrawable.progress = burnInOffsets.progress - } - } - - launch("$TAG#viewModel.deviceEntryViewAlpha") { - viewModel.deviceEntryViewAlpha.collect { alpha -> view.alpha = alpha } - } } } @@ -212,7 +220,7 @@ object DeviceEntryIconViewBinder { } disposables += - bgView.repeatWhenAttached { + bgView.repeatWhenAttached(mainImmediateDispatcher) { repeatOnLifecycle(Lifecycle.State.CREATED) { launch("$TAG#bgViewModel.alpha") { bgViewModel.alpha.collect { alpha -> bgView.alpha = alpha } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index aeb327035c79..60460bf68c12 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -135,7 +135,10 @@ object KeyguardRootViewBinder { } else if ( event.action == MotionEvent.ACTION_UP && !event.isTouchscreenSource() ) { - statusBarKeyguardViewManager?.showBouncer(true) + statusBarKeyguardViewManager?.showBouncer( + true, + "KeyguardRootViewBinder: click on lockscreen", + ) consumed = true } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt index 58d482b8a66f..9c8f04b419fb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt @@ -28,6 +28,7 @@ import androidx.constraintlayout.widget.ConstraintSet import com.android.systemui.biometrics.AuthController import com.android.systemui.customization.R as customR import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.shared.model.KeyguardSection @@ -48,6 +49,7 @@ import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.VibratorHelper import dagger.Lazy import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DisposableHandle @@ -56,6 +58,7 @@ class DefaultDeviceEntrySection @Inject constructor( @Application private val applicationScope: CoroutineScope, + @Main private val mainDispatcher: CoroutineDispatcher, private val authController: AuthController, private val windowManager: WindowManager, @ShadeDisplayAware private val context: Context, @@ -91,6 +94,7 @@ constructor( disposableHandle = DeviceEntryIconViewBinder.bind( applicationScope, + mainDispatcher, it, deviceEntryIconViewModel.get(), deviceEntryForegroundViewModel.get(), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt index 9038922466df..803e2c0b0f96 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt @@ -106,7 +106,10 @@ constructor( } fun onTapped() { - statusBarKeyguardViewManager.showPrimaryBouncer(/* scrimmed */ true) + statusBarKeyguardViewManager.showPrimaryBouncer( + /* scrimmed */ true, + "AlternateBouncerUdfpsIconViewModel#onTapped", + ) } val bgColor: Flow<Int> = deviceEntryBackgroundViewModel.color diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt index cff651114c93..45f43bb484c8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt @@ -47,7 +47,9 @@ constructor( /** Reports the alternate bouncer visible state if the scene container flag is enabled. */ val isVisible: Flow<Boolean> = - alternateBouncerInteractor.get().isVisible.onEach { SceneContainerFlag.unsafeAssertInNewMode() } + alternateBouncerInteractor.get().isVisible.onEach { + SceneContainerFlag.unsafeAssertInNewMode() + } /** Progress to a fully transitioned alternate bouncer. 1f represents fully transitioned. */ val transitionToAlternateBouncerProgress: Flow<Float> = @@ -63,7 +65,10 @@ constructor( transitionToAlternateBouncerProgress.map { it == 1f }.distinctUntilChanged() fun onTapped() { - statusBarKeyguardViewManager.showPrimaryBouncer(/* scrimmed */ true) + statusBarKeyguardViewManager.showPrimaryBouncer( + /* scrimmed */ true, + "AlternateBouncerViewModel#onTapped", + ) } fun onRemovedFromWindow() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt index 13cd5839e1c8..9b4bd67f227e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt @@ -257,7 +257,9 @@ constructor( if (SceneContainerFlag.isEnabled) { deviceEntryInteractor.attemptDeviceEntry() } else { - keyguardViewController.get().showPrimaryBouncer(/* scrim */ true) + keyguardViewController + .get() + .showPrimaryBouncer(/* scrim */ true, "DeviceEntryIconViewModel#onUserInteraction") } deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt index 9312bca04994..a0458f0172f5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt @@ -64,7 +64,7 @@ constructor( val shortcutsAlpha: Flow<Float> = transitionAnimation.sharedFlow( - duration = FromLockscreenTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION, + duration = 200.milliseconds, onStep = alphaForAnimationStep, // Rapid swipes to bouncer, and may end up skipping intermediate values that would've // caused a complete fade out of lockscreen elements. Ensure it goes to 0f. 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 deleted file mode 100644 index 8df916fe6969..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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, - isFalseTouchDetected: Boolean, - 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 && !isFalseTouchDetected, - 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 f07238895aa5..d6d185195c51 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 @@ -33,6 +33,7 @@ import androidx.compose.animation.graphics.vector.AnimatedImageVector import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image +import androidx.compose.foundation.OverscrollEffect import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable @@ -54,6 +55,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ButtonDefaults @@ -107,7 +110,9 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastCoerceIn import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.util.fastRoundToInt @@ -120,6 +125,7 @@ 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.gesture.effect.rememberOffsetOverscrollEffect import com.android.compose.ui.graphics.painter.rememberDrawablePainter import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.compose.Icon @@ -137,7 +143,9 @@ 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.MediaSettingsButtonViewModel import com.android.systemui.media.remedia.ui.viewmodel.MediaViewModel +import kotlin.math.abs import kotlin.math.max import kotlin.math.min @@ -208,43 +216,16 @@ private fun CardCarouselContent( onDismissed: () -> Unit, modifier: Modifier = Modifier, ) { - val pagerState = - rememberDismissibleHorizontalPagerState( - isDismissible = behavior.isCarouselDismissible, - isScrollingEnabled = behavior.isCarouselScrollingEnabled, - ) { - viewModel.cards.size - } + val pagerState = rememberPagerState { viewModel.cards.size } + LaunchedEffect(pagerState.currentPage) { viewModel.onCardSelected(pagerState.currentPage) } + var isFalseTouchDetected: Boolean by remember(behavior.isCarouselScrollFalseTouch) { mutableStateOf(false) } + val isSwipingEnabled = behavior.isCarouselScrollingEnabled && !isFalseTouchDetected 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 - }, - ) - } - }, - isFalseTouchDetected = isFalseTouchDetected, + Box( modifier = modifier.padding(8.dp).clip(roundedCornerShape).pointerInput(behavior) { if (behavior.isCarouselScrollFalseTouch != null) { @@ -253,13 +234,54 @@ private fun CardCarouselContent( isFalseTouchDetected = behavior.isCarouselScrollFalseTouch.invoke() } } - }, - ) { index -> - Card( - viewModel = viewModel.cards[index], - presentationStyle = presentationStyle, - modifier = Modifier.clip(roundedCornerShape), - ) + } + ) { + @Composable + fun PagerContent(overscrollEffect: OverscrollEffect? = null) { + Box { + HorizontalPager( + state = pagerState, + userScrollEnabled = isSwipingEnabled, + pageSpacing = 8.dp, + key = { index: Int -> viewModel.cards[index].key }, + overscrollEffect = overscrollEffect ?: rememberOffsetOverscrollEffect(), + ) { pageIndex: Int -> + Card( + viewModel = viewModel.cards[pageIndex], + presentationStyle = presentationStyle, + modifier = Modifier.clip(roundedCornerShape), + ) + } + + if (pagerState.pageCount > 1) { + PagerDots( + pagerState = pagerState, + activeColor = Color(0xffdee0ff), + nonActiveColor = Color(0xffa7a9ca), + dotSize = 6.dp, + spaceSize = 6.dp, + modifier = Modifier.align(Alignment.BottomCenter).padding(8.dp), + ) + } + } + } + + if (behavior.isCarouselDismissible) { + SwipeToDismiss(content = { PagerContent() }, onDismissed = onDismissed) + } else { + val overscrollEffect = rememberOffsetOverscrollEffect() + SwipeToReveal( + foregroundContent = { PagerContent(overscrollEffect) }, + foregroundContentEffect = overscrollEffect, + revealedContent = { revealAmount -> + RevealedContent( + viewModel = viewModel.settingsButtonViewModel, + revealAmount = revealAmount, + ) + }, + isSwipingEnabled = isSwipingEnabled, + ) + } } } @@ -496,16 +518,19 @@ private fun ContentScope.CardForegroundContent( modifier = Modifier.weight(1f).padding(end = 8.dp), ) + val playPauseSize = DpSize(width = 48.dp, height = 48.dp) if (viewModel.actionButtonLayout == MediaCardActionButtonLayout.WithPlayPause) { AnimatedVisibility(visible = viewModel.playPauseAction != null) { PlayPauseAction( viewModel = checkNotNull(viewModel.playPauseAction), - buttonWidth = 48.dp, + buttonSize = playPauseSize, buttonColor = colorScheme.primary, iconColor = colorScheme.onPrimary, buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp }, ) } + } else { + Spacer(Modifier.size(playPauseSize)) } } @@ -565,14 +590,19 @@ private fun ContentScope.CardForegroundContent( } } - 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 }, - ) + val playPauseSize = DpSize(width = 48.dp, height = 48.dp) + if (viewModel.actionButtonLayout == MediaCardActionButtonLayout.WithPlayPause) { + AnimatedVisibility(visible = viewModel.playPauseAction != null) { + PlayPauseAction( + viewModel = checkNotNull(viewModel.playPauseAction), + buttonSize = playPauseSize, + buttonColor = colorScheme.primary, + iconColor = colorScheme.onPrimary, + buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp }, + ) + } + } else { + Spacer(Modifier.size(playPauseSize)) } } } @@ -628,7 +658,7 @@ private fun ContentScope.CompactCardForeground( AnimatedVisibility(visible = viewModel.playPauseAction != null) { PlayPauseAction( viewModel = checkNotNull(viewModel.playPauseAction), - buttonWidth = 72.dp, + buttonSize = DpSize(width = 72.dp, height = 48.dp), buttonColor = MaterialTheme.colorScheme.primaryContainer, iconColor = MaterialTheme.colorScheme.onPrimaryContainer, buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 24.dp }, @@ -1084,7 +1114,7 @@ private fun OutputSwitcherChip( @Composable private fun ContentScope.PlayPauseAction( viewModel: MediaPlayPauseActionViewModel, - buttonWidth: Dp, + buttonSize: DpSize, buttonColor: Color, iconColor: Color, buttonCornerRadius: (isPlaying: Boolean) -> Dp, @@ -1102,7 +1132,7 @@ private fun ContentScope.PlayPauseAction( enabled = viewModel.onClick != null, colors = ButtonDefaults.buttonColors(containerColor = buttonColor), shape = RoundedCornerShape(cornerRadius), - modifier = Modifier.size(width = buttonWidth, height = 48.dp), + modifier = Modifier.size(buttonSize), ) { when (viewModel.state) { is MediaSessionState.Playing, @@ -1186,6 +1216,65 @@ private fun SecondaryActionContent( } } +/** + * Renders the revealed content on the sides of the horizontal pager. + * + * @param revealAmount A callback that can return the amount of revealing done. This value will be + * in a range slightly larger than `-1` to `+1` where `1` is fully revealed on the left-hand side, + * `-1` is fully revealed on the right-hand side, and `0` is not revealed at all. Numbers lower + * than `-1` or greater than `1` are possible when the overscroll effect adds additional pixels of + * offset. + */ +@Composable +private fun RevealedContent( + viewModel: MediaSettingsButtonViewModel, + revealAmount: () -> Float, + modifier: Modifier = Modifier, +) { + val horizontalPadding = 18.dp + + // This custom layout's purpose is only to place the icon in the center of the revealed content, + // taking into account the amount of reveal. + Layout( + content = { + Icon( + icon = viewModel.icon, + modifier = + Modifier.size(48.dp) + .padding(12.dp) + .graphicsLayer { + alpha = abs(revealAmount()).fastCoerceIn(0f, 1f) + rotationZ = revealAmount() * 90 + } + .clickable { viewModel.onClick() }, + ) + }, + modifier = modifier, + ) { measurables, constraints -> + check(measurables.size == 1) + val placeable = measurables[0].measure(constraints) + val totalWidth = + min(horizontalPadding.roundToPx() * 2 + placeable.measuredWidth, constraints.maxWidth) + + layout(totalWidth, constraints.maxHeight) { + coordinates?.size?.let { size -> + val reveal = revealAmount() + val x = + if (reveal >= 0f) { + ((size.width * abs(reveal)) - placeable.measuredWidth) / 2 + } else { + size.width * (1 - abs(reveal) / 2) - placeable.measuredWidth / 2 + } + + placeable.place( + x = x.fastRoundToInt(), + y = (size.height - placeable.measuredHeight) / 2, + ) + } + } + } +} + /** Enumerates all supported media presentation styles. */ enum class MediaPresentationStyle { /** The "normal" 3-row carousel look. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt new file mode 100644 index 000000000000..b80bf4143252 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToDismiss.kt @@ -0,0 +1,115 @@ +/* + * 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.foundation.OverscrollEffect +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +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.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.util.fastRoundToInt +import kotlinx.coroutines.launch + +/** Swipe to dismiss that supports nested scrolling. */ +@Composable +fun SwipeToDismiss( + content: @Composable (overscrollEffect: OverscrollEffect?) -> Unit, + onDismissed: () -> Unit, + modifier: Modifier = Modifier, +) { + val scope = rememberCoroutineScope() + val offsetAnimatable = remember { Animatable(0f) } + + // This is the width of the revealed content UI box. It's not a state because it's not + // observed in any composition and is an object with a value to avoid the extra cost + // associated with boxing and unboxing an int. + val revealedContentBoxWidth = remember { + object { + var value = 0 + } + } + + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + return if (offsetAnimatable.value > 0f && available.x < 0f) { + scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) } + Offset(available.x, 0f) + } else if (offsetAnimatable.value < 0f && available.x > 0f) { + scope.launch { offsetAnimatable.snapTo(offsetAnimatable.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 { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) } + Offset(available.x, 0f) + } else if (available.x < 0f) { + scope.launch { offsetAnimatable.snapTo(offsetAnimatable.value + available.x) } + Offset(available.x, 0f) + } else { + Offset.Zero + } + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + scope.launch { + offsetAnimatable.animateTo( + if (offsetAnimatable.value >= revealedContentBoxWidth.value / 2f) { + revealedContentBoxWidth.value * 2f + } else if (offsetAnimatable.value <= -revealedContentBoxWidth.value / 2f) { + -revealedContentBoxWidth.value * 2f + } else { + 0f + } + ) + if (offsetAnimatable.value != 0f) { + onDismissed() + } + } + return super.onPostFling(consumed, available) + } + } + } + + Box( + modifier = + modifier + .onSizeChanged { revealedContentBoxWidth.value = it.width } + .nestedScroll(nestedScrollConnection) + .offset { IntOffset(x = offsetAnimatable.value.fastRoundToInt(), y = 0) } + ) { + content(null) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt new file mode 100644 index 000000000000..770762c7a29f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/SwipeToReveal.kt @@ -0,0 +1,264 @@ +/* + * 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.foundation.OverscrollEffect +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.overscroll +import androidx.compose.foundation.withoutVisualEffect +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerType +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.util.fastCoerceIn +import androidx.compose.ui.util.fastRoundToInt +import com.android.compose.gesture.NestedDraggable +import com.android.compose.gesture.effect.OffsetOverscrollEffect +import com.android.compose.gesture.effect.rememberOffsetOverscrollEffect +import com.android.compose.gesture.nestedDraggable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Swipe to reveal that supports nested scrolling and an overscroll effect. + * + * @param foregroundContent The content to show above all else; this is the content that can be + * swiped sideways to reveal the [revealedContent]. This may contain a horizontally-scrollable + * component (for example a `HorizontalPager`). + * @param revealedContent The content that is shown below the [foregroundContent]; this is the + * content that can be revealed by swiping the [foregroundContent] sideways. + */ +@Composable +fun SwipeToReveal( + foregroundContent: @Composable (overscrollEffect: OverscrollEffect?) -> Unit, + foregroundContentEffect: OffsetOverscrollEffect, + revealedContent: @Composable BoxScope.(revealAmount: () -> Float) -> Unit, + isSwipingEnabled: Boolean, + modifier: Modifier = Modifier, +) { + // This composable supports an overscroll effect, to make it possible for the user to + // "stretch" the UI when the side is fully revealed but the user keeps trying to reveal it + // further. + val revealedContentEffect = rememberOffsetOverscrollEffect() + + // This is the width of the revealed content UI box. It's not a state because it's not + // observed in any composition and is an object with a value to avoid the extra cost + // associated with boxing and unboxing an int. + val revealedContentBoxWidth = remember { + object { + var value = 0 + } + } + + // In order to support the drag to reveal, infrastructure has to be put in place where a + // NestedDraggable helps by consuming the unconsumed drags and flings and applying the + // overscroll visual effect. + // + // This is the NestedDraggalbe controller. + val revealedContentDragController = rememberRevealedContentDragController { + revealedContentBoxWidth.value.toFloat() + } + + Box( + modifier = + modifier + .nestedDraggable( + enabled = isSwipingEnabled, + draggable = + remember { + object : NestedDraggable { + override fun onDragStarted( + position: Offset, + sign: Float, + pointersDown: Int, + pointerType: PointerType?, + ): NestedDraggable.Controller { + return revealedContentDragController + } + + override fun shouldConsumeNestedPostScroll(sign: Float): Boolean { + return revealedContentDragController.shouldConsumePostScrolls( + sign + ) + } + + override fun shouldConsumeNestedPreScroll(sign: Float): Boolean { + return revealedContentDragController.shouldConsumePreScrolls( + sign + ) + } + } + }, + orientation = Orientation.Horizontal, + overscrollEffect = revealedContentEffect.withoutVisualEffect(), + ) + .overscroll(revealedContentEffect) + ) { + val density = LocalDensity.current + + /** + * Returns the amount of visual offset, in pixels, that is comprised of both the offset from + * dragging and the overscroll effect's additional pixels after applying its animation curve + * on the raw distance. + */ + fun offsetWithOverscroll(): Float { + return revealedContentDragController.offset + + OffsetOverscrollEffect.computeOffset( + density, + foregroundContentEffect.overscrollDistance, + ) + + OffsetOverscrollEffect.computeOffset( + density, + revealedContentEffect.overscrollDistance, + ) + } + + /** + * Returns the ratio of the amount by which the revealed content is revealed, where: + * - `0` means none of it is revealed + * - `+1` means all of it is revealed to the start of the foreground content + * - `-1` means all of it is revealed to the end of the foreground content + * + * The number could be smaller than `-1` or larger than `+1` to model overscrolling. + */ + fun revealAmount(): Float { + return (offsetWithOverscroll() / revealedContentBoxWidth.value) + } + + Layout( + content = { revealedContent { revealAmount() } }, + modifier = Modifier.matchParentSize(), + ) { measurables, constraints -> + check(measurables.size == 1) + val placeable = measurables[0].measure(constraints.copy(minWidth = 0, minHeight = 0)) + // Keep revealedContentBoxWidth up to date with the latest value. + revealedContentBoxWidth.value = placeable.measuredWidth + + // Place the revealed content on the correct side, depending on the direction of the + // reveal. + val alignedToStart = revealAmount() >= 0f + layout(constraints.maxWidth, constraints.maxHeight) { + coordinates?.size?.let { size -> + placeable.place( + x = if (alignedToStart) 0 else size.width - placeable.measuredWidth, + y = 0, + ) + } + } + } + + Box( + modifier = + Modifier.absoluteOffset { + IntOffset(revealedContentDragController.offset.fastRoundToInt(), y = 0) + } + ) { + foregroundContent(foregroundContentEffect) + } + } +} + +@Composable +private fun rememberRevealedContentDragController( + maxBound: () -> Float +): RevealedContentDragController { + val scope = rememberCoroutineScope() + return remember { RevealedContentDragController(scope = scope, maxBound = maxBound) } +} + +private class RevealedContentDragController( + private val scope: CoroutineScope, + private val maxBound: () -> Float, +) : NestedDraggable.Controller { + private val offsetAnimatable = Animatable(0f) + private var lastTarget = 0f + private var range = 0f..1f + private var shouldConsumePreScrolls by mutableStateOf(false) + + override val autoStopNestedDrags: Boolean + get() = true + + val offset: Float + get() = offsetAnimatable.value + + fun shouldConsumePreScrolls(sign: Float): Boolean { + if (!shouldConsumePreScrolls) return false + + if (lastTarget > 0f && sign < 0f) { + range = 0f..maxBound() + return true + } + + if (lastTarget < 0f && sign > 0f) { + range = -maxBound()..0f + return true + } + + return false + } + + fun shouldConsumePostScrolls(sign: Float): Boolean { + val max = maxBound() + if (sign > 0f && lastTarget < max) { + range = 0f..maxBound() + return true + } + + if (sign < 0f && lastTarget > -max) { + range = -maxBound()..0f + return true + } + + return false + } + + override fun onDrag(delta: Float): Float { + val previousTarget = lastTarget + lastTarget = (lastTarget + delta).fastCoerceIn(range.start, range.endInclusive) + val newTarget = lastTarget + scope.launch { offsetAnimatable.snapTo(newTarget) } + return lastTarget - previousTarget + } + + override suspend fun onDragStopped(velocity: Float, awaitFling: suspend () -> Unit): Float { + val rangeMiddle = range.start + (range.endInclusive - range.start) / 2f + lastTarget = + when { + lastTarget >= rangeMiddle -> range.endInclusive + else -> range.start + } + + shouldConsumePreScrolls = lastTarget != 0f + val newTarget = lastTarget + + scope.launch { offsetAnimatable.animateTo(newTarget) } + return velocity + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSettingsButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSettingsButtonViewModel.kt new file mode 100644 index 000000000000..4f4c25c3c74f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSettingsButtonViewModel.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.ui.viewmodel + +import com.android.systemui.common.shared.model.Icon + +data class MediaSettingsButtonViewModel(val icon: Icon.Resource, val onClick: () -> Unit) 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 index 19b08fa212db..a57da63a7a81 100644 --- 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 @@ -252,6 +252,21 @@ constructor( } } + val settingsButtonViewModel = + MediaSettingsButtonViewModel( + icon = + Icon.Resource( + res = R.drawable.ic_settings, + contentDescription = + ContentDescription.Resource(res = R.string.controls_media_settings_button), + ), + onClick = { + falsingSystem.runIfNotFalseTap(FalsingManager.LOW_PENALTY) { + interactor.openMediaSettings() + } + }, + ) + /** Whether the carousel should be visible. */ val isCarouselVisible: Boolean get() = diff --git a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt index 4559a7aea1a2..7b3f4c61088b 100644 --- a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt +++ b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt @@ -79,6 +79,7 @@ constructor( SceneContainerPluginState( scene = idleState.currentScene, overlays = idleState.currentOverlays, + isVisible = sceneInteractor.get().isVisible.value, invisibleDueToOcclusion = invisibleDueToOcclusion, ) ) @@ -100,12 +101,17 @@ constructor( mapOf<Long, (SceneContainerPluginState) -> Boolean>( SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE to { - it.scene != Scenes.Gone || it.overlays.isNotEmpty() + when { + !it.isVisible -> false + it.scene != Scenes.Gone -> true + it.overlays.isNotEmpty() -> true + else -> false + } }, SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to { when { - it.invisibleDueToOcclusion -> false + !it.isVisible -> false it.scene == Scenes.Lockscreen -> true it.scene == Scenes.Shade -> true Overlays.NotificationsShade in it.overlays -> true @@ -114,19 +120,23 @@ constructor( }, SYSUI_STATE_QUICK_SETTINGS_EXPANDED to { - it.scene == Scenes.QuickSettings || - Overlays.QuickSettingsShade in it.overlays + when { + !it.isVisible -> false + it.scene == Scenes.QuickSettings -> true + Overlays.QuickSettingsShade in it.overlays -> true + else -> false + } }, - SYSUI_STATE_BOUNCER_SHOWING to { Overlays.Bouncer in it.overlays }, + SYSUI_STATE_BOUNCER_SHOWING to { it.isVisible && Overlays.Bouncer in it.overlays }, SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to { - it.scene == Scenes.Lockscreen && !it.invisibleDueToOcclusion + it.isVisible && it.scene == Scenes.Lockscreen }, SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED to { it.scene == Scenes.Lockscreen && it.invisibleDueToOcclusion }, - SYSUI_STATE_COMMUNAL_HUB_SHOWING to { it.scene == Scenes.Communal }, + SYSUI_STATE_COMMUNAL_HUB_SHOWING to { it.isVisible && it.scene == Scenes.Communal }, ) } @@ -134,5 +144,6 @@ constructor( val scene: SceneKey, val overlays: Set<OverlayKey>, val invisibleDueToOcclusion: Boolean, + val isVisible: Boolean, ) } diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt b/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt index 1e18f24c9e65..195535669c7e 100644 --- a/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt +++ b/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt @@ -16,8 +16,6 @@ package com.android.systemui.model -import com.android.systemui.dagger.qualifiers.DisplayId - /** * In-bulk updates multiple flag values and commits the update. * @@ -32,16 +30,8 @@ import com.android.systemui.dagger.qualifiers.DisplayId * SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to (sceneKey == Scenes.Lockscreen), * ) * ``` - * - * You can inject [displayId] by injecting it using: - * ``` - * @DisplayId private val displayId: Int`, - * ``` */ -fun SysUiState.updateFlags( - @DisplayId displayId: Int, - vararg flagValuePairs: Pair<Long, Boolean>, -) { +fun SysUiState.updateFlags(vararg flagValuePairs: Pair<Long, Boolean>) { flagValuePairs.forEach { (flag, enabled) -> setFlag(flag, enabled) } - commitUpdate(displayId) + commitUpdate() } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt index 8dc27bf4ac3e..080940169e46 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt @@ -86,7 +86,10 @@ constructor( */ private fun initializeKeyGestureEventHandler() { if (useKeyGestureEventHandler()) { - inputManager.registerKeyGestureEventHandler(callbacks) + inputManager.registerKeyGestureEventHandler( + listOf(KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES), + callbacks, + ) } } @@ -156,11 +159,8 @@ constructor( controller.updateNoteTaskForCurrentUserAndManagedProfiles() } - override fun handleKeyGestureEvent( - event: KeyGestureEvent, - focusedToken: IBinder?, - ): Boolean { - return this@NoteTaskInitializer.handleKeyGestureEvent(event) + override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?) { + this@NoteTaskInitializer.handleKeyGestureEvent(event) } } @@ -202,23 +202,19 @@ constructor( return !isMultiPress && !isLongPress } - private fun handleKeyGestureEvent(event: KeyGestureEvent): Boolean { - // This method is on input hot path and should be kept lightweight. Shift all complex - // processing onto background executor wherever possible. + private fun handleKeyGestureEvent(event: KeyGestureEvent) { if (event.keyGestureType != KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES) { - return false + return } debugLog { "handleKeyGestureEvent: Received OPEN_NOTES gesture event from keycodes: " + event.keycodes.contentToString() } if (event.keycodes.size == 1 && event.keycodes[0] == KEYCODE_STYLUS_BUTTON_TAIL) { - debugLog { "Note task triggered by stylus tail button" } backgroundExecutor.execute { controller.showNoteTask(TAIL_BUTTON) } - return true + } else { + backgroundExecutor.execute { controller.showNoteTask(KEYBOARD_SHORTCUT) } } - backgroundExecutor.execute { controller.showNoteTask(KEYBOARD_SHORTCUT) } - return true } companion object { 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 05a60a6db31e..9f04f69bd0bf 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,9 @@ package com.android.systemui.qs.composefragment import android.annotation.SuppressLint import android.content.Context +import android.content.res.Configuration +import android.graphics.Canvas +import android.graphics.Path import android.graphics.PointF import android.graphics.Rect import android.os.Bundle @@ -35,6 +38,7 @@ import androidx.activity.setViewTreeOnBackPressedDispatcherOwner import androidx.annotation.VisibleForTesting import androidx.compose.animation.core.tween import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -48,6 +52,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 @@ -70,6 +75,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 +109,7 @@ import com.android.compose.theme.PlatformTheme import com.android.mechanics.GestureContext import com.android.systemui.Dumpable import com.android.systemui.Flags +import com.android.systemui.Flags.notificationShadeBlur import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer import com.android.systemui.brightness.ui.compose.ContainerColors import com.android.systemui.compose.modifiers.sysuiResTag @@ -119,7 +127,6 @@ import com.android.systemui.qs.composefragment.SceneKeys.debugName import com.android.systemui.qs.composefragment.SceneKeys.toIdleSceneKey import com.android.systemui.qs.composefragment.ui.GridAnchor import com.android.systemui.qs.composefragment.ui.NotificationScrimClipParams -import com.android.systemui.qs.composefragment.ui.notificationScrimClip import com.android.systemui.qs.composefragment.ui.quickQuickSettingsToQuickSettings import com.android.systemui.qs.composefragment.ui.toEditMode import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel @@ -235,7 +242,7 @@ constructor( FrameLayoutTouchPassthrough( context, { notificationScrimClippingParams.isEnabled }, - { notificationScrimClippingParams.params.top }, + snapshotFlow { notificationScrimClippingParams.params }, // Only allow scrolling when we are fully expanded. That way, we don't intercept // swipes in lockscreen (when somehow QS is receiving touches). { (scrollState.canScrollForward && viewModel.isQsFullyExpanded) || isCustomizing }, @@ -251,7 +258,7 @@ constructor( @Composable private fun Content() { - PlatformTheme { + PlatformTheme(isDarkTheme = if (notificationShadeBlur()) isSystemInDarkTheme() else true) { 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. @@ -270,11 +277,6 @@ constructor( } } .graphicsLayer { alpha = viewModel.viewAlpha } - .thenIf(notificationScrimClippingParams.isEnabled) { - Modifier.notificationScrimClip { - notificationScrimClippingParams.params - } - } .thenIf(!Flags.notificationShadeBlur()) { Modifier.offset { IntOffset( @@ -746,19 +748,31 @@ constructor( Box( Modifier.systemGestureExclusionInShade( enabled = { - layoutState.transitionState is TransitionState.Idle + /* + * While we are transitioning into QS (either from QQS + * or from gone), the global position of the brightness + * slider will change in every frame. This causes + * the modifier to send a new gesture exclusion + * rectangle on every frame. Instead, only apply the + * modifier when this is settled. + */ + layoutState.transitionState is TransitionState.Idle && + viewModel.isNotTransitioning } ) ) { - BrightnessSliderContainer( - viewModel = containerViewModel.brightnessSliderViewModel, - containerColors = - ContainerColors( - Color.Transparent, - ContainerColors.defaultContainerColor, - ), - modifier = Modifier.fillMaxWidth(), - ) + AlwaysDarkMode { + BrightnessSliderContainer( + viewModel = + containerViewModel.brightnessSliderViewModel, + containerColors = + ContainerColors( + Color.Transparent, + ContainerColors.defaultContainerColor, + ), + modifier = Modifier.fillMaxWidth(), + ) + } } } val TileGrid = @@ -1043,17 +1057,75 @@ private const val EDIT_MODE_TIME_MILLIS = 500 private class FrameLayoutTouchPassthrough( context: Context, private val clippingEnabledProvider: () -> Boolean, - private val clippingTopProvider: () -> Int, + private val clippingParams: Flow<NotificationScrimClipParams>, private val canScrollForwardQs: () -> Boolean, private val emitMotionEventForFalsing: () -> Unit, ) : FrameLayout(context) { + + init { + repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + clippingParams.collect { currentClipParams = it } + } + } + } + + private val currentClippingPath = Path() + private var lastWidth = -1 + set(value) { + if (field != value) { + field = value + updateClippingPath() + } + } + + private var currentClipParams = NotificationScrimClipParams() + set(value) { + if (field != value) { + field = value + updateClippingPath() + } + } + + private fun updateClippingPath() { + currentClippingPath.rewind() + if (clippingEnabledProvider()) { + val right = width + currentClipParams.rightInset + val left = -currentClipParams.leftInset + val top = currentClipParams.top + val bottom = currentClipParams.bottom + currentClippingPath.addRoundRect( + left.toFloat(), + top.toFloat(), + right.toFloat(), + bottom.toFloat(), + currentClipParams.radius.toFloat(), + currentClipParams.radius.toFloat(), + Path.Direction.CW, + ) + } + invalidate() + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + lastWidth = right - left + } + + override fun dispatchDraw(canvas: Canvas) { + if (!currentClippingPath.isEmpty) { + canvas.clipOutPath(currentClippingPath) + } + super.dispatchDraw(canvas) + } + override fun isTransformedTouchPointInView( x: Float, y: Float, child: View?, outLocalPoint: PointF?, ): Boolean { - return if (clippingEnabledProvider() && y + translationY > clippingTopProvider()) { + return if (clippingEnabledProvider() && y + translationY > currentClipParams.top) { false } else { super.isTransformedTouchPointInView(x, y, child, outLocalPoint) @@ -1236,3 +1308,31 @@ 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 [notificationShadeBlur] is removed + */ +@Composable +private fun AlwaysDarkMode(content: @Composable () -> Unit) { + if (notificationShadeBlur()) { + content() + } else { + 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/composefragment/ui/NotificationScrimClip.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt deleted file mode 100644 index 3049a40f18c4..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.composefragment.ui - -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.ClipOp -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.CompositingStrategy -import androidx.compose.ui.graphics.drawscope.clipRect -import androidx.compose.ui.graphics.graphicsLayer - -/** - * Clipping modifier for clipping out the notification scrim as it slides over QS. It will clip out - * ([ClipOp.Difference]) a `RoundRect(-leftInset, top, width + rightInset, bottom, radius, radius)` - * from the QS container. - */ -fun Modifier.notificationScrimClip(clipParams: () -> NotificationScrimClipParams): Modifier { - return this.graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } - .drawWithContent { - drawContent() - val params = clipParams() - val left = -params.leftInset.toFloat() - val right = size.width + params.rightInset.toFloat() - val top = params.top.toFloat() - val bottom = params.bottom.toFloat() - val clipSize = Size(right - left, bottom - top) - if (!clipSize.isEmpty()) { - clipRect { - drawRoundRect( - color = Color.Black, - cornerRadius = CornerRadius(params.radius.toFloat()), - blendMode = BlendMode.Clear, - topLeft = Offset(left, top), - size = Size(right - left, bottom - top), - ) - } - } - } -} - -/** Params for [notificationScrimClip]. */ -data class NotificationScrimClipParams( - val top: Int = 0, - val bottom: Int = 0, - val leftInset: Int = 0, - val rightInset: Int = 0, - val radius: Int = 0, -) diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClipParams.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClipParams.kt new file mode 100644 index 000000000000..db320d3b9f1c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClipParams.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.composefragment.ui + +/** Params for [notificationScrimClip]. */ +data class NotificationScrimClipParams( + val top: Int = 0, + val bottom: Int = 0, + val leftInset: Int = 0, + val rightInset: Int = 0, + val radius: Int = 0, +) diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt index b61fa9cfe264..b7e9c5296bc9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt @@ -312,6 +312,11 @@ constructor( source = containerViewModel.editModeViewModel.isEditing, ) + /** True if we are not in an expansion (from Gone to QQS/QS) animation. */ + val isNotTransitioning by derivedStateOf { + viewTranslationY == 0f && viewAlpha == 1f && constrainedSquishinessFraction == 1f + } + private val inFirstPage: Boolean get() = inFirstPageViewModel.inFirstPage diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt index 405ce8a8e5e0..005c8b26b0d8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt @@ -35,7 +35,6 @@ import androidx.compose.ui.draganddrop.mimeTypes import androidx.compose.ui.draganddrop.toAndroidDragEvent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.IntRect -import androidx.compose.ui.unit.center import androidx.compose.ui.unit.toRect import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel @@ -44,6 +43,7 @@ import com.android.systemui.qs.pipeline.shared.TileSpec /** Holds the [TileSpec] of the tile being moved and receives drag and drop events. */ interface DragAndDropState { val draggedCell: SizedTile<EditTileViewModel>? + val isDraggedCellRemovable: Boolean val draggedPosition: Offset val dragInProgress: Boolean val dragType: DragType? @@ -76,7 +76,7 @@ enum class DragType { @Composable fun Modifier.dragAndDropRemoveZone( dragAndDropState: DragAndDropState, - onDrop: (TileSpec) -> Unit, + onDrop: (TileSpec, removalEnabled: Boolean) -> Unit, ): Modifier { val target = remember(dragAndDropState) { @@ -87,13 +87,15 @@ fun Modifier.dragAndDropRemoveZone( override fun onDrop(event: DragAndDropEvent): Boolean { return dragAndDropState.draggedCell?.let { - onDrop(it.tile.tileSpec) + onDrop(it.tile.tileSpec, dragAndDropState.isDraggedCellRemovable) dragAndDropState.onDrop() true } ?: false } override fun onEntered(event: DragAndDropEvent) { + if (!dragAndDropState.isDraggedCellRemovable) return + dragAndDropState.movedOutOfBounds() } } @@ -168,10 +170,10 @@ private fun DragAndDropEvent.toOffset(): Offset { } private fun insertAfter(item: LazyGridItemInfo, offset: Offset): Boolean { - // We want to insert the tile after the target if we're aiming at the right side of a large tile + // We want to insert the tile after the target if we're aiming at the end of a large tile // TODO(ostonge): Verify this behavior in RTL - val itemCenter = item.offset + item.size.center - return item.span != 1 && offset.x > itemCenter.x + val itemCenter = item.offset.x + item.size.width * .75 + return item.span != 1 && offset.x > itemCenter } @OptIn(ExperimentalFoundationApi::class) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt index 868855840922..70f1674acd3b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.geometry.Offset import com.android.systemui.qs.panels.shared.model.SizedTile +import com.android.systemui.qs.panels.ui.compose.selection.PlacementEvent import com.android.systemui.qs.panels.ui.model.GridCell import com.android.systemui.qs.panels.ui.model.TileGridCell import com.android.systemui.qs.panels.ui.model.toGridCells @@ -60,6 +61,11 @@ class EditTileListState( override var dragType by mutableStateOf<DragType?>(null) private set + // A dragged cell can be removed if it was added in the drag movement OR if it's marked as + // removable + override val isDraggedCellRemovable: Boolean + get() = dragType == DragType.Add || draggedCell?.tile?.isRemovable ?: false + override val dragInProgress: Boolean get() = draggedCell != null @@ -76,10 +82,16 @@ class EditTileListState( return _tiles.indexOfFirst { it is TileGridCell && it.tile.tileSpec == tileSpec } } + fun isRemovable(tileSpec: TileSpec): Boolean { + return _tiles.find { + it is TileGridCell && it.tile.tileSpec == tileSpec && it.tile.isRemovable + } != null + } + /** Resize the tile corresponding to the [TileSpec] to [toIcon] */ fun resizeTile(tileSpec: TileSpec, toIcon: Boolean) { val fromIndex = indexOf(tileSpec) - if (fromIndex != -1) { + if (fromIndex != INVALID_INDEX) { val cell = _tiles[fromIndex] as TileGridCell if (cell.isIcon == toIcon) return @@ -97,9 +109,6 @@ class EditTileListState( override fun onStarted(cell: SizedTile<EditTileViewModel>, dragType: DragType) { draggedCell = cell this.dragType = dragType - - // Add spacers to the grid to indicate where the user can move a tile - regenerateGrid() } override fun onTargeting(target: Int, insertAfter: Boolean) { @@ -111,7 +120,7 @@ class EditTileListState( } val insertionIndex = if (insertAfter) target + 1 else target - if (fromIndex != -1) { + if (fromIndex != INVALID_INDEX) { val cell = _tiles.removeAt(fromIndex) regenerateGrid() _tiles.add(insertionIndex.coerceIn(0, _tiles.size), cell) @@ -149,6 +158,43 @@ class EditTileListState( regenerateGrid() } + /** + * Return the appropriate index to move the tile to for the placement [event] + * + * The grid includes spacers. As a result, indexes from the grid need to be translated to the + * corresponding index from [currentTileSpecs]. + */ + fun targetIndexForPlacement(event: PlacementEvent): Int { + val currentTileSpecs = tileSpecs() + return when (event) { + is PlacementEvent.PlaceToTileSpec -> { + currentTileSpecs.indexOf(event.targetSpec) + } + is PlacementEvent.PlaceToIndex -> { + if (event.targetIndex >= _tiles.size) { + currentTileSpecs.size + } else if (event.targetIndex <= 0) { + 0 + } else { + // The index may point to a spacer, so first find the first tile located + // after index, then use its position as a target + val targetTile = + _tiles.subList(event.targetIndex, _tiles.size).firstOrNull { + it is TileGridCell + } as? TileGridCell + + if (targetTile == null) { + currentTileSpecs.size + } else { + val targetIndex = currentTileSpecs.indexOf(targetTile.tile.tileSpec) + val fromIndex = currentTileSpecs.indexOf(event.movingSpec) + if (fromIndex < targetIndex) targetIndex - 1 else targetIndex + } + } + } + } + } + /** Regenerate the list of [GridCell] with their new potential rows */ private fun regenerateGrid() { _tiles.filterIsInstance<TileGridCell>().toGridCells(columns).let { @@ -170,4 +216,8 @@ class EditTileListState( _tiles.addAll(it) } } + + companion object { + const val INVALID_INDEX = -1 + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt index 1176095fbb1c..f3ed07a8a753 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt @@ -46,6 +46,8 @@ import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsViewModel import com.android.systemui.plugins.qs.TileDetailsViewModel import com.android.systemui.qs.flags.QsDetailedView import com.android.systemui.qs.panels.ui.viewmodel.DetailsViewModel +import com.android.systemui.qs.tiles.dialog.CastDetailsContent +import com.android.systemui.qs.tiles.dialog.CastDetailsViewModel import com.android.systemui.qs.tiles.dialog.InternetDetailsContent import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel import com.android.systemui.qs.tiles.dialog.ModesDetailsContent @@ -155,6 +157,7 @@ private fun MapTileDetailsContent(tileDetailsViewModel: TileDetailsViewModel) { is BluetoothDetailsViewModel -> BluetoothDetailsContent(tileDetailsViewModel.detailsContentViewModel) is ModesDetailsViewModel -> ModesDetailsContent(tileDetailsViewModel) + is CastDetailsViewModel -> CastDetailsContent(tileDetailsViewModel) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt index 46f05d0ac895..f8eaa6c3bcfb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateDpAsState @@ -34,6 +35,7 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.clipScrollableContainer import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope @@ -78,6 +80,7 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.key @@ -96,6 +99,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.isSpecified import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.onGloballyPositioned @@ -125,6 +129,7 @@ import com.android.systemui.qs.panels.shared.model.SizedTileImpl import com.android.systemui.qs.panels.ui.compose.DragAndDropState import com.android.systemui.qs.panels.ui.compose.DragType import com.android.systemui.qs.panels.ui.compose.EditTileListState +import com.android.systemui.qs.panels.ui.compose.EditTileListState.Companion.INVALID_INDEX import com.android.systemui.qs.panels.ui.compose.dragAndDropRemoveZone import com.android.systemui.qs.panels.ui.compose.dragAndDropTileList import com.android.systemui.qs.panels.ui.compose.dragAndDropTileSource @@ -152,7 +157,6 @@ import com.android.systemui.qs.panels.ui.model.AvailableTileGridCell import com.android.systemui.qs.panels.ui.model.GridCell import com.android.systemui.qs.panels.ui.model.SpacerGridCell import com.android.systemui.qs.panels.ui.model.TileGridCell -import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec @@ -161,7 +165,6 @@ import com.android.systemui.res.R import kotlin.math.abs import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay import kotlinx.coroutines.launch object TileType @@ -225,7 +228,7 @@ fun DefaultEditTileGrid( columns: Int, largeTilesSpan: Int, modifier: Modifier, - onAddTile: (TileSpec) -> Unit, + onAddTile: (TileSpec, Int) -> Unit, onRemoveTile: (TileSpec) -> Unit, onSetTiles: (List<TileSpec>) -> Unit, onResize: (TileSpec, toIcon: Boolean) -> Unit, @@ -243,6 +246,15 @@ fun DefaultEditTileGrid( null } + LaunchedEffect(selectionState.placementEvent) { + selectionState.placementEvent?.let { event -> + listState + .targetIndexForPlacement(event) + .takeIf { it != INVALID_INDEX } + ?.let { onAddTile(event.movingSpec, it) } + } + } + Scaffold( containerColor = Color.Transparent, topBar = { EditModeTopBar(onStopEditing = onStopEditing, onReset = reset) }, @@ -272,29 +284,23 @@ fun DefaultEditTileGrid( .padding(top = innerPadding.calculateTopPadding()) .clipScrollableContainer(Orientation.Vertical) .verticalScroll(scrollState) - .dragAndDropRemoveZone(listState, onRemoveTile), + .dragAndDropRemoveZone(listState) { spec, removalEnabled -> + if (removalEnabled) { + // If removal is enabled, remove the tile + onRemoveTile(spec) + } else { + // Otherwise submit the new tile ordering + onSetTiles(listState.tileSpecs()) + selectionState.select(spec) + } + }, ) { - AnimatedContent( - targetState = listState.dragInProgress || selectionState.selected, - label = "QSEditHeader", - contentAlignment = Alignment.Center, + CurrentTilesGridHeader( + listState, + selectionState, + onRemoveTile, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp), - ) { showRemoveTarget -> - EditGridHeader { - if (showRemoveTarget) { - RemoveTileTarget { - selectionState.selection?.let { - selectionState.unSelect() - onRemoveTile(it) - } - } - } else { - EditGridCenteredText( - text = stringResource(id = R.string.drag_to_rearrange_tiles) - ) - } - } - } + ) CurrentTilesGrid( listState, @@ -315,7 +321,7 @@ fun DefaultEditTileGrid( // Using the fully qualified name here as a workaround for AnimatedVisibility // not being available from a Box androidx.compose.animation.AnimatedVisibility( - visible = !listState.dragInProgress, + visible = !listState.dragInProgress && !selectionState.placementEnabled, enter = fadeIn(), exit = fadeOut(), ) { @@ -340,7 +346,7 @@ fun DefaultEditTileGrid( availableTiles, selectionState, columns, - onAddTile, + { onAddTile(it, listState.tileSpecs().size) }, // Add to the end listState, ) } @@ -398,6 +404,76 @@ private fun AutoScrollGrid( } } +private enum class EditModeHeaderState { + Remove, + Place, + Idle, +} + +@Composable +private fun rememberEditModeState( + listState: EditTileListState, + selectionState: MutableSelectionState, +): State<EditModeHeaderState> { + val editGridHeaderState = remember { mutableStateOf(EditModeHeaderState.Idle) } + LaunchedEffect( + listState.dragInProgress, + selectionState.selected, + selectionState.placementEnabled, + ) { + val canRemove = + listState.isDraggedCellRemovable || + selectionState.selection?.let { listState.isRemovable(it) } ?: false + + editGridHeaderState.value = + when { + selectionState.placementEnabled -> EditModeHeaderState.Place + canRemove -> EditModeHeaderState.Remove + else -> EditModeHeaderState.Idle + } + } + + return editGridHeaderState +} + +@Composable +private fun CurrentTilesGridHeader( + listState: EditTileListState, + selectionState: MutableSelectionState, + onRemoveTile: (TileSpec) -> Unit, + modifier: Modifier = Modifier, +) { + val editGridHeaderState by rememberEditModeState(listState, selectionState) + + AnimatedContent( + targetState = editGridHeaderState, + label = "QSEditHeader", + contentAlignment = Alignment.Center, + modifier = modifier, + ) { state -> + EditGridHeader { + when (state) { + EditModeHeaderState.Remove -> { + RemoveTileTarget { + selectionState.selection?.let { + selectionState.unSelect() + onRemoveTile(it) + } + } + } + EditModeHeaderState.Place -> { + EditGridCenteredText(text = stringResource(id = R.string.tap_to_position_tile)) + } + EditModeHeaderState.Idle -> { + EditGridCenteredText( + text = stringResource(id = R.string.drag_to_rearrange_tiles) + ) + } + } + } + } +} + @Composable private fun EditGridHeader( modifier: Modifier = Modifier, @@ -484,8 +560,14 @@ private fun CurrentTilesGrid( } .testTag(CURRENT_TILES_GRID_TEST_TAG), ) { - EditTiles(cells, listState, selectionState, coroutineScope, largeTilesSpan, onRemoveTile) { - resizingOperation -> + EditTiles( + cells, + listState, + selectionState, + coroutineScope, + largeTilesSpan, + onRemoveTile = onRemoveTile, + ) { resizingOperation -> when (resizingOperation) { is TemporaryResizeOperation -> { currentListState.resizeTile(resizingOperation.spec, resizingOperation.toIcon) @@ -585,6 +667,7 @@ private fun GridCell.key(index: Int): Any { * @param selectionState the [MutableSelectionState] for this grid * @param coroutineScope the [CoroutineScope] to be used for the tiles * @param largeTilesSpan the width used for large tiles + * @param onRemoveTile the callback when a tile is removed from this grid * @param onResize the callback when a tile has a new [ResizeOperation] */ fun LazyGridScope.EditTiles( @@ -628,12 +711,33 @@ fun LazyGridScope.EditTiles( modifier = Modifier.animateItem(), ) } - is SpacerGridCell -> SpacerGridCell() + is SpacerGridCell -> + SpacerGridCell( + Modifier.pointerInput(Unit) { + detectTapGestures(onTap = { selectionState.onTap(index) }) + } + ) } } } @Composable +private fun rememberTileState( + tile: EditTileViewModel, + selectionState: MutableSelectionState, +): State<TileState> { + val tileState = remember { mutableStateOf(TileState.None) } + val canShowRemovalBadge = tile.isRemovable + + LaunchedEffect(selectionState.selection, selectionState.placementEnabled, canShowRemovalBadge) { + tileState.value = + selectionState.tileStateFor(tile.tileSpec, tileState.value, canShowRemovalBadge) + } + + return tileState +} + +@Composable private fun TileGridCell( cell: TileGridCell, index: Int, @@ -646,29 +750,7 @@ private fun TileGridCell( modifier: Modifier = Modifier, ) { val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1) - val canShowRemovalBadge = cell.tile.availableEditActions.contains(AvailableEditActions.REMOVE) - var tileState by remember { mutableStateOf(TileState.None) } - - LaunchedEffect(selectionState.selection, canShowRemovalBadge) { - tileState = - when { - selectionState.selection == cell.tile.tileSpec -> { - if (tileState == TileState.None && canShowRemovalBadge) { - // The tile decoration is None if a tile is newly composed OR the removal - // badge can't be shown. - // For newly composed and selected tiles, such as dragged tiles or moved - // tiles from resizing, introduce a short delay. This avoids clipping issues - // on the border and resizing handle, as well as letting the selection - // animation play correctly. - delay(250) - } - TileState.Selected - } - canShowRemovalBadge -> TileState.Removable - else -> TileState.None - } - } - + val tileState by rememberTileState(cell.tile, selectionState) val resizingState = rememberResizingState(cell.tile.tileSpec, cell.isIcon) val progress: () -> Float = { if (tileState == TileState.Selected) { @@ -696,12 +778,16 @@ private fun TileGridCell( with(LocalDensity.current) { (largeTilesSpan - 1) * TileArrangementPadding.roundToPx() } val colors = EditModeTileDefaults.editTileColors() val toggleSizeLabel = stringResource(R.string.accessibility_qs_edit_toggle_tile_size_action) - val clickLabel = + val togglePlacementModeLabel = + stringResource(R.string.accessibility_qs_edit_toggle_placement_mode) + val decorationClickLabel = when (tileState) { - TileState.None -> null TileState.Removable -> stringResource(id = R.string.accessibility_qs_edit_remove_tile_action) TileState.Selected -> toggleSizeLabel + TileState.None, + TileState.Placeable, + TileState.GreyedOut -> null } InteractiveTileContainer( tileState = tileState, @@ -720,8 +806,13 @@ private fun TileGridCell( coroutineScope.launch { resizingState.toggleCurrentValue() } } }, - onClickLabel = clickLabel, + onClickLabel = decorationClickLabel, ) { + val placeableColor = MaterialTheme.colorScheme.primary.copy(alpha = .4f) + val backgroundColor by + animateColorAsState( + if (tileState == TileState.Placeable) placeableColor else colors.background + ) Box( modifier .fillMaxSize() @@ -734,7 +825,11 @@ private fun TileGridCell( CustomAccessibilityAction(toggleSizeLabel) { onResize(FinalResizeOperation(cell.tile.tileSpec, !cell.isIcon)) true - } + }, + CustomAccessibilityAction(togglePlacementModeLabel) { + selectionState.togglePlacementMode(cell.tile.tileSpec) + true + }, ) } .selectableTile(cell.tile.tileSpec, selectionState) @@ -744,9 +839,14 @@ private fun TileGridCell( DragType.Move, selectionState::unSelect, ) - .tileBackground(colors.background) + .tileBackground { backgroundColor } ) { - EditTile(tile = cell.tile, state = resizingState, progress = progress) + EditTile( + tile = cell.tile, + tileState = tileState, + state = resizingState, + progress = progress, + ) } } } @@ -791,7 +891,7 @@ private fun AvailableTileGridCell( } else { Modifier } - Box(draggableModifier.fillMaxSize().tileBackground(colors.background)) { + Box(draggableModifier.fillMaxSize().tileBackground { colors.background }) { // Icon SmallTileContent( iconProvider = { cell.tile.icon }, @@ -834,11 +934,13 @@ private fun SpacerGridCell(modifier: Modifier = Modifier) { @Composable fun EditTile( tile: EditTileViewModel, + tileState: TileState, state: ResizingState, progress: () -> Float, colors: TileColors = EditModeTileDefaults.editTileColors(), ) { val iconSizeDiff = CommonTileDefaults.IconSize - CommonTileDefaults.LargeTileIconSize + val alpha by animateFloatAsState(if (tileState == TileState.GreyedOut) .4f else 1f) Row( horizontalArrangement = spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically, @@ -871,7 +973,8 @@ fun EditTile( placeable.place(startPadding.roundToInt(), 0) } } - .largeTilePadding(), + .largeTilePadding() + .graphicsLayer { this.alpha = alpha }, ) { // Icon Box(Modifier.size(ToggleTargetSize)) { @@ -889,7 +992,7 @@ fun EditTile( label = tile.label.text, secondaryLabel = tile.appName?.text, colors = colors, - modifier = Modifier.weight(1f).graphicsLayer { alpha = progress() }, + modifier = Modifier.weight(1f).graphicsLayer { this.alpha = progress() }, ) } } @@ -908,9 +1011,9 @@ private fun MeasureScope.iconHorizontalCenter(containerSize: Int): Float { CommonTileDefaults.TileStartPadding.toPx() } -private fun Modifier.tileBackground(color: Color): Modifier { +private fun Modifier.tileBackground(color: () -> Color): Modifier { // Clip tile contents from overflowing past the tile - return clip(RoundedCornerShape(InactiveCornerRadius)).drawBehind { drawRect(color) } + return clip(RoundedCornerShape(InactiveCornerRadius)).drawBehind { drawRect(color()) } } private object EditModeTileDefaults { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt index 984343a45797..233af548fff2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt @@ -42,7 +42,6 @@ import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel -import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.shared.ui.ElementKeys.toElementKey import com.android.systemui.res.R @@ -171,7 +170,7 @@ constructor( otherTiles = otherTiles, columns = columns, modifier = modifier, - onAddTile = { onAddTile(it, POSITION_AT_END) }, + onAddTile = onAddTile, onRemoveTile = onRemoveTile, onSetTiles = onSetTiles, onResize = iconTilesViewModel::resize, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt index 3dfde86bf8d9..50b29557683d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt @@ -16,15 +16,17 @@ package com.android.systemui.qs.panels.ui.compose.selection -import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput +import com.android.systemui.common.ui.compose.gestures.detectEagerTapGestures import com.android.systemui.qs.pipeline.shared.TileSpec +import kotlinx.coroutines.delay /** Creates the state of the current selected tile that is remembered across compositions. */ @Composable @@ -38,6 +40,17 @@ class MutableSelectionState { var selection by mutableStateOf<TileSpec?>(null) private set + /** + * Whether the current selection is in placement mode or not. + * + * A tile in placement mode can be positioned by tapping at the desired location in the grid. + */ + var placementEnabled by mutableStateOf(false) + private set + + /** Latest event from coming from placement mode. */ + var placementEvent by mutableStateOf<PlacementEvent?>(null) + val selected: Boolean get() = selection != null @@ -47,37 +60,122 @@ class MutableSelectionState { fun unSelect() { selection = null + exitPlacementMode() } -} -/** - * Listens for click events to select/unselect the given [TileSpec]. Use this on current tiles as - * they can be selected. - */ -fun Modifier.selectableTile( - tileSpec: TileSpec, - selectionState: MutableSelectionState, - onClick: () -> Unit = {}, -): Modifier { - return pointerInput(Unit) { - detectTapGestures( - onTap = { - if (selectionState.selection == tileSpec) { - selectionState.unSelect() - } else { - selectionState.select(tileSpec) + /** Selects [tileSpec] and enable placement mode. */ + fun enterPlacementMode(tileSpec: TileSpec) { + selection = tileSpec + placementEnabled = true + } + + /** Disable placement mode but maintains current selection. */ + private fun exitPlacementMode() { + placementEnabled = false + } + + fun togglePlacementMode(tileSpec: TileSpec) { + if (placementEnabled) exitPlacementMode() else enterPlacementMode(tileSpec) + } + + suspend fun tileStateFor( + tileSpec: TileSpec, + previousState: TileState, + canShowRemovalBadge: Boolean, + ): TileState { + return when { + placementEnabled && selection == tileSpec -> TileState.Placeable + placementEnabled -> TileState.GreyedOut + selection == tileSpec -> { + if (previousState == TileState.None && canShowRemovalBadge) { + // The tile decoration is None if a tile is newly composed OR the removal + // badge can't be shown. + // For newly composed and selected tiles, such as dragged tiles or moved + // tiles from resizing, introduce a short delay. This avoids clipping issues + // on the border and resizing handle, as well as letting the selection + // animation play correctly. + delay(250) } - onClick() + TileState.Selected } - ) + canShowRemovalBadge -> TileState.Removable + else -> TileState.None + } + } + + /** + * Tap callback on a tile. + * + * Tiles can be selected and placed using placement mode. + */ + fun onTap(tileSpec: TileSpec) { + when { + placementEnabled && selection == tileSpec -> { + exitPlacementMode() + } + placementEnabled -> { + selection?.let { placementEvent = PlacementEvent.PlaceToTileSpec(it, tileSpec) } + exitPlacementMode() + } + selection == tileSpec -> { + unSelect() + } + else -> { + select(tileSpec) + } + } + } + + /** + * Tap on a position. + * + * Use on grid items not associated with a [TileSpec], such as a spacer. Spacers can't be + * selected, but selections can be moved to their position. + */ + fun onTap(index: Int) { + when { + placementEnabled -> { + selection?.let { placementEvent = PlacementEvent.PlaceToIndex(it, index) } + exitPlacementMode() + } + selected -> { + unSelect() + } + } } } +// Not using data classes here as distinct placement events may have the same moving spec and target +@Stable +sealed interface PlacementEvent { + val movingSpec: TileSpec + + /** Placement event corresponding to [movingSpec] moving to [targetSpec]'s position */ + class PlaceToTileSpec(override val movingSpec: TileSpec, val targetSpec: TileSpec) : + PlacementEvent + + /** Placement event corresponding to [movingSpec] moving to [targetIndex] */ + class PlaceToIndex(override val movingSpec: TileSpec, val targetIndex: Int) : PlacementEvent +} + /** - * Listens for click events to unselect any tile. Use this on available tiles as they can't be - * selected. + * Listens for click events on selectable tiles. + * + * Use this on current tiles as they can be selected. + * + * @param tileSpec the [TileSpec] of the tile this modifier is applied to + * @param selectionState the [MutableSelectionState] representing the grid's selection */ @Composable -fun Modifier.clearSelectionTile(selectionState: MutableSelectionState): Modifier { - return pointerInput(Unit) { detectTapGestures(onTap = { selectionState.unSelect() }) } +fun Modifier.selectableTile(tileSpec: TileSpec, selectionState: MutableSelectionState): Modifier { + return pointerInput(Unit) { + detectEagerTapGestures( + doubleTapEnabled = { + // Double tap enabled if where not in placement mode already + !selectionState.placementEnabled + }, + onDoubleTap = { selectionState.enterPlacementMode(tileSpec) }, + onTap = { selectionState.onTap(tileSpec) }, + ) + } } 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 57f63c755b43..8ffc4be88e7c 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 @@ -17,6 +17,7 @@ package com.android.systemui.qs.panels.ui.compose.selection import androidx.compose.animation.animateColor +import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Transition import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateFloatAsState @@ -37,9 +38,13 @@ import androidx.compose.material3.Icon import androidx.compose.material3.LocalMinimumInteractiveComponentSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +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.drawBehind @@ -73,7 +78,9 @@ import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.RES import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.ResizingPillHeight import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.ResizingPillWidth import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.SelectedBorderWidth +import com.android.systemui.qs.panels.ui.compose.selection.TileState.GreyedOut import com.android.systemui.qs.panels.ui.compose.selection.TileState.None +import com.android.systemui.qs.panels.ui.compose.selection.TileState.Placeable import com.android.systemui.qs.panels.ui.compose.selection.TileState.Removable import com.android.systemui.qs.panels.ui.compose.selection.TileState.Selected import kotlin.math.cos @@ -104,10 +111,11 @@ fun InteractiveTileContainer( ) { val transition: Transition<TileState> = updateTransition(tileState) val decorationColor by transition.animateColor() - val decorationAngle by transition.animateAngle() + val decorationAngle by animateAngle(tileState) val decorationSize by transition.animateSize() val decorationOffset by transition.animateOffset() - val decorationAlpha by transition.animateFloat { state -> if (state == None) 0f else 1f } + val decorationAlpha by + transition.animateFloat { state -> if (state == Removable || state == Selected) 1f else 0f } val badgeIconAlpha by transition.animateFloat { state -> if (state == Removable) 1f else 0f } val selectionBorderAlpha by transition.animateFloat { state -> if (state == Selected) 1f else 0f } @@ -282,27 +290,61 @@ private fun Modifier.resizable(selected: Boolean, state: ResizingState): Modifie } enum class TileState { + /** Tile is displayed as-is, no additional decoration needed. */ None, + /** Tile can be removed by the user. This is displayed by a badge in the upper end corner. */ Removable, + /** + * Tile is selected and resizable. One tile can be selected at a time in the grid. This is when + * we display the resizing handle and a highlighted border around the tile. + */ Selected, + /** + * Tile placeable. This state means that the grid is in placement mode and this tile is + * selected. It should be highlighted to stand out in the grid. + */ + Placeable, + /** + * Tile is faded out. This state means that the grid is in placement mode and this tile isn't + * selected. It serves as a target to place the selected tile. + */ + GreyedOut, } @Composable private fun Transition<TileState>.animateColor(): State<Color> { return animateColor { state -> when (state) { - None -> Color.Transparent + None, + GreyedOut -> Color.Transparent Removable -> MaterialTheme.colorScheme.primaryContainer - Selected -> MaterialTheme.colorScheme.primary + Selected, + Placeable -> MaterialTheme.colorScheme.primary } } } +/** + * Animate the angle of the tile decoration based on the previous state + * + * Some [TileState] don't have a visible decoration, and the angle should only animate when going + * between visible states. + */ @Composable -private fun Transition<TileState>.animateAngle(): State<Float> { - return animateFloat { state -> - if (state == Removable) BADGE_ANGLE_RAD else RESIZING_PILL_ANGLE_RAD +private fun animateAngle(tileState: TileState): State<Float> { + val animatable = remember { Animatable(0f) } + var animate by remember { mutableStateOf(false) } + LaunchedEffect(tileState) { + val targetAngle = tileState.decorationAngle() + + if (targetAngle == null) { + animate = false + } else { + if (animate) animatable.animateTo(targetAngle) else animatable.snapTo(targetAngle) + animate = true + } } + return animatable.asState() } @Composable @@ -310,7 +352,9 @@ private fun Transition<TileState>.animateSize(): State<Size> { return animateSize { state -> with(LocalDensity.current) { when (state) { - None -> Size.Zero + None, + Placeable, + GreyedOut -> Size.Zero Removable -> Size(BadgeSize.toPx()) Selected -> Size(ResizingPillWidth.toPx(), ResizingPillHeight.toPx()) } @@ -323,7 +367,9 @@ private fun Transition<TileState>.animateOffset(): State<Offset> { return animateOffset { state -> with(LocalDensity.current) { when (state) { - None -> Offset.Zero + None, + Placeable, + GreyedOut -> Offset.Zero Removable -> Offset(BadgeXOffset.toPx(), BadgeYOffset.toPx()) Selected -> Offset(-SelectedBorderWidth.toPx(), 0f) } @@ -331,6 +377,16 @@ private fun Transition<TileState>.animateOffset(): State<Offset> { } } +private fun TileState.decorationAngle(): Float? { + return when (this) { + Removable -> BADGE_ANGLE_RAD + Selected -> RESIZING_PILL_ANGLE_RAD + None, + Placeable, + GreyedOut -> null // No visible decoration + } +} + private fun Size(size: Float) = Size(size, size) private fun offsetForAngle(angle: Float, radius: Float, center: Offset): Offset { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt index be6ce5c5b4f4..cf325f531c38 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt @@ -66,6 +66,9 @@ data class EditTileViewModel( ) : CategoryAndName { override val name get() = label.text + + val isRemovable + get() = availableEditActions.contains(AvailableEditActions.REMOVE) } enum class AvailableEditActions { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java index c60e3da9d833..49ec1baeeeee 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java @@ -48,11 +48,13 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QSTile.BooleanState; +import com.android.systemui.plugins.qs.TileDetailsViewModel; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.qs.tiles.dialog.CastDetailsViewModel; import com.android.systemui.res.R; import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor; import com.android.systemui.statusbar.connectivity.NetworkController; @@ -93,6 +95,7 @@ public class CastTile extends QSTileImpl<BooleanState> { private final ShadeDialogContextInteractor mShadeDialogContextInteractor; private boolean mCastTransportAllowed; private boolean mHotspotConnected; + private final CastDetailsViewModel.Factory mCastDetailsViewModelFactory; @Inject public CastTile( @@ -113,7 +116,8 @@ public class CastTile extends QSTileImpl<BooleanState> { ConnectivityRepository connectivityRepository, TileJavaAdapter javaAdapter, FeatureFlags featureFlags, - ShadeDialogContextInteractor shadeDialogContextInteractor + ShadeDialogContextInteractor shadeDialogContextInteractor, + CastDetailsViewModel.Factory castDetailsViewModelFactory ) { super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); @@ -124,6 +128,7 @@ public class CastTile extends QSTileImpl<BooleanState> { mJavaAdapter = javaAdapter; mFeatureFlags = featureFlags; mShadeDialogContextInteractor = shadeDialogContextInteractor; + mCastDetailsViewModelFactory = castDetailsViewModelFactory; mController.observe(this, mCallback); mKeyguard.observe(this, mCallback); if (!mFeatureFlags.isEnabled(SIGNAL_CALLBACK_DEPRECATION)) { @@ -172,12 +177,7 @@ public class CastTile extends QSTileImpl<BooleanState> { @Override protected void handleClick(@Nullable Expandable expandable) { - if (getState().state == Tile.STATE_UNAVAILABLE) { - return; - } - - List<CastDevice> activeDevices = getActiveDevices(); - if (willPopDialog()) { + handleClick(() -> { if (!mKeyguard.isShowing()) { showDialog(expandable); } else { @@ -187,16 +187,44 @@ public class CastTile extends QSTileImpl<BooleanState> { showDialog(null /* view */); }); } + }); + } + + @Override + public boolean getDetailsViewModel(Consumer<TileDetailsViewModel> callback) { + CastDetailsViewModel viewModel = mCastDetailsViewModelFactory + .create(mShadeDialogContextInteractor.getContext(), ROUTE_TYPE_REMOTE_DISPLAY); + handleClick(() -> { + if (!mKeyguard.isShowing()) { + callback.accept(viewModel); + } else { + mActivityStarter.dismissKeyguardThenExecute(() -> { + callback.accept(viewModel); + return false; + }, null /* cancelAction */, true/* afterKeyguardGone */); + } + }); + return true; + } + + private void handleClick(Runnable showPromptCallback) { + if (getState().state == Tile.STATE_UNAVAILABLE) { + return; + } + + List<CastDevice> activeDevices = getActiveDevices(); + if (willShowPrompt()) { + showPromptCallback.run(); } else { mController.stopCasting(activeDevices.get(0), StopReason.STOP_QS_TILE); } } - // We want to pop up the media route selection dialog if we either have no active devices - // (neither routes nor projection), or if we have an active route. In other cases, we assume - // that a projection is active. This is messy, but this tile never correctly handled the - // case where multiple devices were active :-/. - private boolean willPopDialog() { + // We want to pop up the media route selection dialog (or show the cast details view) if we + // either have no active devices (neither routes nor projection), or if we have an active + // route. In other cases, we assume that a projection is active. This is messy, but this tile + // never correctly handled the case where multiple devices were active :-/. + private boolean willShowPrompt() { List<CastDevice> activeDevices = getActiveDevices(); return activeDevices.isEmpty() || (activeDevices.get(0).getTag() instanceof RouteInfo); } @@ -303,7 +331,7 @@ public class CastTile extends QSTileImpl<BooleanState> { state.secondaryLabel = ""; } state.expandedAccessibilityClassName = Button.class.getName(); - state.forceExpandIcon = willPopDialog(); + state.forceExpandIcon = willShowPrompt(); } else { state.state = Tile.STATE_UNAVAILABLE; String noWifi = mContext.getString(R.string.quick_settings_cast_no_network); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsContent.kt new file mode 100644 index 000000000000..cd33c964ce1b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsContent.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.dialog + +import android.view.LayoutInflater +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import com.android.internal.R +import com.android.internal.app.MediaRouteControllerContentManager + +@Composable +fun CastDetailsContent(castDetailsViewModel: CastDetailsViewModel) { + if (castDetailsViewModel.shouldShowChooserDialog()) { + // TODO(b/378514236): Show the chooser UI here. + return + } + + val contentManager: MediaRouteControllerContentManager = remember { + castDetailsViewModel.createControllerContentManager() + } + + AndroidView( + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + factory = { context -> + // Inflate with the existing dialog xml layout + val view = + LayoutInflater.from(context).inflate(R.layout.media_route_controller_dialog, null) + contentManager.bindViews(view) + contentManager.onAttachedToWindow() + + view + }, + onRelease = { contentManager.onDetachedFromWindow() }, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModel.kt new file mode 100644 index 000000000000..72322eff8bed --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModel.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.dialog + +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Drawable +import android.provider.Settings +import com.android.internal.app.MediaRouteControllerContentManager +import com.android.internal.app.MediaRouteDialogPresenter +import com.android.systemui.plugins.qs.TileDetailsViewModel +import com.android.systemui.qs.tiles.base.domain.actions.QSTileIntentUserInputHandler +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** The view model used for the screen record details view in the Quick Settings */ +class CastDetailsViewModel +@AssistedInject +constructor( + private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, + @Assisted private val context: Context, + @Assisted private val routeTypes: Int, +) : MediaRouteControllerContentManager.Delegate, TileDetailsViewModel { + @AssistedFactory + fun interface Factory { + fun create(context: Context, routeTypes: Int): CastDetailsViewModel + } + + fun shouldShowChooserDialog(): Boolean { + return MediaRouteDialogPresenter.shouldShowChooserDialog(context, routeTypes) + } + + fun createControllerContentManager(): MediaRouteControllerContentManager { + return MediaRouteControllerContentManager(context, this) + } + + override fun clickOnSettingsButton() { + qsTileIntentUserActionHandler.handle( + /* expandable= */ null, + Intent(Settings.ACTION_CAST_SETTINGS), + ) + } + + // TODO(b/388321032): Replace this string with a string in a translatable xml file, + override val title: String + get() = "Cast screen to device" + + // TODO(b/388321032): Replace this string with a string in a translatable xml file, + override val subTitle: String + get() = "Searching for devices..." + + override fun setMediaRouteDeviceTitle(title: CharSequence?) { + // TODO(b/378514236): Finish implementing this function. + } + + override fun setMediaRouteDeviceIcon(icon: Drawable?) { + // TODO(b/378514236): Finish implementing this function. + } + + override fun dismissView() { + // TODO(b/378514236): Finish implementing this function. + } +} diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt index 263ef09ea767..5d4a774d77f9 100644 --- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt @@ -19,14 +19,18 @@ package com.android.systemui.reardisplay import android.content.Context import android.hardware.devicestate.DeviceStateManager import android.hardware.devicestate.feature.flags.Flags +import android.os.Handler +import android.view.accessibility.AccessibilityManager import androidx.annotation.VisibleForTesting import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.domain.interactor.RearDisplayStateInteractor import com.android.systemui.statusbar.phone.SystemUIDialog +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -52,6 +56,8 @@ internal constructor( private val rearDisplayInnerDialogDelegateFactory: RearDisplayInnerDialogDelegate.Factory, @Application private val scope: CoroutineScope, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val accessibilityManager: AccessibilityManager, + @Background private val handler: Handler, ) : CoreStartable, AutoCloseable { companion object { @@ -77,6 +83,12 @@ internal constructor( override fun start() { if (Flags.deviceStateRdmV2()) { var dialog: SystemUIDialog? = null + var touchExplorationEnabled = AtomicBoolean(false) + + accessibilityManager.addTouchExplorationStateChangeListener( + { enabled -> touchExplorationEnabled.set(enabled) }, + handler, + ) keyguardUpdateMonitor.registerCallback(keyguardCallback) @@ -99,6 +111,7 @@ internal constructor( rearDisplayInnerDialogDelegateFactory.create( rearDisplayContext, deviceStateManager::cancelStateRequest, + touchExplorationEnabled.get(), ) dialog = delegate.createDialog().apply { show() } } diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt index f5facf42ee67..96f1bd270239 100644 --- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt @@ -20,7 +20,10 @@ import android.annotation.SuppressLint import android.content.Context import android.os.Bundle import android.view.MotionEvent +import android.view.View +import android.widget.Button import android.widget.SeekBar +import android.widget.TextView import com.android.systemui.haptics.slider.HapticSlider import com.android.systemui.haptics.slider.HapticSliderPlugin import com.android.systemui.haptics.slider.HapticSliderViewBinder @@ -45,6 +48,7 @@ class RearDisplayInnerDialogDelegate internal constructor( private val systemUIDialogFactory: SystemUIDialog.Factory, @Assisted private val rearDisplayContext: Context, + @Assisted private val touchExplorationEnabled: Boolean, private val vibratorHelper: VibratorHelper, private val msdlPlayer: MSDLPlayer, private val systemClock: SystemClock, @@ -82,6 +86,7 @@ internal constructor( fun create( rearDisplayContext: Context, onCanceledRunnable: Runnable, + touchExplorationEnabled: Boolean, ): RearDisplayInnerDialogDelegate } @@ -95,11 +100,32 @@ internal constructor( @SuppressLint("ClickableViewAccessibility") override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { + dialog.apply { setContentView(R.layout.activity_rear_display_enabled) setCanceledOnTouchOutside(false) + requireViewById<Button>(R.id.cancel_button).let { it -> + if (!touchExplorationEnabled) { + return@let + } + + it.visibility = View.VISIBLE + it.setOnClickListener { onCanceledRunnable.run() } + } + + requireViewById<TextView>(R.id.seekbar_instructions).let { it -> + if (touchExplorationEnabled) { + it.visibility = View.GONE + } + } + requireViewById<SeekBar>(R.id.seekbar).let { it -> + if (touchExplorationEnabled) { + it.visibility = View.GONE + return@let + } + // Create and bind the HapticSliderPlugin val hapticSliderPlugin = HapticSliderPlugin( diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 3ad0867192d3..06fc8610c97b 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -33,10 +33,8 @@ import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor import com.android.systemui.bouncer.shared.logging.BouncerUiEvent import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorActual -import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.DisplayId import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor @@ -82,6 +80,7 @@ import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.kotlin.sample import com.android.systemui.util.printSection import com.android.systemui.util.println +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.google.android.msdl.data.model.MSDLToken import com.google.android.msdl.domain.MSDLPlayer import dagger.Lazy @@ -123,7 +122,6 @@ constructor( private val bouncerInteractor: BouncerInteractor, private val keyguardInteractor: KeyguardInteractor, private val sysUiState: SysUiState, - @DisplayId private val displayId: Int, private val sceneLogger: SceneLogger, @FalsingCollectorActual private val falsingCollector: FalsingCollector, private val falsingManager: FalsingManager, @@ -197,7 +195,8 @@ constructor( return } - printSection("Scene state") { + printSection("Framework state") { + println("isVisible", sceneInteractor.isVisible.value) println("currentScene", sceneInteractor.currentScene.value.debugName) println( "currentOverlays", @@ -732,21 +731,26 @@ constructor( sceneInteractor.transitionState .mapNotNull { it as? ObservableTransitionState.Idle } .distinctUntilChanged(), + sceneInteractor.isVisible, occlusionInteractor.invisibleDueToOcclusion, - ) { idleState, invisibleDueToOcclusion -> + ) { idleState, isVisible, invisibleDueToOcclusion -> SceneContainerPlugin.SceneContainerPluginState( scene = idleState.currentScene, overlays = idleState.currentOverlays, + isVisible = isVisible, invisibleDueToOcclusion = invisibleDueToOcclusion, ) } - .collect { sceneContainerPluginState -> + .map { sceneContainerPluginState -> + SceneContainerPlugin.EvaluatorByFlag.map { (flag, evaluator) -> + flag to evaluator(sceneContainerPluginState) + } + .toMap() + } + .distinctUntilChanged() + .collect { flags -> sysUiState.updateFlags( - displayId, - *SceneContainerPlugin.EvaluatorByFlag.map { (flag, evaluator) -> - flag to evaluator.invoke(sceneContainerPluginState) - } - .toTypedArray(), + *(flags.entries.map { (key, value) -> key to value }).toTypedArray() ) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 24e7976011f4..b90624245cc5 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -206,6 +206,7 @@ import com.google.android.msdl.data.model.MSDLToken; import com.google.android.msdl.domain.MSDLPlayer; import dagger.Lazy; + import kotlin.Unit; import kotlinx.coroutines.CoroutineDispatcher; @@ -4267,7 +4268,8 @@ public final class NotificationPanelViewController implements == AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.getId() || action == AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP.getId()) { - mStatusBarKeyguardViewManager.showPrimaryBouncer(true); + mStatusBarKeyguardViewManager.showPrimaryBouncer(true, + "NotificationPanelViewController#performAccessibilityAction"); return true; } return super.performAccessibilityAction(host, action, args); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index e6834ad6cdda..b211f0729318 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -97,6 +97,11 @@ constructor( loggingReason = "ShadeControllerSceneImpl.instantCollapseShade", transitionKey = Instant, ) + + shadeInteractor.collapseQuickSettingsShade( + loggingReason = "ShadeControllerSceneImpl.instantCollapseShade", + transitionKey = Instant, + ) } override fun animateCollapseShade( diff --git a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java index 4b8cc00e1c28..421b5ea23278 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java @@ -39,9 +39,13 @@ import androidx.annotation.VisibleForTesting; import com.android.keyguard.CarrierTextManager; import com.android.settingslib.AccessibilityContentDescriptions; import com.android.settingslib.mobile.TelephonyIcons; +import com.android.systemui.Flags; import com.android.systemui.dagger.SysUISingleton; +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.kairos.ExperimentalKairosApi; +import com.android.systemui.kairos.KairosNetwork; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.res.R; import com.android.systemui.shade.ShadeDisplayAware; @@ -52,17 +56,30 @@ import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider; import com.android.systemui.statusbar.phone.StatusBarLocation; import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos; import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder; import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView; import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModelKairos; import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel; import com.android.systemui.util.CarrierConfigTracker; +import dagger.Lazy; + +import kotlin.OptIn; +import kotlin.Pair; + +import kotlinx.coroutines.CoroutineScope; +import kotlinx.coroutines.Job; + +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CancellationException; import java.util.function.Consumer; import javax.inject.Inject; +@OptIn(markerClass = ExperimentalKairosApi.class) public class ShadeCarrierGroupController { private static final String TAG = "ShadeCarrierGroup"; @@ -100,38 +117,43 @@ public class ShadeCarrierGroupController { private final ShadeCarrierGroupControllerLogger mLogger; + private final Lazy<MobileUiAdapterKairos> mMobileUiAdapterKairos; + private final CoroutineScope mAppScope; + private final KairosNetwork mKairosNetwork; + private final SignalCallback mSignalCallback = new SignalCallback() { - @Override - public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) { - int slotIndex = getSlotIndex(indicators.subId); - if (slotIndex >= SIM_SLOTS) { - Log.w(TAG, "setMobileDataIndicators - slot: " + slotIndex); - return; - } - if (slotIndex == INVALID_SIM_SLOT_INDEX) { - Log.e(TAG, "Invalid SIM slot index for subscription: " + indicators.subId); - return; - } - mInfos[slotIndex] = new CellSignalState( - indicators.statusIcon.visible, - indicators.statusIcon.icon, - indicators.statusIcon.contentDescription, - indicators.typeContentDescription.toString(), - indicators.roaming - ); - mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget(); - } + @Override + public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) { + int slotIndex = getSlotIndex(indicators.subId); + if (slotIndex >= SIM_SLOTS) { + Log.w(TAG, "setMobileDataIndicators - slot: " + slotIndex); + return; + } + if (slotIndex == INVALID_SIM_SLOT_INDEX) { + Log.e(TAG, "Invalid SIM slot index for subscription: " + indicators.subId); + return; + } + mInfos[slotIndex] = new CellSignalState( + indicators.statusIcon.visible, + indicators.statusIcon.icon, + indicators.statusIcon.contentDescription, + indicators.typeContentDescription.toString(), + indicators.roaming + ); + mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget(); + } - @Override - public void setNoSims(boolean hasNoSims, boolean simDetected) { - if (hasNoSims) { - for (int i = 0; i < SIM_SLOTS; i++) { - mInfos[i] = mInfos[i].changeVisibility(false); - } - } - mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget(); + @Override + public void setNoSims(boolean hasNoSims, boolean simDetected) { + if (hasNoSims) { + for (int i = 0; i < SIM_SLOTS; i++) { + mInfos[i] = mInfos[i].changeVisibility(false); } - }; + } + mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget(); + } + }; + private final ArrayList<Job> mBindingJobs = new ArrayList<>(); private static class Callback implements CarrierTextManager.CarrierTextCallback { private H mHandler; @@ -159,7 +181,10 @@ public class ShadeCarrierGroupController { SlotIndexResolver slotIndexResolver, MobileUiAdapter mobileUiAdapter, MobileContextProvider mobileContextProvider, - StatusBarPipelineFlags statusBarPipelineFlags + StatusBarPipelineFlags statusBarPipelineFlags, + Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos, + CoroutineScope appScope, + KairosNetwork kairosNetwork ) { mContext = context; mActivityStarter = activityStarter; @@ -174,6 +199,9 @@ public class ShadeCarrierGroupController { .build(); mCarrierConfigTracker = carrierConfigTracker; mSlotIndexResolver = slotIndexResolver; + mMobileUiAdapterKairos = mobileUiAdapterKairos; + mAppScope = appScope; + mKairosNetwork = kairosNetwork; View.OnClickListener onClickListener = v -> { if (!v.isVisibleToUser()) { return; @@ -195,8 +223,12 @@ public class ShadeCarrierGroupController { mMobileIconsViewModel = mobileUiAdapter.getMobileIconsViewModel(); if (mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) { - mobileUiAdapter.setShadeCarrierGroupController(this); - MobileIconsBinder.bind(view, mMobileIconsViewModel); + if (Flags.statusBarMobileIconKairos()) { + mobileUiAdapterKairos.get().setShadeCarrierGroupController(this); + } else { + mobileUiAdapter.setShadeCarrierGroupController(this); + MobileIconsBinder.bind(view, mMobileIconsViewModel); + } } mCarrierDividers[0] = view.getCarrierDivider1(); @@ -243,21 +275,52 @@ public class ShadeCarrierGroupController { List<IconData> iconDataList = processSubIdList(subIds); - for (IconData iconData : iconDataList) { - ShadeCarrier carrier = mCarrierGroups[iconData.slotIndex]; - - Context mobileContext = - mMobileContextProvider.getMobileContextForSub(iconData.subId, mContext); - ModernShadeCarrierGroupMobileView modernMobileView = ModernShadeCarrierGroupMobileView - .constructAndBind( - mobileContext, - mMobileIconsViewModel.getLogger(), - "mobile_carrier_shade_group", - (ShadeCarrierGroupMobileIconViewModel) mMobileIconsViewModel - .viewModelForSub(iconData.subId, - StatusBarLocation.SHADE_CARRIER_GROUP) - ); - carrier.addModernMobileView(modernMobileView); + if (Flags.statusBarMobileIconKairos()) { + for (Job job : mBindingJobs) { + job.cancel(new CancellationException()); + } + mBindingJobs.clear(); + MobileIconsViewModelKairos mobileIconsViewModel = + mMobileUiAdapterKairos.get().getMobileIconsViewModel(); + for (IconData iconData : iconDataList) { + ShadeCarrier carrier = mCarrierGroups[iconData.slotIndex]; + + Context mobileContext = + mMobileContextProvider.getMobileContextForSub(iconData.subId, mContext); + + Pair<ModernShadeCarrierGroupMobileView, Job> viewAndJob = + ModernShadeCarrierGroupMobileView.constructAndBind( + mobileContext, + mobileIconsViewModel.getLogger(), + "mobile_carrier_shade_group", + mobileIconsViewModel.shadeCarrierGroupIcon(iconData.subId), + mAppScope, + iconData.subId, + StatusBarLocation.SHADE_CARRIER_GROUP, + mKairosNetwork + ); + mBindingJobs.add(viewAndJob.getSecond()); + carrier.addModernMobileView(viewAndJob.getFirst()); + } + } else { + for (IconData iconData : iconDataList) { + ShadeCarrier carrier = mCarrierGroups[iconData.slotIndex]; + + Context mobileContext = + mMobileContextProvider.getMobileContextForSub(iconData.subId, mContext); + + ModernShadeCarrierGroupMobileView modernMobileView = + ModernShadeCarrierGroupMobileView + .constructAndBind( + mobileContext, + mMobileIconsViewModel.getLogger(), + "mobile_carrier_shade_group", + (ShadeCarrierGroupMobileIconViewModel) mMobileIconsViewModel + .viewModelForSub(iconData.subId, + StatusBarLocation.SHADE_CARRIER_GROUP) + ); + carrier.addModernMobileView(modernMobileView); + } } } @@ -288,7 +351,6 @@ public class ShadeCarrierGroupController { * Sets a {@link OnSingleCarrierChangedListener}. * * This will get notified when the number of carriers changes between 1 and "not one". - * @param listener */ public void setOnSingleCarrierChangedListener( @Nullable OnSingleCarrierChangedListener listener) { @@ -489,6 +551,9 @@ public class ShadeCarrierGroupController { private final MobileUiAdapter mMobileUiAdapter; private final MobileContextProvider mMobileContextProvider; private final StatusBarPipelineFlags mStatusBarPipelineFlags; + private final CoroutineScope mAppScope; + private final KairosNetwork mKairosNetwork; + private final Lazy<MobileUiAdapterKairos> mMobileUiAdapterKairos; @Inject public Builder( @@ -503,7 +568,10 @@ public class ShadeCarrierGroupController { SlotIndexResolver slotIndexResolver, MobileUiAdapter mobileUiAdapter, MobileContextProvider mobileContextProvider, - StatusBarPipelineFlags statusBarPipelineFlags + StatusBarPipelineFlags statusBarPipelineFlags, + @Application CoroutineScope appScope, + KairosNetwork kairosNetwork, + Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos ) { mActivityStarter = activityStarter; mHandler = handler; @@ -517,6 +585,9 @@ public class ShadeCarrierGroupController { mMobileUiAdapter = mobileUiAdapter; mMobileContextProvider = mobileContextProvider; mStatusBarPipelineFlags = statusBarPipelineFlags; + mAppScope = appScope; + mKairosNetwork = kairosNetwork; + mMobileUiAdapterKairos = mobileUiAdapterKairos; } public Builder setShadeCarrierGroup(ShadeCarrierGroup view) { @@ -538,8 +609,10 @@ public class ShadeCarrierGroupController { mSlotIndexResolver, mMobileUiAdapter, mMobileContextProvider, - mStatusBarPipelineFlags - ); + mStatusBarPipelineFlags, + mMobileUiAdapterKairos, + mAppScope, + mKairosNetwork); } } @@ -571,7 +644,8 @@ public class ShadeCarrierGroupController { public static class SubscriptionManagerSlotIndexResolver implements SlotIndexResolver { @Inject - public SubscriptionManagerSlotIndexResolver() {} + public SubscriptionManagerSlotIndexResolver() { + } @Override public int getSlotIndex(int subscriptionId) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt index 52de0abf7d3c..043742245227 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt @@ -187,12 +187,18 @@ constructor( override fun collapseNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) { if (shadeModeInteractor.isDualShade) { - // TODO(b/356596436): Hide without animation if transitionKey is Instant. - sceneInteractor.hideOverlay( - overlay = Overlays.NotificationsShade, - loggingReason = loggingReason, - transitionKey = transitionKey, - ) + if (transitionKey == Instant) { + sceneInteractor.instantlyHideOverlay( + overlay = Overlays.NotificationsShade, + loggingReason = loggingReason, + ) + } else { + sceneInteractor.hideOverlay( + overlay = Overlays.NotificationsShade, + loggingReason = loggingReason, + transitionKey = transitionKey, + ) + } } else if (transitionKey == Instant) { // TODO(b/356596436): Define instant transition instead of snapToScene(). sceneInteractor.snapToScene( @@ -215,12 +221,18 @@ constructor( bypassNotificationsShade: Boolean, ) { if (shadeModeInteractor.isDualShade) { - // TODO(b/356596436): Hide without animation if transitionKey is Instant. - sceneInteractor.hideOverlay( - overlay = Overlays.QuickSettingsShade, - loggingReason = loggingReason, - transitionKey = transitionKey, - ) + if (transitionKey == Instant) { + sceneInteractor.instantlyHideOverlay( + overlay = Overlays.QuickSettingsShade, + loggingReason = loggingReason, + ) + } else { + sceneInteractor.hideOverlay( + overlay = Overlays.QuickSettingsShade, + loggingReason = loggingReason, + transitionKey = transitionKey, + ) + } return } 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 5609326362fc..88242762da78 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 @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalKairosApi::class) + package com.android.systemui.shade.ui.viewmodel import android.content.Context @@ -28,6 +30,8 @@ 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.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.KairosNetwork import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.plugins.ActivityStarter @@ -47,6 +51,7 @@ import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.TintedIconManager import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModelKairos import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import java.util.Locale @@ -76,6 +81,8 @@ constructor( private val tintedIconManagerFactory: TintedIconManager.Factory, private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, val statusBarIconController: StatusBarIconController, + val kairosNetwork: KairosNetwork, + val mobileIconsViewModelKairos: dagger.Lazy<MobileIconsViewModelKairos>, ) : ExclusiveActivatable() { private val hydrator = Hydrator("ShadeHeaderViewModel.hydrator") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index e44701dba87c..4daf61a895c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -64,6 +64,7 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.annotations.KeepForWeakReference; import com.android.internal.os.SomeArgs; +import com.android.internal.statusbar.DisableStates; import com.android.internal.statusbar.IAddTileResultCallback; import com.android.internal.statusbar.IStatusBar; import com.android.internal.statusbar.IUndoMediaTransferCallback; @@ -85,6 +86,7 @@ import java.io.FileOutputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Map; /** * This class takes the functions from IStatusBar that come in on @@ -184,6 +186,8 @@ public class CommandQueue extends IStatusBar.Stub implements private static final int MSG_TOGGLE_QUICK_SETTINGS_PANEL = 82 << MSG_SHIFT; private static final int MSG_WALLET_ACTION_LAUNCH_GESTURE = 83 << MSG_SHIFT; private static final int MSG_DISPLAY_REMOVE_SYSTEM_DECORATIONS = 85 << MSG_SHIFT; + private static final int MSG_DISABLE_ALL = 86 << MSG_SHIFT; + public static final int FLAG_EXCLUDE_NONE = 0; public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0; public static final int FLAG_EXCLUDE_RECENTS_PANEL = 1 << 1; @@ -654,7 +658,8 @@ public class CommandQueue extends IStatusBar.Stub implements /** * Called to notify that disable flags are updated. - * @see Callbacks#disable(int, int, int, boolean). + * @see Callbacks#disable(int, int, int, boolean) + * @see Callbacks#disableForAllDisplays(DisableStates) */ public void disable(int displayId, @DisableFlags int state1, @Disable2Flags int state2, boolean animate) { @@ -682,6 +687,27 @@ public class CommandQueue extends IStatusBar.Stub implements disable(displayId, state1, state2, true); } + @Override + public void disableForAllDisplays(DisableStates disableStates) throws RemoteException { + synchronized (mLock) { + for (Map.Entry<Integer, Pair<Integer, Integer>> displaysWithStates : + disableStates.displaysWithStates.entrySet()) { + int displayId = displaysWithStates.getKey(); + Pair<Integer, Integer> states = displaysWithStates.getValue(); + setDisabled(displayId, states.first, states.second); + } + mHandler.removeMessages(MSG_DISABLE_ALL); + Message msg = mHandler.obtainMessage(MSG_DISABLE_ALL, disableStates); + if (Looper.myLooper() == mHandler.getLooper()) { + // If its the right looper execute immediately so hides can be handled quickly. + mHandler.handleMessage(msg); + msg.recycle(); + } else { + msg.sendToTarget(); + } + } + } + /** * Apply current disable flags by {@link CommandQueue#disable(int, int, int, boolean)}. * @@ -1552,6 +1578,21 @@ public class CommandQueue extends IStatusBar.Stub implements args.argi4 != 0 /* animate */); } break; + case MSG_DISABLE_ALL: + DisableStates disableStates = (DisableStates) msg.obj; + boolean animate = disableStates.animate; + Map<Integer, Pair<Integer, Integer>> displaysWithDisableStates = + disableStates.displaysWithStates; + for (Map.Entry<Integer, Pair<Integer, Integer>> displayWithDisableStates : + displaysWithDisableStates.entrySet()) { + int displayId = displayWithDisableStates.getKey(); + Pair<Integer, Integer> states = displayWithDisableStates.getValue(); + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).disable(displayId, states.first, states.second, + animate); + } + } + break; case MSG_EXPAND_NOTIFICATIONS: for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).animateExpandNotificationsPanel(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java index 03c191e40ccf..2030606e4274 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java @@ -38,6 +38,7 @@ import com.android.internal.R; import com.android.internal.widget.CachingIconView; import com.android.internal.widget.ConversationLayout; import com.android.internal.widget.ImageFloatingTextView; +import com.android.systemui.statusbar.notification.icon.IconPack; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationContentView; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; @@ -61,10 +62,19 @@ public class NotificationGroupingUtil { private static final VisibilityApplicator VISIBILITY_APPLICATOR = new VisibilityApplicator(); private static final VisibilityApplicator APP_NAME_APPLICATOR = new AppNameApplicator(); private static final ResultApplicator LEFT_ICON_APPLICATOR = new LeftIconApplicator(); - private static final DataExtractor ICON_EXTRACTOR = new DataExtractor() { + + @VisibleForTesting + static final DataExtractor ICON_EXTRACTOR = new DataExtractor() { @Override public Object extractData(ExpandableNotificationRow row) { - return row.getEntry().getSbn().getNotification(); + if (NotificationBundleUi.isEnabled()) { + if (row.getEntryAdapter().getSbn() != null) { + return row.getEntryAdapter().getSbn().getNotification(); + } + return null; + } else { + return row.getEntry().getSbn().getNotification(); + } } }; @@ -253,7 +263,7 @@ public class NotificationGroupingUtil { if (NotificationBundleUi.isEnabled()) { sbn = row.getEntryAdapter() != null ? row.getEntryAdapter().getSbn() : null; } else { - sbn = row.getEntry().getSbn(); + sbn = row.getEntryLegacy().getSbn(); } return (sbn != null && sbn.getNotification().showsTime()); } @@ -357,7 +367,8 @@ public class NotificationGroupingUtil { boolean isEmpty(View view); } - private interface DataExtractor { + @VisibleForTesting + interface DataExtractor { Object extractData(ExpandableNotificationRow row); } @@ -395,13 +406,17 @@ public class NotificationGroupingUtil { } } - private abstract static class IconComparator implements ViewComparator { + @VisibleForTesting + static class IconComparator implements ViewComparator { @Override public boolean compare(View parent, View child, Object parentData, Object childData) { return false; } protected boolean hasSameIcon(Object parentData, Object childData) { + if (parentData == null || childData == null) { + return false; + } Icon parentIcon = ((Notification) parentData).getSmallIcon(); Icon childIcon = ((Notification) childData).getSmallIcon(); return parentIcon.sameAs(childIcon); @@ -411,6 +426,10 @@ public class NotificationGroupingUtil { * @return whether two ImageViews have the same colorFilterSet or none at all */ protected boolean hasSameColor(Object parentData, Object childData) { + if ((parentData == null && childData != null) + || (parentData != null && childData == null)) { + return false; + } int parentColor = ((Notification) parentData).color; int childColor = ((Notification) childData).color; return parentColor == childColor; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index 472dc823423e..e292bcf1f7a8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -24,6 +24,7 @@ import android.util.IndentingPrintWriter import android.util.Log import android.util.MathUtils import android.view.Choreographer +import android.view.Display import android.view.View import androidx.annotation.VisibleForTesting import androidx.dynamicanimation.animation.FloatPropertyCompat @@ -42,7 +43,9 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionChangeEvent import com.android.systemui.shade.ShadeExpansionListener +import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.shade.domain.interactor.ShadeModeInteractor +import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK import com.android.systemui.statusbar.phone.DozeParameters @@ -52,6 +55,7 @@ import com.android.systemui.util.WallpaperController import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor import com.android.wm.shell.appzoomout.AppZoomOut +import dagger.Lazy import java.io.PrintWriter import java.util.Optional import javax.inject.Inject @@ -83,6 +87,7 @@ constructor( private val appZoomOutOptional: Optional<AppZoomOut>, @Application private val applicationScope: CoroutineScope, dumpManager: DumpManager, + private val shadeDisplaysRepository: Lazy<ShadeDisplaysRepository>, ) : ShadeExpansionListener, Dumpable { companion object { private const val WAKE_UP_ANIMATION_ENABLED = true @@ -228,6 +233,14 @@ constructor( private data class WakeAndUnlockBlurData(val radius: Float, val useZoom: Boolean = true) + private val isShadeOnDefaultDisplay: Boolean + get() = + if (ShadeWindowGoesAround.isEnabled) { + shadeDisplaysRepository.get().displayId.value == Display.DEFAULT_DISPLAY + } else { + true + } + /** Blur radius of the wake and unlock animation on this frame, and whether to zoom out. */ private var wakeAndUnlockBlurData = WakeAndUnlockBlurData(0f) set(value) { @@ -265,9 +278,14 @@ constructor( var blur = shadeRadius.toInt() // If the blur comes from waking up, we don't want to zoom out the background val zoomOut = - if (shadeRadius != wakeAndUnlockBlurData.radius|| wakeAndUnlockBlurData.useZoom) - blurRadiusToZoomOut(blurRadius = shadeRadius) - else 0f + when { + // When the shade is in another display, we don't want to zoom out the background. + // Only the default display is supported right now. + !isShadeOnDefaultDisplay -> 0f + shadeRadius != wakeAndUnlockBlurData.radius || wakeAndUnlockBlurData.useZoom -> + blurRadiusToZoomOut(blurRadius = shadeRadius) + else -> 0f + } // Make blur be 0 if it is necessary to stop blur effect. if (scrimsVisible) { if (!Flags.notificationShadeBlur()) { @@ -353,7 +371,9 @@ constructor( interpolator = Interpolators.FAST_OUT_SLOW_IN addUpdateListener { animation: ValueAnimator -> wakeAndUnlockBlurData = - WakeAndUnlockBlurData(blurUtils.blurRadiusOfRatio(animation.animatedValue as Float)) + WakeAndUnlockBlurData( + blurUtils.blurRadiusOfRatio(animation.animatedValue as Float) + ) } addListener( object : AnimatorListenerAdapter() { @@ -428,8 +448,10 @@ constructor( applicationScope.launch { wallpaperInteractor.wallpaperSupportsAmbientMode.collect { supported -> wallpaperSupportsAmbientMode = supported - if (getNewWakeBlurRadius(prevDozeAmount) == wakeAndUnlockBlurData.radius - && !wakeAndUnlockBlurData.useZoom) { + if ( + getNewWakeBlurRadius(prevDozeAmount) == wakeAndUnlockBlurData.radius && + !wakeAndUnlockBlurData.useZoom + ) { // Update wake and unlock radius only if the previous value comes from wake-up. updateWakeBlurRadius(prevDozeAmount) } @@ -452,6 +474,21 @@ constructor( scheduleUpdate() } } + + applicationScope.launch { + windowRootViewBlurInteractor.isBlurCurrentlySupported.collect { supported -> + if (supported) { + // when battery saver changes, try scheduling an update. + scheduleUpdate() + } else { + // when blur becomes unsupported, no more updates will be scheduled, + // reset updateScheduled state. + updateScheduled = false + // reset blur and internal state to 0 + onBlurApplied(0, 0.0f) + } + } + } } fun addListener(listener: DepthListener) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt index 6ce350cb95f5..c862460ad6f7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt @@ -194,6 +194,7 @@ constructor( ): OngoingActivityChipModel.Active { return OngoingActivityChipModel.Active.Timer( key = KEY, + isImportantForPrivacy = true, icon = OngoingActivityChipModel.ChipIcon.SingleColorIcon( Icon.Resource( @@ -232,6 +233,7 @@ constructor( private fun createIconOnlyCastChip(deviceName: String?): OngoingActivityChipModel.Active { return OngoingActivityChipModel.Active.IconOnly( key = KEY, + isImportantForPrivacy = true, icon = OngoingActivityChipModel.ChipIcon.SingleColorIcon( Icon.Resource( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt index 1f2079d83e6f..356731cb3777 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt @@ -17,7 +17,7 @@ package com.android.systemui.statusbar.chips.notification.domain.model import com.android.systemui.statusbar.StatusBarIconView -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels /** Modeling all the data needed to render a status bar notification chip. */ data class NotificationChipModel( @@ -25,7 +25,7 @@ data class NotificationChipModel( /** The user-readable name of the app that posted this notification. */ val appName: String, val statusBarChipIconView: StatusBarIconView?, - val promotedContent: PromotedNotificationContentModel, + val promotedContent: PromotedNotificationContentModels, /** The time when the notification first appeared as promoted. */ val creationTime: Long, /** True if the app managing this notification is currently visible to the user. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index 1a802d634894..dfbd12d5a215 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -72,6 +72,8 @@ constructor( headsUpState: TopPinnedState ): OngoingActivityChipModel.Active { StatusBarNotifChips.unsafeAssertInNewMode() + // Chips are never shown when locked, so it's safe to use the version with sensitive content + val chipContent = promotedContent.privateVersion val contentDescription = getContentDescription(this.appName) val icon = if (this.statusBarChipIconView != null) { @@ -115,65 +117,62 @@ constructor( // If the user tapped this chip to show the HUN, we want to just show the icon because // the HUN will show the rest of the information. return OngoingActivityChipModel.Active.IconOnly( - this.key, - icon, - colors, - onClickListenerLegacy, - clickBehavior, + key = this.key, + icon = icon, + colors = colors, + onClickListenerLegacy = onClickListenerLegacy, + clickBehavior = clickBehavior, ) } - if (this.promotedContent.shortCriticalText != null) { + if (chipContent.shortCriticalText != null) { return OngoingActivityChipModel.Active.Text( - this.key, - icon, - colors, - this.promotedContent.shortCriticalText, - onClickListenerLegacy, - clickBehavior, + key = this.key, + icon = icon, + colors = colors, + text = chipContent.shortCriticalText, + onClickListenerLegacy = onClickListenerLegacy, + clickBehavior = clickBehavior, ) } - if ( - Flags.promoteNotificationsAutomatically() && - this.promotedContent.wasPromotedAutomatically - ) { + if (Flags.promoteNotificationsAutomatically() && chipContent.wasPromotedAutomatically) { // When we're promoting notifications automatically, the `when` time set on the // notification will likely just be set to the current time, which would cause the chip // to always show "now". We don't want early testers to get that experience since it's // not what will happen at launch, so just don't show any time.onometerstate return OngoingActivityChipModel.Active.IconOnly( - this.key, - icon, - colors, - onClickListenerLegacy, - clickBehavior, + key = this.key, + icon = icon, + colors = colors, + onClickListenerLegacy = onClickListenerLegacy, + clickBehavior = clickBehavior, ) } - if (this.promotedContent.time == null) { + if (chipContent.time == null) { return OngoingActivityChipModel.Active.IconOnly( - this.key, - icon, - colors, - onClickListenerLegacy, - clickBehavior, + key = this.key, + icon = icon, + colors = colors, + onClickListenerLegacy = onClickListenerLegacy, + clickBehavior = clickBehavior, ) } - when (this.promotedContent.time) { + when (chipContent.time) { is PromotedNotificationContentModel.When.Time -> { return if ( - this.promotedContent.time.currentTimeMillis >= + chipContent.time.currentTimeMillis >= systemClock.currentTimeMillis() + FUTURE_TIME_THRESHOLD_MILLIS ) { OngoingActivityChipModel.Active.ShortTimeDelta( - this.key, - icon, - colors, - time = this.promotedContent.time.currentTimeMillis, - onClickListenerLegacy, - clickBehavior, + key = this.key, + icon = icon, + colors = colors, + time = chipContent.time.currentTimeMillis, + onClickListenerLegacy = onClickListenerLegacy, + clickBehavior = clickBehavior, ) } else { // Don't show a `when` time that's close to now or in the past because it's @@ -185,21 +184,21 @@ constructor( // automatically handles this for us and we're hoping to launch the notification // chips at the same time as the Compose chips. return OngoingActivityChipModel.Active.IconOnly( - this.key, - icon, - colors, - onClickListenerLegacy, - clickBehavior, + key = this.key, + icon = icon, + colors = colors, + onClickListenerLegacy = onClickListenerLegacy, + clickBehavior = clickBehavior, ) } } is PromotedNotificationContentModel.When.Chronometer -> { return OngoingActivityChipModel.Active.Timer( - this.key, - icon, - colors, - startTimeMs = this.promotedContent.time.elapsedRealtimeMillis, - isEventInFuture = this.promotedContent.time.isCountDown, + key = this.key, + icon = icon, + colors = colors, + startTimeMs = chipContent.time.elapsedRealtimeMillis, + isEventInFuture = chipContent.time.isCountDown, onClickListenerLegacy = onClickListenerLegacy, clickBehavior = clickBehavior, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt index 572c7faf19e6..fce428dcb7e1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt @@ -94,9 +94,11 @@ constructor( TAG, LogLevel.INFO, {}, - { "State: Recording(taskPackage=null) due to force-start" }, + { + "State: Recording(hostPackage=null, taskPackage=null) due to force-start" + }, ) - ScreenRecordChipModel.Recording(recordedTask = null) + ScreenRecordChipModel.Recording(hostPackage = null, recordedTask = null) } else { when (screenRecordState) { is ScreenRecordModel.DoingNothing -> { @@ -124,13 +126,25 @@ constructor( } else { null } + val hostPackage = + if (mediaProjectionState is MediaProjectionState.Projecting) { + mediaProjectionState.hostPackage + } else { + null + } logger.log( TAG, LogLevel.INFO, - { str1 = recordedTask?.baseIntent?.component?.packageName }, - { "State: Recording(taskPackage=$str1)" }, + { + str1 = hostPackage + str2 = recordedTask?.baseIntent?.component?.packageName + }, + { "State: Recording(hostPackage=$str1, taskPackage=$str2)" }, + ) + ScreenRecordChipModel.Recording( + hostPackage = hostPackage, + recordedTask = recordedTask, ) - ScreenRecordChipModel.Recording(recordedTask) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/model/ScreenRecordChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/model/ScreenRecordChipModel.kt index ba6cd4df8d47..0a7278792e70 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/model/ScreenRecordChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/model/ScreenRecordChipModel.kt @@ -29,10 +29,13 @@ sealed interface ScreenRecordChipModel { /** * There's an active screen recording happening. * + * @property hostPackage the package name of the app that is receiving the content of the media + * projection (aka which app the phone screen contents are being sent to). * @property recordedTask the task being recorded if the user is recording only a single app. * Null if the user is recording the entire screen or we don't have the task info yet. */ data class Recording( + val hostPackage: String?, val recordedTask: ActivityManager.RunningTaskInfo?, ) : ScreenRecordChipModel } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt index 55c89a96422f..363b8beab2d7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt @@ -77,6 +77,7 @@ constructor( is ScreenRecordChipModel.Starting -> { OngoingActivityChipModel.Active.Countdown( key = KEY, + isImportantForPrivacy = true, colors = ColorsModel.Red, secondsUntilStarted = state.millisUntilStarted.toCountdownSeconds(), ) @@ -84,6 +85,7 @@ constructor( is ScreenRecordChipModel.Recording -> { OngoingActivityChipModel.Active.Timer( key = KEY, + isImportantForPrivacy = true, icon = OngoingActivityChipModel.ChipIcon.SingleColorIcon( Icon.Resource( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt index 92e17bdd511a..0defa531d18d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt @@ -222,6 +222,7 @@ constructor( ): OngoingActivityChipModel.Active { return OngoingActivityChipModel.Active.Timer( key = KEY, + isImportantForPrivacy = true, icon = OngoingActivityChipModel.ChipIcon.SingleColorIcon( Icon.Resource( @@ -257,6 +258,7 @@ constructor( private fun createIconOnlyShareToAppChip(): OngoingActivityChipModel.Active { return OngoingActivityChipModel.Active.IconOnly( key = KEY, + isImportantForPrivacy = true, icon = OngoingActivityChipModel.ChipIcon.SingleColorIcon( Icon.Resource( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt index b94e7b249233..104c2b546200 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt @@ -107,11 +107,15 @@ fun OngoingActivityChip( } } .thenIf(isClickable) { Modifier.widthIn(min = minWidth) } - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - layout(placeable.width, placeable.height) { - if (constraints.maxWidth >= minWidth.roundToPx()) { - placeable.place(0, 0) + // For non-privacy-related chips, only show the chip if there's enough space for at + // least the minimum width. + .thenIf(!model.isImportantForPrivacy) { + Modifier.layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + layout(placeable.width, placeable.height) { + if (constraints.maxWidth >= minWidth.roundToPx()) { + placeable.place(0, 0) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt index 364e6656ee9d..e28f3684b0fa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt @@ -57,6 +57,11 @@ sealed class OngoingActivityChipModel { * A key that uniquely identifies this chip. Used for better visual effects, like animation. */ open val key: String, + /** + * True if this chip is critical for privacy so we should keep it visible at all times, and + * false otherwise. + */ + open val isImportantForPrivacy: Boolean = false, /** The icon to show on the chip. If null, no icon will be shown. */ open val icon: ChipIcon?, /** What colors to use for the chip. */ @@ -81,6 +86,7 @@ sealed class OngoingActivityChipModel { /** This chip shows only an icon and nothing else. */ data class IconOnly( override val key: String, + override val isImportantForPrivacy: Boolean = false, override val icon: ChipIcon, override val colors: ColorsModel, override val onClickListenerLegacy: View.OnClickListener?, @@ -91,6 +97,7 @@ sealed class OngoingActivityChipModel { ) : Active( key, + isImportantForPrivacy, icon, colors, onClickListenerLegacy, @@ -105,6 +112,7 @@ sealed class OngoingActivityChipModel { /** The chip shows a timer, counting up from [startTimeMs]. */ data class Timer( override val key: String, + override val isImportantForPrivacy: Boolean = false, override val icon: ChipIcon, override val colors: ColorsModel, /** @@ -138,6 +146,7 @@ sealed class OngoingActivityChipModel { ) : Active( key, + isImportantForPrivacy, icon, colors, onClickListenerLegacy, @@ -155,6 +164,7 @@ sealed class OngoingActivityChipModel { */ data class ShortTimeDelta( override val key: String, + override val isImportantForPrivacy: Boolean = false, override val icon: ChipIcon, override val colors: ColorsModel, /** @@ -175,6 +185,7 @@ sealed class OngoingActivityChipModel { ) : Active( key, + isImportantForPrivacy, icon, colors, onClickListenerLegacy, @@ -196,6 +207,7 @@ sealed class OngoingActivityChipModel { */ data class Countdown( override val key: String, + override val isImportantForPrivacy: Boolean = false, override val colors: ColorsModel, /** The number of seconds until an event is started. */ val secondsUntilStarted: Long, @@ -205,6 +217,7 @@ sealed class OngoingActivityChipModel { ) : Active( key, + isImportantForPrivacy, icon = null, colors, onClickListenerLegacy = null, @@ -219,6 +232,7 @@ sealed class OngoingActivityChipModel { /** This chip shows the specified [text] in the chip. */ data class Text( override val key: String, + override val isImportantForPrivacy: Boolean = false, override val icon: ChipIcon, override val colors: ColorsModel, // TODO(b/361346412): Enforce a max length requirement? @@ -231,6 +245,7 @@ sealed class OngoingActivityChipModel { ) : Active( key, + isImportantForPrivacy, icon, colors, onClickListenerLegacy, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt index 8228b5533fca..76d2af86e239 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt @@ -265,11 +265,12 @@ constructor( // [shouldSquish] returns false for that model, but protect against it just in case.) val currentIcon = icon ?: return this return OngoingActivityChipModel.Active.IconOnly( - key, - currentIcon, - colors, - onClickListenerLegacy, - clickBehavior, + key = key, + isImportantForPrivacy = isImportantForPrivacy, + icon = currentIcon, + colors = colors, + onClickListenerLegacy = onClickListenerLegacy, + clickBehavior = clickBehavior, ) } @@ -374,6 +375,9 @@ constructor( * Sort the given chip [bundle] in order of priority, and divide the chips between active, * overflow, and inactive (see [MultipleOngoingActivityChipsModel] for a description of each). */ + // IMPORTANT: PromotedNotificationsInteractor re-implements this same ordering scheme. Any + // changes here should also be made in PromotedNotificationsInteractor. + // TODO(b/402471288): Create a single source of truth for the ordering. private fun rankChips(bundle: ChipBundle): MultipleOngoingActivityChipsModel { val activeChips = mutableListOf<OngoingActivityChipModel.Active>() val overflowChips = mutableListOf<OngoingActivityChipModel.Active>() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 4558017a98c8..b5ab0920a470 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -68,6 +68,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.No import com.android.systemui.statusbar.notification.headsup.PinnedStatus; import com.android.systemui.statusbar.notification.icon.IconPack; import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController; import com.android.systemui.statusbar.notification.row.NotificationGuts; @@ -198,7 +199,7 @@ public final class NotificationEntry extends ListEntry { // TODO(b/377565433): Move into NotificationContentModel during/after // NotificationRowContentBinderRefactor. - private PromotedNotificationContentModel mPromotedNotificationContentModel; + private PromotedNotificationContentModels mPromotedNotificationContentModels; /** * True if both @@ -1106,9 +1107,9 @@ public final class NotificationEntry extends ListEntry { * Gets the content needed to render this notification as a promoted notification on various * surfaces (like status bar chips and AOD). */ - public PromotedNotificationContentModel getPromotedNotificationContentModel() { + public PromotedNotificationContentModels getPromotedNotificationContentModels() { if (PromotedNotificationContentModel.featureFlagEnabled()) { - return mPromotedNotificationContentModel; + return mPromotedNotificationContentModels; } else { Log.wtf(TAG, "getting promoted content without feature flag enabled", new Throwable()); return null; @@ -1127,10 +1128,10 @@ public final class NotificationEntry extends ListEntry { * Sets the content needed to render this notification as a promoted notification on various * surfaces (like status bar chips and AOD). */ - public void setPromotedNotificationContentModel( - @Nullable PromotedNotificationContentModel promotedNotificationContentModel) { + public void setPromotedNotificationContentModels( + @Nullable PromotedNotificationContentModels promotedNotificationContentModels) { if (PromotedNotificationContentModel.featureFlagEnabled()) { - this.mPromotedNotificationContentModel = promotedNotificationContentModel; + this.mPromotedNotificationContentModels = promotedNotificationContentModels; } else { Log.wtf(TAG, "setting promoted content without feature flag enabled", new Throwable()); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java index afba85b49c30..c8ec85d0a227 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java @@ -24,10 +24,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.systemui.dagger.qualifiers.Application; -import com.android.systemui.statusbar.notification.collection.ListEntry; -import com.android.systemui.statusbar.notification.collection.PipelineEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.PipelineEntry; import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter; @@ -100,7 +99,7 @@ public class ColorizedFgsCoordinator implements Coordinator { public boolean isInSection(PipelineEntry entry) { NotificationEntry notificationEntry = entry.getRepresentativeEntry(); if (notificationEntry != null) { - return isRichOngoing(notificationEntry); + return isRichOngoing(notificationEntry) || isPromotedNotifChip(notificationEntry); } return false; } @@ -159,4 +158,10 @@ public class ColorizedFgsCoordinator implements Coordinator { return entry.getImportance() > IMPORTANCE_MIN && notification.isStyle(Notification.CallStyle.class); } + + private boolean isPromotedNotifChip(NotificationEntry entry) { + return PromotedNotificationUi.isEnabled() + && entry.getImportance() > IMPORTANCE_MIN + && mOrderedPromotedNotifKeys.contains(entry.getKey()); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index a0eab43f854b..26b86f9ed74d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -15,6 +15,8 @@ */ package com.android.systemui.statusbar.notification.collection.coordinator +import com.android.systemui.Flags.notificationSkipSilentUpdates + import android.app.Notification import android.app.Notification.GROUP_ALERT_SUMMARY import android.util.ArrayMap @@ -465,15 +467,32 @@ constructor( } hunMutator.updateNotification(posted.key, pinnedStatus) } - } else { + } else { // shouldHeadsUpEver = false if (posted.isHeadsUpEntry) { - // We don't want this to be interrupting anymore, let's remove it - // If the notification is pinned by the user, the only way a user can un-pin - // it is by tapping the status bar notification chip. Since that's a clear - // user action, we should remove the HUN immediately instead of waiting for - // any sort of minimum timeout. - val shouldRemoveImmediately = posted.isPinnedByUser - hunMutator.removeNotification(posted.key, shouldRemoveImmediately) + if (notificationSkipSilentUpdates()) { + if (posted.isPinnedByUser) { + // We don't want this to be interrupting anymore, let's remove it + // If the notification is pinned by the user, the only way a user + // can un-pin it by tapping the status bar notification chip. Since + // that's a clear user action, we should remove the HUN immediately + // instead of waiting for any sort of minimum timeout. + // TODO(b/401068530) Ensure that status bar chip HUNs are not + // removed for silent update + hunMutator.removeNotification(posted.key, + /* releaseImmediately= */ true) + } else { + // Do NOT remove HUN for non-user update. + // Let the HUN show for its remaining duration. + } + } else { + // We don't want this to be interrupting anymore, let's remove it + // If the notification is pinned by the user, the only way a user can + // un-pin it is by tapping the status bar notification chip. Since + // that's a clear user action, we should remove the HUN immediately + // instead of waiting for any sort of minimum timeout. + val shouldRemoveImmediately = posted.isPinnedByUser + hunMutator.removeNotification(posted.key, shouldRemoveImmediately) + } } else { // Don't let the bind finish cancelHeadsUpBind(posted.entry) @@ -573,24 +592,34 @@ constructor( isBinding = isBinding, ) } - // Handle cancelling heads up here, rather than in the OnBeforeFinalizeFilter, so - // that - // work can be done before the ShadeListBuilder is run. This prevents re-entrant - // behavior between this Coordinator, HeadsUpManager, and VisualStabilityManager. - if (posted?.shouldHeadsUpEver == false) { - if (posted.isHeadsUpEntry) { - // We don't want this to be interrupting anymore, let's remove it - mHeadsUpManager.removeNotification( - posted.key, - /* removeImmediately= */ false, - "onEntryUpdated", - ) - } else if (posted.isBinding) { + if (notificationSkipSilentUpdates()) { + // TODO(b/403703828) Move canceling to OnBeforeFinalizeFilter, since we are not + // removing from HeadsUpManager and don't need to deal with re-entrant behavior + // between HeadsUpCoordinator, HeadsUpManager, and VisualStabilityManager. + if (posted?.shouldHeadsUpEver == false + && !posted.isHeadsUpEntry && posted.isBinding) { // Don't let the bind finish cancelHeadsUpBind(posted.entry) } + } else { + // Handle cancelling heads up here, rather than in the OnBeforeFinalizeFilter, + // so that work can be done before the ShadeListBuilder is run. This prevents + // re-entrant behavior between this Coordinator, HeadsUpManager, and + // VisualStabilityManager. + if (posted?.shouldHeadsUpEver == false) { + if (posted.isHeadsUpEntry) { + // We don't want this to be interrupting anymore, let's remove it + mHeadsUpManager.removeNotification( + posted.key, + /* removeImmediately= */ false, + "onEntryUpdated", + ) + } else if (posted.isBinding) { + // Don't let the bind finish + cancelHeadsUpBind(posted.entry) + } + } } - // Update last updated time for this entry setUpdateTime(entry, mSystemClock.currentTimeMillis()) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index 1e5aa01714be..6042bff4bb97 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -83,7 +83,6 @@ import com.android.systemui.statusbar.notification.logging.dagger.NotificationsL import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractorImpl; import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; -import com.android.systemui.statusbar.notification.row.NotificationActionClickManager; import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactory; import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactoryLooperImpl; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; @@ -325,7 +324,7 @@ public interface NotificationsModule { if (PromotedNotificationContentModel.featureFlagEnabled()) { return implProvider.get(); } else { - return (entry, recoveredBuilder, imageModelProvider) -> null; + return (entry, recoveredBuilder, redactionType, imageModelProvider) -> null; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt index adc049e7cdf1..e7cc342ab65c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt @@ -20,6 +20,7 @@ import android.app.Notification.CallStyle.CALL_TYPE_ONGOING import android.app.Notification.CallStyle.CALL_TYPE_SCREENING import android.app.Notification.CallStyle.CALL_TYPE_UNKNOWN import android.app.Notification.EXTRA_CALL_TYPE +import android.app.Notification.FLAG_ONGOING_EVENT import android.app.PendingIntent import android.content.Context import android.graphics.drawable.Icon @@ -29,12 +30,13 @@ import com.android.app.tracing.traceSection import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.collection.GroupEntry -import com.android.systemui.statusbar.notification.collection.PipelineEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.PipelineEntry import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel @@ -140,7 +142,7 @@ private class ActiveNotificationsStoreBuilder( private fun NotificationEntry.toModel(): ActiveNotificationModel { val promotedContent = if (PromotedNotificationContentModel.featureFlagEnabled()) { - promotedNotificationContentModel + promotedNotificationContentModels } else { null } @@ -149,6 +151,8 @@ private class ActiveNotificationsStoreBuilder( key = key, groupKey = sbn.groupKey, whenTime = sbn.notification.`when`, + isForegroundService = sbn.notification.isForegroundService, + isOngoingEvent = (sbn.notification.flags and FLAG_ONGOING_EVENT) != 0, isAmbient = sectionStyleProvider.isMinimized(this), isRowDismissed = isRowDismissed, isSilent = sectionStyleProvider.isSilent(this), @@ -176,6 +180,8 @@ private fun ActiveNotificationsStore.createOrReuse( key: String, groupKey: String?, whenTime: Long, + isForegroundService: Boolean, + isOngoingEvent: Boolean, isAmbient: Boolean, isRowDismissed: Boolean, isSilent: Boolean, @@ -194,13 +200,15 @@ private fun ActiveNotificationsStore.createOrReuse( isGroupSummary: Boolean, bucket: Int, callType: CallType, - promotedContent: PromotedNotificationContentModel?, + promotedContent: PromotedNotificationContentModels?, ): ActiveNotificationModel { return individuals[key]?.takeIf { it.isCurrent( key = key, groupKey = groupKey, whenTime = whenTime, + isForegroundService = isForegroundService, + isOngoingEvent = isOngoingEvent, isAmbient = isAmbient, isRowDismissed = isRowDismissed, isSilent = isSilent, @@ -226,6 +234,8 @@ private fun ActiveNotificationsStore.createOrReuse( key = key, groupKey = groupKey, whenTime = whenTime, + isForegroundService = isForegroundService, + isOngoingEvent = isOngoingEvent, isAmbient = isAmbient, isRowDismissed = isRowDismissed, isSilent = isSilent, @@ -252,6 +262,8 @@ private fun ActiveNotificationModel.isCurrent( key: String, groupKey: String?, whenTime: Long, + isForegroundService: Boolean, + isOngoingEvent: Boolean, isAmbient: Boolean, isRowDismissed: Boolean, isSilent: Boolean, @@ -270,12 +282,14 @@ private fun ActiveNotificationModel.isCurrent( isGroupSummary: Boolean, bucket: Int, callType: CallType, - promotedContent: PromotedNotificationContentModel?, + promotedContent: PromotedNotificationContentModels?, ): Boolean { return when { key != this.key -> false groupKey != this.groupKey -> false whenTime != this.whenTime -> false + isForegroundService != this.isForegroundService -> false + isOngoingEvent != this.isOngoingEvent -> false isAmbient != this.isAmbient -> false isRowDismissed != this.isRowDismissed -> false isSilent != this.isSilent -> false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java index d09546fe80ca..3caaf542e787 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java @@ -67,6 +67,7 @@ public class FooterView extends StackScrollerDecorView { private FooterViewButton mSettingsButton; private FooterViewButton mHistoryButton; private boolean mShouldBeHidden; + private boolean mIsBlurSupported; // Footer label private TextView mSeenNotifsFooterTextView; @@ -390,15 +391,20 @@ public class FooterView extends StackScrollerDecorView { if (!notificationFooterBackgroundTintOptimization()) { if (notificationShadeBlur()) { - Color backgroundColor = Color.valueOf( - SurfaceEffectColors.surfaceEffect1(getContext())); - scHigh = ColorUtils.setAlphaComponent(backgroundColor.toArgb(), 0xFF); - // Apply alpha on background drawables. - int backgroundAlpha = (int) (backgroundColor.alpha() * 0xFF); - clearAllBg.setAlpha(backgroundAlpha); - settingsBg.setAlpha(backgroundAlpha); - if (historyBg != null) { - historyBg.setAlpha(backgroundAlpha); + if (mIsBlurSupported) { + Color backgroundColor = Color.valueOf( + SurfaceEffectColors.surfaceEffect1(getContext())); + scHigh = ColorUtils.setAlphaComponent(backgroundColor.toArgb(), 0xFF); + // Apply alpha on background drawables. + int backgroundAlpha = (int) (backgroundColor.alpha() * 0xFF); + clearAllBg.setAlpha(backgroundAlpha); + settingsBg.setAlpha(backgroundAlpha); + if (historyBg != null) { + historyBg.setAlpha(backgroundAlpha); + } + } else { + scHigh = mContext.getColor( + com.android.internal.R.color.materialColorSurfaceContainer); } } else { scHigh = mContext.getColor( @@ -438,6 +444,16 @@ public class FooterView extends StackScrollerDecorView { } } + public void setIsBlurSupported(boolean isBlurSupported) { + if (notificationShadeBlur()) { + if (mIsBlurSupported == isBlurSupported) { + return; + } + mIsBlurSupported = isBlurSupported; + updateColors(); + } + } + @Override @NonNull public ExpandableViewState createExpandableViewState() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt index 3383ce98549f..3213754ca9a9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt @@ -20,6 +20,7 @@ import android.view.View import androidx.lifecycle.lifecycleScope import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.Flags.notificationShadeBlur import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.statusbar.notification.NotificationActivityStarter import com.android.systemui.statusbar.notification.NotificationActivityStarter.SettingsIntent @@ -81,6 +82,14 @@ object FooterViewBinder { launch { bindHistoryButton(footer, viewModel, notificationActivityStarter) } } launch { bindMessage(footer, viewModel) } + + if (notificationShadeBlur()) { + launch { + viewModel.isBlurSupported.collect { supported -> + footer.setIsBlurSupported(supported) + } + } + } } private suspend fun bindClearAllButton( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt index c895c41960d4..c1fc72c618ba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt @@ -31,6 +31,7 @@ import com.android.systemui.util.kotlin.sample import com.android.systemui.util.ui.AnimatableEvent import com.android.systemui.util.ui.AnimatedValue import com.android.systemui.util.ui.toAnimatedValueFlow +import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow @@ -48,6 +49,7 @@ constructor( notificationSettingsInteractor: NotificationSettingsInteractor, seenNotificationsInteractor: SeenNotificationsInteractor, shadeInteractor: ShadeInteractor, + windowRootViewBlurInteractor: WindowRootViewBlurInteractor, ) { /** A message to show instead of the footer buttons. */ val message: FooterMessageViewModel = @@ -119,6 +121,8 @@ constructor( } } + val isBlurSupported = windowRootViewBlurInteractor.isBlurCurrentlySupported + private val manageOrHistoryButtonText: Flow<Int> = notificationSettingsInteractor.isNotificationHistoryEnabled.map { shouldLaunchHistory -> if (shouldLaunchHistory) R.string.manage_notifications_history_text diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt index 27b2788f0b08..a8a7e885d1f7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt @@ -40,6 +40,8 @@ import android.graphics.drawable.Icon import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.shade.ShadeDisplayAware +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE +import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator.Companion.EXTRA_AUTOMATICALLY_EXTRACTED_SHORT_CRITICAL_TEXT import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator.Companion.EXTRA_WAS_AUTOMATICALLY_PROMOTED @@ -48,6 +50,7 @@ import com.android.systemui.statusbar.notification.promoted.shared.model.Promote import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.OldProgress import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels import com.android.systemui.statusbar.notification.row.shared.ImageModel import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider.ImageSizeClass.MediumSquare @@ -60,8 +63,9 @@ interface PromotedNotificationContentExtractor { fun extractContent( entry: NotificationEntry, recoveredBuilder: Notification.Builder, + @RedactionType redactionType: Int, imageModelProvider: ImageModelProvider, - ): PromotedNotificationContentModel? + ): PromotedNotificationContentModels? } @SysUISingleton @@ -76,8 +80,9 @@ constructor( override fun extractContent( entry: NotificationEntry, recoveredBuilder: Notification.Builder, + @RedactionType redactionType: Int, imageModelProvider: ImageModelProvider, - ): PromotedNotificationContentModel? { + ): PromotedNotificationContentModels? { if (!PromotedNotificationContentModel.featureFlagEnabled()) { logger.logExtractionSkipped(entry, "feature flags disabled") return null @@ -95,7 +100,55 @@ constructor( return null } - val contentBuilder = PromotedNotificationContentModel.Builder(entry.key) + val privateVersion = + extractPrivateContent( + key = entry.key, + notification = notification, + recoveredBuilder = recoveredBuilder, + lastAudiblyAlertedMs = entry.lastAudiblyAlertedMs, + imageModelProvider = imageModelProvider, + ) + val publicVersion = + if (redactionType == REDACTION_TYPE_NONE) { + privateVersion + } else { + if (notification.publicVersion == null) { + privateVersion.toDefaultPublicVersion() + } else { + // TODO(b/400991304): implement extraction for [Notification.publicVersion] + privateVersion.toDefaultPublicVersion() + } + } + return PromotedNotificationContentModels( + privateVersion = privateVersion, + publicVersion = publicVersion, + ) + .also { logger.logExtractionSucceeded(entry, it) } + } + + private fun PromotedNotificationContentModel.toDefaultPublicVersion(): + PromotedNotificationContentModel = + PromotedNotificationContentModel.Builder(key = identity.key).let { + it.style = if (style == Style.Ineligible) Style.Ineligible else Style.Base + it.smallIcon = smallIcon + it.iconLevel = iconLevel + it.appName = appName + it.time = time + it.lastAudiblyAlertedMs = lastAudiblyAlertedMs + it.profileBadgeResId = profileBadgeResId + it.colors = colors + it.build() + } + + private fun extractPrivateContent( + key: String, + notification: Notification, + recoveredBuilder: Notification.Builder, + lastAudiblyAlertedMs: Long, + imageModelProvider: ImageModelProvider, + ): PromotedNotificationContentModel { + + val contentBuilder = PromotedNotificationContentModel.Builder(key) // TODO: Pitch a fit if style is unsupported or mandatory fields are missing once // FLAG_PROMOTED_ONGOING is set reliably and we're not testing status bar chips. @@ -108,7 +161,7 @@ constructor( contentBuilder.subText = notification.subText() contentBuilder.time = notification.extractWhen() contentBuilder.shortCriticalText = notification.shortCriticalText() - contentBuilder.lastAudiblyAlertedMs = entry.lastAudiblyAlertedMs + contentBuilder.lastAudiblyAlertedMs = lastAudiblyAlertedMs contentBuilder.profileBadgeResId = null // TODO contentBuilder.title = notification.title(recoveredBuilder.style) contentBuilder.text = notification.text(recoveredBuilder.style) @@ -124,7 +177,7 @@ constructor( recoveredBuilder.extractStyleContent(notification, contentBuilder, imageModelProvider) - return contentBuilder.build().also { logger.logExtractionSucceeded(entry, it) } + return contentBuilder.build() } private fun Notification.smallIconModel(imageModelProvider: ImageModelProvider): ImageModel? = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt index 5f9678a06b59..6b6203d6ea4d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt @@ -23,7 +23,7 @@ import com.android.systemui.log.core.LogLevel.ERROR import com.android.systemui.log.core.LogLevel.INFO import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels import javax.inject.Inject @OptIn(ExperimentalStdlibApi::class) @@ -56,7 +56,7 @@ constructor(@PromotedNotificationLog private val buffer: LogBuffer) { fun logExtractionSucceeded( entry: NotificationEntry, - content: PromotedNotificationContentModel, + content: PromotedNotificationContentModels, ) { buffer.log( EXTRACTION_TAG, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt index ec4ee4560ea1..d9778bdde0a5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt @@ -22,6 +22,7 @@ import com.android.systemui.statusbar.notification.promoted.shared.model.Promote import com.android.systemui.util.kotlin.FlowDumperImpl import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @SysUISingleton @@ -34,6 +35,16 @@ constructor( /** The content to show as the promoted notification on AOD */ val content: Flow<PromotedNotificationContentModel?> = promotedNotificationsInteractor.aodPromotedNotification + .map { + // TODO(b/400991304): show the private version when unlocked + it?.publicVersion + } + .distinctUntilNewInstance() val isPresent: Flow<Boolean> = content.map { it != null }.dumpWhileCollecting("isPresent") + + /** + * Returns flow where all subsequent repetitions of the same object instance are filtered out. + */ + private fun <T> Flow<T>.distinctUntilNewInstance() = distinctUntilChanged { a, b -> a === b } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt index a99ca072b6c8..08e7528631c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt @@ -19,17 +19,24 @@ package com.android.systemui.statusbar.notification.promoted.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor +import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor +import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor +import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor +import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style.Ineligible +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -38,52 +45,169 @@ import kotlinx.coroutines.flow.map * presented order of current notification status bar chips. */ @SysUISingleton +@OptIn(ExperimentalCoroutinesApi::class) class PromotedNotificationsInteractor @Inject constructor( - activeNotificationsInteractor: ActiveNotificationsInteractor, + private val activeNotificationsInteractor: ActiveNotificationsInteractor, + screenRecordChipInteractor: ScreenRecordChipInteractor, + mediaProjectionChipInteractor: MediaProjectionChipInteractor, callChipInteractor: CallChipInteractor, notifChipsInteractor: StatusBarNotificationChipsInteractor, @Background backgroundDispatcher: CoroutineDispatcher, ) { + private val screenRecordChipNotification: Flow<NotifAndPromotedContent?> = + screenRecordChipInteractor.screenRecordState.flatMapLatest { screenRecordModel -> + when (screenRecordModel) { + is ScreenRecordChipModel.DoingNothing -> flowOf(null) + is ScreenRecordChipModel.Starting -> flowOf(null) + is ScreenRecordChipModel.Recording -> + createRecordingNotificationFlow(hostPackage = screenRecordModel.hostPackage) + } + } + + private val mediaProjectionChipNotification: Flow<NotifAndPromotedContent?> = + mediaProjectionChipInteractor.projection.flatMapLatest { projectionModel -> + when (projectionModel) { + is ProjectionChipModel.NotProjecting -> flowOf(null) + is ProjectionChipModel.Projecting -> + createRecordingNotificationFlow( + hostPackage = projectionModel.projectionState.hostPackage + ) + } + } + + /** + * Creates a flow emitting the screen-recording-related notification corresponding to the given + * package name (if we can find it). + * + * @param hostPackage the package name of the app that is receiving the content of the media + * projection (aka which app the phone screen contents are being sent to). + */ + private fun createRecordingNotificationFlow( + hostPackage: String? + ): Flow<NotifAndPromotedContent?> = + if (hostPackage == null) { + flowOf(null) + } else { + activeNotificationsInteractor.allRepresentativeNotifications + .map { allNotifs -> + findBestMatchingMediaProjectionNotif(allNotifs.values, hostPackage) + } + .distinctUntilChanged() + } + + /** + * Finds the best notification that matches the given [hostPackage] that looks like a recording + * notification, or null if we couldn't find a uniquely good match. + */ + private fun findBestMatchingMediaProjectionNotif( + allNotifs: Collection<ActiveNotificationModel>, + hostPackage: String, + ): NotifAndPromotedContent? { + val candidates = allNotifs.filter { it.packageName == hostPackage } + if (candidates.isEmpty()) { + return null + } + + candidates + .findOnlyOrNull { it.isForegroundService } + ?.let { + return it.toNotifAndPromotedContent() + } + candidates + .findOnlyOrNull { it.isOngoingEvent } + ?.let { + return it.toNotifAndPromotedContent() + } + candidates + .findOnlyOrNull { it.isForegroundService && it.isOngoingEvent } + ?.let { + return it.toNotifAndPromotedContent() + } + // We weren't able to find exactly 1 match for the given [hostPackage], so just don't match + // at all. + return null + } + + /** + * Returns the single notification matching the given [predicate] if there's only 1 match, or + * null if there's 0 or 2+ matches. + */ + private fun List<ActiveNotificationModel>.findOnlyOrNull( + predicate: (ActiveNotificationModel) -> Boolean + ): ActiveNotificationModel? { + val list = this.filter(predicate) + return if (list.size == 1) { + list.first() + } else { + null + } + } + + private fun ActiveNotificationModel.toNotifAndPromotedContent(): NotifAndPromotedContent { + return NotifAndPromotedContent(this.key, this.promotedContent) + } + + private val callNotification: Flow<NotifAndPromotedContent?> = + callChipInteractor.ongoingCallState + .map { + when (it) { + is OngoingCallModel.InCall -> + NotifAndPromotedContent(it.notificationKey, it.promotedContent) + is OngoingCallModel.NoCall -> null + } + } + .distinctUntilChanged() + + private val promotedChipNotifications: Flow<List<NotifAndPromotedContent>> = + notifChipsInteractor.allNotificationChips + .map { chips -> chips.map { NotifAndPromotedContent(it.key, it.promotedContent) } } + .distinctUntilChanged() + /** * This is the ordered list of notifications (and the promoted content) represented as chips in * the status bar. */ private val orderedChipNotifications: Flow<List<NotifAndPromotedContent>> = - combine(callChipInteractor.ongoingCallState, notifChipsInteractor.allNotificationChips) { - callState, - notifChips -> - buildList { - val callData = callState.getNotifData()?.also { add(it) } - addAll( - notifChips.mapNotNull { - when (it.key) { - callData?.key -> null // do not re-add the same call - else -> NotifAndPromotedContent(it.key, it.promotedContent) - } - } - ) + combine( + screenRecordChipNotification, + mediaProjectionChipNotification, + callNotification, + promotedChipNotifications, + ) { screenRecordNotif, mediaProjectionNotif, callNotif, promotedNotifs -> + val chipNotifications = mutableListOf<NotifAndPromotedContent>() + val usedKeys = mutableListOf<String>() + + fun addToList(item: NotifAndPromotedContent?) { + if (item != null && !usedKeys.contains(item.key)) { + chipNotifications.add(item) + usedKeys.add(item.key) + } } - } - private fun OngoingCallModel.getNotifData(): NotifAndPromotedContent? = - when (this) { - is OngoingCallModel.InCall -> NotifAndPromotedContent(notificationKey, promotedContent) - is OngoingCallModel.NoCall -> null + // IMPORTANT: This ordering is prescribed by OngoingActivityChipsViewModel. Be sure to + // always keep this ordering in sync with that view model. + // TODO(b/402471288): Create a single source of truth for the ordering. + addToList(screenRecordNotif) + addToList(mediaProjectionNotif) + addToList(callNotif) + promotedNotifs.forEach { addToList(it) } + + chipNotifications } /** * The top promoted notification represented by a chip, with the order determined by the order * of the chips, not the notifications. */ - private val topPromotedChipNotification: Flow<PromotedNotificationContentModel?> = + private val topPromotedChipNotification: Flow<PromotedNotificationContentModels?> = orderedChipNotifications .map { list -> list.firstNotNullOfOrNull { it.promotedContent } } .distinctUntilNewInstance() /** This is the AOD promoted notification, which should avoid regular changing. */ - val aodPromotedNotification: Flow<PromotedNotificationContentModel?> = + val aodPromotedNotification: Flow<PromotedNotificationContentModels?> = combine( topPromotedChipNotification, activeNotificationsInteractor.topLevelRepresentativeNotifications, @@ -105,13 +229,13 @@ constructor( .flowOn(backgroundDispatcher) private fun List<ActiveNotificationModel>.firstAodEligibleOrNull(): - PromotedNotificationContentModel? { + PromotedNotificationContentModels? { return this.firstNotNullOfOrNull { it.promotedContent?.takeIfAodEligible() } } - private fun PromotedNotificationContentModel.takeIfAodEligible(): - PromotedNotificationContentModel? { - return this.takeUnless { it.style == Ineligible } + private fun PromotedNotificationContentModels.takeIfAodEligible(): + PromotedNotificationContentModels? { + return this.takeUnless { it.privateVersion.style == Ineligible } } /** @@ -127,7 +251,7 @@ constructor( */ private data class NotifAndPromotedContent( val key: String, - val promotedContent: PromotedNotificationContentModel?, + val promotedContent: PromotedNotificationContentModels?, ) { /** * Define the equals of this object to only check the reference equality of the promoted @@ -145,7 +269,7 @@ constructor( /** Define the hashCode to be very quick, even if it increases collisions. */ override fun hashCode(): Int { var result = key.hashCode() - result = 31 * result + (promotedContent?.identity?.hashCode() ?: 0) + result = 31 * result + (promotedContent?.key?.hashCode() ?: 0) return result } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt index 57b07204fc6a..ffacf62fccce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt @@ -29,6 +29,31 @@ import com.android.systemui.statusbar.notification.row.ImageResult import com.android.systemui.statusbar.notification.row.LazyImage import com.android.systemui.statusbar.notification.row.shared.ImageModel +data class PromotedNotificationContentModels( + /** The potentially redacted version of the content that will be exposed to the public */ + val publicVersion: PromotedNotificationContentModel, + /** The unredacted version of the content that will be kept private */ + val privateVersion: PromotedNotificationContentModel, +) { + val key: String + get() = privateVersion.identity.key + + init { + check(publicVersion.identity.key == privateVersion.identity.key) { + "public and private models must have the same key" + } + } + + fun toRedactedString(): String { + val publicVersionString = + "==privateVersion".takeIf { privateVersion === publicVersion } + ?: publicVersion.toRedactedString() + return ("PromotedNotificationContentModels(" + + "privateVersion=${privateVersion.toRedactedString()}, " + + "publicVersion=$publicVersionString)") + } +} + /** * The content needed to render a promoted notification to surfaces besides the notification stack, * like the skeleton view on AOD or the status bar chip. 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 2a3b266c8d10..bef3c691cb4d 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 @@ -976,7 +976,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return mEntry; } - @Nullable + @NonNull public EntryAdapter getEntryAdapter() { NotificationBundleUi.unsafeAssertInNewMode(); return mEntryAdapter; @@ -1005,7 +1005,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } else if (isAboveShelf() != wasAboveShelf) { mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf); } - updateBackgroundTint(); + if (notificationRowTransparency()) { + updateBackgroundTint(); + } } /** @@ -2684,30 +2686,58 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } /** + * Whether to allow dismissal with the whole-row translation animation. + * + * If true, either animation is permissible. + * If false, usingRTX behavior is forbidden, only clipping animation should be used. + * + * Usually either is OK, except for promoted notifications, where we always need to + * dismiss with content clipping/partial translation animation instead, so that we + * can show the demotion options. + * @return + */ + private boolean allowDismissUsingRowTranslationX() { + if (Flags.permissionHelperInlineUiRichOngoing()) { + return !isPromotedOngoing(); + } else { + // Don't change behavior unless the flag is on. + return true; + } + } + + /** * Set the dismiss behavior of the view. * * @param usingRowTranslationX {@code true} if the view should translate using regular * translationX, otherwise the contents will be * translated. + * @param forceUpdateChildren {@code true} to force initialization, {@code false} if lazy + * behavior is OK. */ @Override - public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) { - if (usingRowTranslationX != mDismissUsingRowTranslationX) { + public void setDismissUsingRowTranslationX(boolean usingRowTranslationX, + boolean forceUpdateChildren) { + // Before updating dismiss behavior, make sure this is an allowable configuration for this + // notification. + usingRowTranslationX = usingRowTranslationX && allowDismissUsingRowTranslationX(); + + if (forceUpdateChildren || (usingRowTranslationX != mDismissUsingRowTranslationX)) { // In case we were already transitioning, let's switch over! float previousTranslation = getTranslation(); if (previousTranslation != 0) { setTranslation(0); } - super.setDismissUsingRowTranslationX(usingRowTranslationX); + super.setDismissUsingRowTranslationX(usingRowTranslationX, forceUpdateChildren); if (previousTranslation != 0) { setTranslation(previousTranslation); } + if (mChildrenContainer != null) { List<ExpandableNotificationRow> notificationChildren = mChildrenContainer.getAttachedChildren(); for (int i = 0; i < notificationChildren.size(); i++) { ExpandableNotificationRow child = notificationChildren.get(i); - child.setDismissUsingRowTranslationX(usingRowTranslationX); + child.setDismissUsingRowTranslationX(usingRowTranslationX, forceUpdateChildren); } } } @@ -3164,7 +3194,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mChildrenContainer.setOnKeyguard(onKeyguard); } } - updateBackgroundTint(); + if (notificationRowTransparency()) { + updateBackgroundTint(); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java index 80cf818e985f..6c990df5d05e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java @@ -292,7 +292,8 @@ public abstract class ExpandableOutlineView extends ExpandableView { * translationX, otherwise the contents will be * translated. */ - public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) { + public void setDismissUsingRowTranslationX(boolean usingRowTranslationX, + boolean forceUpdateChildren) { mDismissUsingRowTranslationX = usingRowTranslationX; } 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 d97e25fdfa22..57ceafcd15c6 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 @@ -60,6 +60,7 @@ 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.shared.model.PromotedNotificationContentModel; +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider; @@ -1003,7 +1004,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder row.mImageModelIndex = result.mRowImageInflater.getNewImageIndex(); if (PromotedNotificationContentModel.featureFlagEnabled()) { - entry.setPromotedNotificationContentModel(result.mPromotedContent); + entry.setPromotedNotificationContentModels(result.mPromotedContent); } boolean setRepliesAndActions = true; @@ -1387,9 +1388,9 @@ public class NotificationContentInflater implements NotificationRowContentBinder mLogger.logAsyncTaskProgress(logKey, "extracting promoted notification content"); final ImageModelProvider imageModelProvider = result.mRowImageInflater.useForContentModel(); - final PromotedNotificationContentModel promotedContent = + final PromotedNotificationContentModels promotedContent = mPromotedNotificationContentExtractor.extractContent(mEntry, - recoveredBuilder, imageModelProvider); + recoveredBuilder, mBindParams.redactionType, imageModelProvider); mLogger.logAsyncTaskProgress(logKey, "extracted promoted notification content: " + promotedContent); @@ -1503,7 +1504,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder static class InflationProgress { RowImageInflater mRowImageInflater; - PromotedNotificationContentModel mPromotedContent; + PromotedNotificationContentModels mPromotedContent; private RemoteViews newContentView; private RemoteViews newHeadsUpView; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index cdb78d99538b..f4e01bf718d9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -29,6 +29,7 @@ import android.content.pm.ShortcutManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -309,6 +310,7 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta }); View gutsView = item.getGutsView(); + try { if (gutsView instanceof NotificationSnooze) { initializeSnoozeView(row, (NotificationSnooze) gutsView); @@ -322,6 +324,8 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta (PartialConversationInfo) gutsView); } else if (gutsView instanceof FeedbackInfo) { initializeFeedbackInfo(row, (FeedbackInfo) gutsView); + } else if (gutsView instanceof PromotedPermissionGutsContent) { + initializeDemoteView(row, (PromotedPermissionGutsContent) gutsView); } return true; } catch (Exception e) { @@ -351,6 +355,31 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta } /** + * Sets up the {@link NotificationSnooze} inside the notification row's guts. + * + * @param row view to set up the guts for + * @param demoteGuts view to set up/bind within {@code row} + */ + private void initializeDemoteView( + final ExpandableNotificationRow row, + PromotedPermissionGutsContent demoteGuts) { + StatusBarNotification sbn = row.getEntry().getSbn(); + demoteGuts.setStatusBarNotification(sbn); + demoteGuts.setOnDemoteAction(new View.OnClickListener() { + @Override + public void onClick(View v) { + try { + // TODO(b/391661009): Signal AutomaticPromotionCoordinator here + mNotificationManager.setCanBePromoted( + sbn.getPackageName(), sbn.getUid(), false, true); + } catch (RemoteException e) { + Log.e(TAG, "Couldn't revoke live update permission", e); + } + } + }); + } + + /** * Sets up the {@link FeedbackInfo} inside the notification row's guts. * * @param row view to set up the guts for diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java index c03dc279888f..f494a4ce40dd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java @@ -272,6 +272,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl } else if (personNotifType >= PeopleNotificationIdentifier.TYPE_FULL_PERSON) { mInfoItem = createConversationItem(mContext); } else if (android.app.Flags.uiRichOngoing() + && android.app.Flags.apiRichOngoing() && Flags.permissionHelperUiRichOngoing() && sbn.getNotification().isPromotedOngoing()) { mInfoItem = createPromotedItem(mContext); @@ -284,6 +285,15 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl } mRightMenuItems.add(mInfoItem); mRightMenuItems.add(mFeedbackItem); + boolean isPromotedOngoing = NotificationBundleUi.isEnabled() + ? mParent.getEntryAdapter().isPromotedOngoing() + : mParent.getEntryLegacy().isPromotedOngoing(); + if (android.app.Flags.uiRichOngoing() && Flags.permissionHelperInlineUiRichOngoing() + && isPromotedOngoing) { + mRightMenuItems.add(createDemoteItem(mContext)); + } + + mLeftMenuItems.addAll(mRightMenuItems); populateMenuViews(); @@ -305,15 +315,19 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl } else { mMenuContainer = new FrameLayout(mContext); } + final int showDismissSetting = Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.SHOW_NEW_NOTIF_DISMISS, /* default = */ 1); final boolean newFlowHideShelf = showDismissSetting == 1; - if (newFlowHideShelf) { - return; - } - List<MenuItem> menuItems = mOnLeft ? mLeftMenuItems : mRightMenuItems; - for (int i = 0; i < menuItems.size(); i++) { - addMenuView(menuItems.get(i), mMenuContainer); + + // Populate menu items if we are using the new permission helper (U+) or if we are using + // the very old dismiss setting (SC-). + // TODO: SHOW_NEW_NOTIF_DISMISS==0 case can likely be removed. + if (Flags.permissionHelperInlineUiRichOngoing() || !newFlowHideShelf) { + List<MenuItem> menuItems = mOnLeft ? mLeftMenuItems : mRightMenuItems; + for (int i = 0; i < menuItems.size(); i++) { + addMenuView(menuItems.get(i), mMenuContainer); + } } } @@ -679,6 +693,15 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl return snooze; } + static MenuItem createDemoteItem(Context context) { + PromotedPermissionGutsContent demoteContent = + (PromotedPermissionGutsContent) LayoutInflater.from(context).inflate( + R.layout.promoted_permission_guts, null, false); + MenuItem info = new NotificationMenuItem(context, null, demoteContent, + R.drawable.unpin_icon); + return info; + } + static NotificationMenuItem createConversationItem(Context context) { Resources res = context.getResources(); String infoDescription = res.getString(R.string.notification_menu_gear_description); @@ -686,7 +709,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl (NotificationConversationInfo) LayoutInflater.from(context).inflate( R.layout.notification_conversation_info, null, false); return new NotificationMenuItem(context, infoDescription, infoContent, - R.drawable.ic_settings); + NotificationMenuItem.OMIT_FROM_SWIPE_MENU); } static NotificationMenuItem createPromotedItem(Context context) { @@ -696,7 +719,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl (PromotedNotificationInfo) LayoutInflater.from(context).inflate( R.layout.promoted_notification_info, null, false); return new NotificationMenuItem(context, infoDescription, infoContent, - R.drawable.ic_settings); + NotificationMenuItem.OMIT_FROM_SWIPE_MENU); } static NotificationMenuItem createPartialConversationItem(Context context) { @@ -706,7 +729,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl (PartialConversationInfo) LayoutInflater.from(context).inflate( R.layout.partial_conversation_info, null, false); return new NotificationMenuItem(context, infoDescription, infoContent, - R.drawable.ic_settings); + NotificationMenuItem.OMIT_FROM_SWIPE_MENU); } static NotificationMenuItem createInfoItem(Context context) { @@ -718,14 +741,14 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate( layoutId, null, false); return new NotificationMenuItem(context, infoDescription, infoContent, - R.drawable.ic_settings); + NotificationMenuItem.OMIT_FROM_SWIPE_MENU); } static MenuItem createFeedbackItem(Context context) { FeedbackInfo feedbackContent = (FeedbackInfo) LayoutInflater.from(context).inflate( R.layout.feedback_info, null, false); MenuItem info = new NotificationMenuItem(context, null, feedbackContent, - -1 /*don't show in slow swipe menu */); + NotificationMenuItem.OMIT_FROM_SWIPE_MENU); return info; } @@ -762,6 +785,10 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl @Override public boolean isWithinSnapMenuThreshold() { + if (getSpaceForMenu() == 0) { + // don't snap open if there are no items + return false; + } float translation = getTranslation(); float snapBackThreshold = getSnapBackThreshold(); float targetRight = getDismissThreshold(); @@ -803,6 +830,10 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl } public static class NotificationMenuItem implements MenuItem { + + // Constant signaling that this MenuItem should not appear in slow swipe. + public static final int OMIT_FROM_SWIPE_MENU = -1; + View mMenuView; GutsContent mGutsContent; String mContentDescription; 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 ae52db88358a..4f1b90544403 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,6 +53,7 @@ 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.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP @@ -595,7 +596,7 @@ constructor( val rowImageInflater: RowImageInflater, val remoteViews: NewRemoteViews, val contentModel: NotificationContentModel, - val promotedContent: PromotedNotificationContentModel?, + val promotedContent: PromotedNotificationContentModels?, ) { var inflatedContentView: View? = null @@ -700,7 +701,12 @@ constructor( ) val imageModelProvider = rowImageInflater.useForContentModel() promotedNotificationContentExtractor - .extractContent(entry, builder, imageModelProvider) + .extractContent( + entry, + builder, + bindParams.redactionType, + imageModelProvider, + ) .also { logger.logAsyncTaskProgress( entry.logKey, @@ -1519,7 +1525,7 @@ constructor( entry.setContentModel(result.contentModel) if (PromotedNotificationContentModel.featureFlagEnabled()) { - entry.promotedNotificationContentModel = result.promotedContent + entry.promotedNotificationContentModels = result.promotedContent } result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java index 01ee788f7fd7..769f0b5a4fa4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java @@ -80,6 +80,7 @@ public class PromotedNotificationInfo extends NotificationInfo { assistantFeedbackController, metricsLogger, onCloseClick); mNotificationManager = iNotificationManager; + mPackageDemotionInteractor = packageDemotionInteractor; bindDemote(entry.getSbn(), pkg); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedPermissionGutsContent.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedPermissionGutsContent.java new file mode 100644 index 000000000000..222a1f4d8adf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedPermissionGutsContent.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row; + +import android.content.Context; +import android.os.Bundle; +import android.service.notification.StatusBarNotification; +import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.internal.logging.MetricsLogger; +import com.android.systemui.res.R; + +/** + * This GutsContent shows an explanatory interstitial telling the user they've just revoked this + * app's permission to post Promoted/Live notifications. + * If the guts are dismissed without further action, the revocation is committed. + * If the user hits undo, the permission is not revoked. + */ +public class PromotedPermissionGutsContent extends LinearLayout + implements NotificationGuts.GutsContent, View.OnClickListener { + + private static final String TAG = "SnoozyPromotedGuts"; + + private NotificationGuts mGutsContainer; + private StatusBarNotification mSbn; + + private TextView mUndoButton; + + private MetricsLogger mMetricsLogger = new MetricsLogger(); + private OnClickListener mDemoteAction; + + public PromotedPermissionGutsContent(Context context, AttributeSet attrs) { + super(context, attrs); + } + + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mUndoButton = (TextView) findViewById(R.id.undo); + mUndoButton.setOnClickListener(this); + mUndoButton.setContentDescription( + getContext().getString(R.string.snooze_undo_content_description)); + + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + dispatchConfigurationChanged(getResources().getConfiguration()); + } + + /** + * Update the content description of the snooze view based on the snooze option and whether the + * snooze options are expanded or not. + * For example, this will be something like "Collapsed\u2029Snooze for 1 hour". The paragraph + * separator is added to introduce a break in speech, to match what TalkBack does by default + * when you e.g. press on a notification. + */ + private void updateContentDescription() { + // + } + + + @Override + public boolean performAccessibilityActionInternal(int action, Bundle arguments) { + if (super.performAccessibilityActionInternal(action, arguments)) { + return true; + } + if (action == R.id.action_snooze_undo) { + undoDemote(mUndoButton); + return true; + } + return false; + } + + /** + * TODO docs + * @param sbn + */ + public void setStatusBarNotification(StatusBarNotification sbn) { + mSbn = sbn; + TextView demoteExplanation = (TextView) findViewById(R.id.demote_explain); + demoteExplanation.setText(mContext.getResources().getString(R.string.demote_explain_text, + mSbn.getPackageName())); + } + + @Override + public void onClick(View v) { + if (mGutsContainer != null) { + mGutsContainer.resetFalsingCheck(); + } + final int id = v.getId(); + if (id == R.id.undo) { + undoDemote(v); + } + + } + + private void undoDemote(View v) { + // Don't commit the demote action, instead log the undo and dismiss the view. + mGutsContainer.closeControls(v, /* save= */ false); + } + + @Override + public int getActualHeight() { + return getHeight(); + } + + @Override + public boolean willBeRemoved() { + return false; + } + + @Override + public View getContentView() { + return this; + } + + @Override + public void setGutsParent(NotificationGuts guts) { + mGutsContainer = guts; + } + + @Override + public boolean handleCloseControls(boolean save, boolean force) { + if (!save) { + // Undo changes and let the guts handle closing the view + return false; + } else { + // Commit demote action. + mDemoteAction.onClick(this); + return false; + } + } + + @Override + public boolean isLeavebehind() { + return true; + } + + @Override + public boolean shouldBeSavedOnClose() { + return true; + } + + @Override + public boolean needsFalsingProtection() { + return false; + } + + public void setOnDemoteAction(OnClickListener demoteAction) { + mDemoteAction = demoteAction; + } + +} 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 53728c7da62d..96527389b5fe 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 @@ -20,6 +20,7 @@ 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.promoted.shared.model.PromotedNotificationContentModels import com.android.systemui.statusbar.notification.stack.PriorityBucket /** @@ -38,6 +39,10 @@ data class ActiveNotificationModel( val groupKey: String?, /** When this notification was posted. */ val whenTime: Long, + /** True if this is a foreground service notification. */ + val isForegroundService: Boolean, + /** True if this notification is for an ongoing event. */ + val isOngoingEvent: Boolean, /** Is this entry in the ambient / minimized section (lowest priority)? */ val isAmbient: Boolean, /** @@ -84,7 +89,7 @@ data class ActiveNotificationModel( * The content needed to render this as a promoted notification on various surfaces, or null if * this notification cannot be rendered as a promoted notification. */ - val promotedContent: PromotedNotificationContentModel?, + val promotedContent: PromotedNotificationContentModels?, ) : ActiveNotificationEntryModel() { init { if (!PromotedNotificationContentModel.featureFlagEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt index 7d489a97f853..5c52500b7f70 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt @@ -169,8 +169,7 @@ constructor( } private fun pullDismissibleRow(translation: Float) { - val targetTranslation = swipedRowMultiplier * translation - val crossedThreshold = abs(targetTranslation) >= magneticDetachThreshold + val crossedThreshold = abs(translation) >= magneticDetachThreshold if (crossedThreshold) { snapNeighborsBack() currentMagneticListeners.swipedListener()?.let { detach(it, translation) } @@ -247,8 +246,7 @@ constructor( } private fun translateDetachedRow(translation: Float) { - val targetTranslation = swipedRowMultiplier * translation - val crossedThreshold = abs(targetTranslation) <= magneticAttachThreshold + val crossedThreshold = abs(translation) <= magneticAttachThreshold if (crossedThreshold) { translationOffset += translation updateRoundness(translation = 0f, animate = true) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 9fea75048e3e..503256accff0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -3220,8 +3220,7 @@ public class NotificationStackScrollLayout updateAnimationState(child); updateChronometerForChild(child); if (child instanceof ExpandableNotificationRow row) { - row.setDismissUsingRowTranslationX(mDismissUsingRowTranslationX); - + row.setDismissUsingRowTranslationX(mDismissUsingRowTranslationX, /* force= */ true); } } @@ -6157,7 +6156,7 @@ public class NotificationStackScrollLayout View child = getChildAt(i); if (child instanceof ExpandableNotificationRow) { ((ExpandableNotificationRow) child).setDismissUsingRowTranslationX( - dismissUsingRowTranslationX); + dismissUsingRowTranslationX, /* force= */ false); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index 000b3f643e9a..12b48eba7a96 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt @@ -89,6 +89,17 @@ constructor( source = shadeModeInteractor.shadeMode.map { getQuickSettingsShadeContentKey(it) }, ) + /** + * Whether the current touch gesture is overscroll. If true, it means the NSSL has already + * consumed part of the gesture. + */ + val isCurrentGestureOverscroll: Boolean by + hydrator.hydratedStateOf( + traceName = "isCurrentGestureOverscroll", + initialValue = false, + source = interactor.isCurrentGestureOverscroll + ) + /** DEBUG: whether the placeholder should be made slightly visible for positional debugging. */ val isVisualDebuggingEnabled: Boolean = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES) @@ -157,13 +168,6 @@ constructor( val syntheticScroll: Flow<Float> = interactor.syntheticScroll.dumpWhileCollecting("syntheticScroll") - /** - * Whether the current touch gesture is overscroll. If true, it means the NSSL has already - * consumed part of the gesture. - */ - val isCurrentGestureOverscroll: Flow<Boolean> = - interactor.isCurrentGestureOverscroll.dumpWhileCollecting("isCurrentGestureOverScroll") - /** Whether remote input is currently active for any notification. */ val isRemoteInputActive = remoteInputInteractor.isRemoteInputActive 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 e4e56c5de65b..8a5b22183563 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -527,7 +527,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp break; case MODE_SHOW_BOUNCER: Trace.beginSection("MODE_SHOW_BOUNCER"); - mKeyguardViewController.showPrimaryBouncer(true); + mKeyguardViewController.showPrimaryBouncer(true, + "BiometricUnlockController#MODE_SHOW_BOUNCER"); Trace.endSection(); break; case MODE_WAKE_AND_UNLOCK_FROM_DREAM: 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 9d9f01b571a7..e617254fa288 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -2407,11 +2407,12 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } if (needsBouncer) { - Log.d(TAG, "showBouncerOrLockScreenIfKeyguard, showingBouncer"); + var reason = "CentralSurfacesImpl#showBouncerOrLockScreenIfKeyguard"; if (SceneContainerFlag.isEnabled()) { - mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */); + mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */, + reason); } else { - mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */); + mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */, reason); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java index 0ba4aabcb0e2..34c193d18814 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java @@ -27,13 +27,19 @@ import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; +import androidx.collection.MutableIntObjectMap; + import com.android.internal.statusbar.StatusBarIcon; +import com.android.systemui.Flags; import com.android.systemui.demomode.DemoMode; +import com.android.systemui.kairos.ExperimentalKairosApi; +import com.android.systemui.kairos.KairosNetwork; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; import com.android.systemui.res.R; import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.StatusIconDisplayable; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos; import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger; import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView; import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; @@ -41,10 +47,20 @@ import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarVie import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView; import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel; +import dagger.Lazy; + +import kotlin.OptIn; +import kotlin.Pair; + +import kotlinx.coroutines.CoroutineScope; +import kotlinx.coroutines.Job; + import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CancellationException; //TODO: This should be a controller, not its own view +@OptIn(markerClass = ExperimentalKairosApi.class) public class DemoStatusIcons extends StatusIconContainer implements DemoMode, DarkReceiver { private static final String TAG = "DemoStatusIcons"; @@ -60,15 +76,27 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da private final MobileIconsViewModel mMobileIconsViewModel; private final StatusBarLocation mLocation; + private final Lazy<MobileUiAdapterKairos> mMobileUiAdapterKairos; + private final KairosNetwork mKairosNetwork; + private final CoroutineScope mAppScope; + + private final MutableIntObjectMap<Job> mBindingJobs = new MutableIntObjectMap<>(); + public DemoStatusIcons( LinearLayout statusIcons, MobileIconsViewModel mobileIconsViewModel, StatusBarLocation location, - int iconSize + int iconSize, + Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos, + KairosNetwork kairosNetwork, + CoroutineScope appScope ) { super(statusIcons.getContext()); mStatusIcons = statusIcons; mIconSize = iconSize; + mMobileUiAdapterKairos = mobileUiAdapterKairos; + mKairosNetwork = kairosNetwork; + mAppScope = appScope; mColor = DarkIconDispatcher.DEFAULT_ICON_TINT; mContrastColor = DarkIconDispatcher.DEFAULT_INVERSE_ICON_TINT; mMobileIconsViewModel = mobileIconsViewModel; @@ -236,24 +264,46 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da /** * Add a {@link ModernStatusBarMobileView} + * * @param mobileContext possibly mcc/mnc overridden mobile context - * @param subId the subscriptionId for this mobile view + * @param subId the subscriptionId for this mobile view */ public void addModernMobileView( Context mobileContext, MobileViewLogger mobileViewLogger, int subId) { Log.d(TAG, "addModernMobileView (subId=" + subId + ")"); - ModernStatusBarMobileView view = ModernStatusBarMobileView.constructAndBind( - mobileContext, - mobileViewLogger, - "mobile", - mMobileIconsViewModel.viewModelForSub(subId, mLocation) - ); - - // mobile always goes at the end - mModernMobileViews.add(view); - addView(view, getChildCount(), createLayoutParams()); + if (Flags.statusBarMobileIconKairos()) { + Pair<ModernStatusBarMobileView, Job> viewAndJob = + ModernStatusBarMobileView.constructAndBind( + mobileContext, + mobileViewLogger, + "mobile", + mMobileUiAdapterKairos.get().getMobileIconsViewModel().viewModelForSub( + subId, mLocation), + mAppScope, + subId, + mLocation, + mKairosNetwork + ); + ModernStatusBarMobileView view = viewAndJob.getFirst(); + mBindingJobs.put(subId, viewAndJob.getSecond()); + // mobile always goes at the end + mModernMobileViews.add(view); + addView(view, getChildCount(), createLayoutParams()); + } else { + ModernStatusBarMobileView view = + ModernStatusBarMobileView.constructAndBind( + mobileContext, + mobileViewLogger, + "mobile", + mMobileIconsViewModel.viewModelForSub(subId, mLocation) + ); + + // mobile always goes at the end + mModernMobileViews.add(view); + addView(view, getChildCount(), createLayoutParams()); + } } /** @@ -299,6 +349,12 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da ModernStatusBarMobileView mobileView = matchingModernMobileView( (ModernStatusBarMobileView) view); if (mobileView != null) { + if (Flags.statusBarMobileIconKairos()) { + Job job = mBindingJobs.remove(mobileView.getSubId()); + if (job != null) { + job.cancel(new CancellationException()); + } + } removeView(mobileView); mModernMobileViews.remove(mobileView); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index f3d72027238f..d68f7df79cd5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -538,10 +538,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private void handleBlurSupportedChanged(boolean isBlurSupported) { this.mIsBlurSupported = isBlurSupported; if (Flags.bouncerUiRevamp()) { - // TODO: animate blur fallback when the bouncer is pulled up. - for (ScrimState state : ScrimState.values()) { - state.setDefaultScrimAlpha(getDefaultScrimAlpha(true)); - } + updateDefaultScrimAlphas(); if (isBlurSupported) { ScrimState.BOUNCER_SCRIMMED.setNotifBlurRadius(mBlurConfig.getMaxBlurRadiusPx()); } else { @@ -549,17 +546,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump } } if (Flags.notificationShadeBlur()) { - float inFrontAlpha = mInFrontAlpha; - float behindAlpha = mBehindAlpha; - float notifAlpha = mNotificationsAlpha; - mState.prepare(mState); - applyState(); - startScrimAnimation(mScrimBehind, behindAlpha); - startScrimAnimation(mNotificationsScrim, notifAlpha); - startScrimAnimation(mScrimInFront, inFrontAlpha); - dispatchBackScrimState(mScrimBehind.getViewAlpha()); - } else if (Flags.bouncerUiRevamp()) { applyAndDispatchState(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 8c44fe56d269..512340913de2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -669,7 +669,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * show if any subsequent events are to be handled. */ if (!SceneContainerFlag.isEnabled() && beginShowingBouncer(event)) { - mPrimaryBouncerInteractor.show(/* isScrimmed= */false); + mPrimaryBouncerInteractor.show(/* isScrimmed= */false, + TAG + "#onPanelExpansionChanged"); } if (!primaryBouncerIsOrWillBeShowing()) { @@ -714,7 +715,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * Shows the notification keyguard or the bouncer depending on * {@link #needsFullscreenBouncer()}. */ - protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) { + protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset, + String reason) { boolean showBouncer = needsFullscreenBouncer() && !mDozing; if (Flags.simPinRaceConditionOnRestart()) { showBouncer = showBouncer && !mIsSleeping; @@ -726,11 +728,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mCentralSurfaces.hideKeyguard(); mSceneInteractorLazy.get().showOverlay( Overlays.Bouncer, - "StatusBarKeyguardViewManager.showBouncerOrKeyguard" + TAG + "#showBouncerOrKeyguard" ); } else { if (Flags.simPinRaceConditionOnRestart()) { - if (mPrimaryBouncerInteractor.show(/* isScrimmed= */ true)) { + if (mPrimaryBouncerInteractor.show(/* isScrimmed= */ true, reason)) { mAttemptsToShowBouncer = 0; mCentralSurfaces.hideKeyguard(); } else { @@ -744,19 +746,19 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb + mAttemptsToShowBouncer++); mExecutor.executeDelayed(() -> showBouncerOrKeyguard(hideBouncerWhenShowing, - isFalsingReset), + isFalsingReset, reason), 500); } } } else { mCentralSurfaces.hideKeyguard(); - mPrimaryBouncerInteractor.show(/* isScrimmed= */ true); + mPrimaryBouncerInteractor.show(/* isScrimmed= */ true, reason); } } } else if (!isFalsingReset) { // Falsing resets can cause this to flicker, so don't reset in this case Log.i(TAG, "Sim bouncer is already showing, issuing a refresh"); - mPrimaryBouncerInteractor.show(/* isScrimmed= */ true); + mPrimaryBouncerInteractor.show(/* isScrimmed= */ true, reason); } } else { @@ -776,7 +778,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * false when the user will be dragging it and translation should be deferred * {@see KeyguardBouncer#show(boolean, boolean)} */ - public void showBouncer(boolean scrimmed) { + public void showBouncer(boolean scrimmed, String reason) { if (SceneContainerFlag.isEnabled()) { mDeviceEntryInteractorLazy.get().attemptDeviceEntry(); return; @@ -787,7 +789,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mAlternateBouncerInteractor.forceShow(); updateAlternateBouncerShowing(mAlternateBouncerInteractor.isVisibleState()); } else { - showPrimaryBouncer(scrimmed); + showPrimaryBouncer(scrimmed, reason); } } @@ -810,8 +812,10 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * * @param scrimmed true when the bouncer should show scrimmed, false when the user will be * dragging it and translation should be deferred {@see KeyguardBouncer#show(boolean, boolean)} + * @param reason string description for what is causing the bouncer to be requested */ - public void showPrimaryBouncer(boolean scrimmed) { + @Override + public void showPrimaryBouncer(boolean scrimmed, String reason) { hideAlternateBouncer( /* updateScrim= */ false, // When the scene framework is on, don't ever clear the pending dismiss action from @@ -823,7 +827,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb "primary bouncer requested" ); } else { - mPrimaryBouncerInteractor.show(scrimmed); + mPrimaryBouncerInteractor.show(scrimmed, reason); } } updateStates(); @@ -870,7 +874,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb ); } - showBouncer(true); + showBouncer(true, TAG + "#dismissWithAction"); Trace.endSection(); return; } @@ -919,10 +923,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb if (SceneContainerFlag.isEnabled()) { mSceneInteractorLazy.get().showOverlay( Overlays.Bouncer, - "StatusBarKeyguardViewManager.dismissWithAction" + TAG + "#dismissWithAction" ); } else { - mPrimaryBouncerInteractor.show(/* isScrimmed= */ true); + mPrimaryBouncerInteractor.show(/* isScrimmed= */ true, + TAG + "#dismissWithAction, afterKeyguardGone"); } } else { // after authentication success, run dismiss action with the option to defer @@ -932,10 +937,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb if (SceneContainerFlag.isEnabled()) { mSceneInteractorLazy.get().showOverlay( Overlays.Bouncer, - "StatusBarKeyguardViewManager.dismissWithAction" + TAG + "#dismissWithAction" ); } else { - mPrimaryBouncerInteractor.show(/* isScrimmed= */ true); + mPrimaryBouncerInteractor.show(/* isScrimmed= */ true, + TAG + "#dismissWithAction"); } // bouncer will handle the dismiss action, so we no longer need to track it here mAfterKeyguardGoneAction = null; @@ -992,7 +998,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } } } else { - showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset); + showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset, "reset"); } if (!SceneContainerFlag.isEnabled() && hideBouncerWhenShowing && isBouncerShowing()) { hideAlternateBouncer(true); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java index 8389aab4aac8..85fc9d4589c0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java @@ -156,7 +156,8 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, if (!row.isPinned()) { mStatusBarStateController.setLeaveOpenOnKeyguardHide(true); } - mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */); + mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */, + "StatusBarRemoteInputCallback#onLockedRemoteInput"); mPendingRemoteInputView = clicked; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index 61b7d80a8c85..b7eada1c6804 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -38,7 +38,7 @@ import com.android.systemui.statusbar.chips.ui.view.ChipChronometer import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.notification.shared.CallType import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository @@ -347,7 +347,7 @@ constructor( * If the call notification also meets promoted notification criteria, this field is filled * in with the content related to promotion. Otherwise null. */ - val promotedContent: PromotedNotificationContentModel?, + val promotedContent: PromotedNotificationContentModels?, /** True if the call is currently ongoing (as opposed to incoming, screening, etc.). */ val isOngoing: Boolean, /** True if the user has swiped away the status bar while in this phone call. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt index 322dfff8f144..9546d374b595 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModel.kt @@ -18,7 +18,7 @@ package com.android.systemui.statusbar.phone.ongoingcall.shared.model import android.app.PendingIntent import com.android.systemui.statusbar.StatusBarIconView -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels /** Represents the state of any ongoing calls. */ sealed interface OngoingCallModel { @@ -47,7 +47,7 @@ sealed interface OngoingCallModel { val intent: PendingIntent?, val notificationKey: String, val appName: String, - val promotedContent: PromotedNotificationContentModel?, + val promotedContent: PromotedNotificationContentModels?, val isAppVisible: Boolean, ) : OngoingCallModel } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/DarkIconManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/DarkIconManager.java index 8d314eeb8493..6dcc0ada8c65 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/DarkIconManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/DarkIconManager.java @@ -19,6 +19,9 @@ package com.android.systemui.statusbar.phone.ui; import android.widget.LinearLayout; import com.android.internal.statusbar.StatusBarIcon; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.kairos.ExperimentalKairosApi; +import com.android.systemui.kairos.KairosNetwork; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.statusbar.StatusIconDisplayable; import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider; @@ -26,13 +29,20 @@ import com.android.systemui.statusbar.phone.DemoStatusIcons; import com.android.systemui.statusbar.phone.StatusBarIconHolder; import com.android.systemui.statusbar.phone.StatusBarLocation; import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos; import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter; +import dagger.Lazy; import dagger.assisted.Assisted; import dagger.assisted.AssistedFactory; import dagger.assisted.AssistedInject; +import kotlin.OptIn; + +import kotlinx.coroutines.CoroutineScope; + /** Version of {@link IconManager} that observes state from the {@link DarkIconDispatcher}. */ +@OptIn(markerClass = ExperimentalKairosApi.class) public class DarkIconManager extends IconManager { private final DarkIconDispatcher mDarkIconDispatcher; private final int mIconHorizontalMargin; @@ -43,13 +53,21 @@ public class DarkIconManager extends IconManager { @Assisted StatusBarLocation location, WifiUiAdapter wifiUiAdapter, MobileUiAdapter mobileUiAdapter, + Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos, MobileContextProvider mobileContextProvider, + KairosNetwork kairosNetwork, + @Application CoroutineScope appScope, @Assisted DarkIconDispatcher darkIconDispatcher) { - super(linearLayout, location, wifiUiAdapter, mobileUiAdapter, mobileContextProvider); - mIconHorizontalMargin = - mContext.getResources() - .getDimensionPixelSize( - com.android.systemui.res.R.dimen.status_bar_icon_horizontal_margin); + super(linearLayout, + location, + wifiUiAdapter, + mobileUiAdapter, + mobileUiAdapterKairos, + mobileContextProvider, + kairosNetwork, + appScope); + mIconHorizontalMargin = mContext.getResources().getDimensionPixelSize( + com.android.systemui.res.R.dimen.status_bar_icon_horizontal_margin); mDarkIconDispatcher = darkIconDispatcher; } @@ -104,7 +122,7 @@ public class DarkIconManager extends IconManager { super.exitDemoMode(); } - /** */ + /** */ @AssistedFactory public interface Factory { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java index fd16c6090cb1..862931af7504 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java @@ -24,12 +24,19 @@ import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI import android.annotation.Nullable; import android.content.Context; import android.os.Bundle; +import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; +import androidx.annotation.OptIn; +import androidx.collection.MutableIntObjectMap; + import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.statusbar.StatusBarIcon.Shape; +import com.android.systemui.Flags; import com.android.systemui.demomode.DemoModeCommandReceiver; +import com.android.systemui.kairos.ExperimentalKairosApi; +import com.android.systemui.kairos.KairosNetwork; import com.android.systemui.modes.shared.ModesUiIcons; import com.android.systemui.statusbar.BaseStatusBarFrameLayout; import com.android.systemui.statusbar.StatusBarIconView; @@ -40,6 +47,7 @@ import com.android.systemui.statusbar.phone.StatusBarIconHolder; import com.android.systemui.statusbar.phone.StatusBarIconHolder.BindableIconHolder; import com.android.systemui.statusbar.phone.StatusBarLocation; import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos; import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder; import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView; import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; @@ -49,20 +57,34 @@ import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiV import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel; import com.android.systemui.util.Assert; +import dagger.Lazy; + +import kotlin.Pair; + +import kotlinx.coroutines.CoroutineScope; +import kotlinx.coroutines.Job; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CancellationException; /** * Turns info from StatusBarIconController into ImageViews in a ViewGroup. */ +@OptIn(markerClass = ExperimentalKairosApi.class) public class IconManager implements DemoModeCommandReceiver { protected final ViewGroup mGroup; private final MobileContextProvider mMobileContextProvider; private final LocationBasedWifiViewModel mWifiViewModel; private final MobileIconsViewModel mMobileIconsViewModel; + private final Lazy<MobileUiAdapterKairos> mMobileUiAdapterKairos; + private final KairosNetwork mKairosNetwork; + private final CoroutineScope mAppScope; + private final MutableIntObjectMap<Job> mBindingJobs = new MutableIntObjectMap<>(); + /** * Stores the list of bindable icons that have been added, keyed on slot name. This ensures * we don't accidentally add the same bindable icon twice. @@ -87,12 +109,17 @@ public class IconManager implements DemoModeCommandReceiver { StatusBarLocation location, WifiUiAdapter wifiUiAdapter, MobileUiAdapter mobileUiAdapter, - MobileContextProvider mobileContextProvider + Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos, + MobileContextProvider mobileContextProvider, + KairosNetwork kairosNetwork, + CoroutineScope appScope ) { mGroup = group; mMobileContextProvider = mobileContextProvider; mContext = group.getContext(); mLocation = location; + mKairosNetwork = kairosNetwork; + mAppScope = appScope; reloadDimens(); @@ -101,6 +128,9 @@ public class IconManager implements DemoModeCommandReceiver { mMobileIconsViewModel = mobileUiAdapter.getMobileIconsViewModel(); MobileIconsBinder.bind(mGroup, mMobileIconsViewModel); + + mMobileUiAdapterKairos = mobileUiAdapterKairos; + mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, mLocation); } @@ -150,7 +180,7 @@ public class IconManager implements DemoModeCommandReceiver { case TYPE_MOBILE_NEW -> addNewMobileIcon(index, slot, holder.getTag()); case TYPE_BINDABLE -> // Safe cast, since only BindableIconHolders can set this tag on themselves - addBindableIcon((BindableIconHolder) holder, index); + addBindableIcon((BindableIconHolder) holder, index); default -> null; }; } @@ -223,13 +253,30 @@ public class IconManager implements DemoModeCommandReceiver { private ModernStatusBarMobileView onCreateModernStatusBarMobileView( String slot, int subId) { Context mobileContext = mMobileContextProvider.getMobileContextForSub(subId, mContext); - return ModernStatusBarMobileView - .constructAndBind( - mobileContext, - mMobileIconsViewModel.getLogger(), - slot, - mMobileIconsViewModel.viewModelForSub(subId, mLocation) - ); + if (Flags.statusBarMobileIconKairos()) { + Pair<ModernStatusBarMobileView, Job> viewAndJob = + ModernStatusBarMobileView.constructAndBind( + mobileContext, + mMobileUiAdapterKairos.get().getMobileIconsViewModel().getLogger(), + slot, + mMobileUiAdapterKairos.get().getMobileIconsViewModel() + .viewModelForSub(subId, mLocation), + mAppScope, + subId, + mLocation, + mKairosNetwork + ); + mBindingJobs.put(subId, viewAndJob.getSecond()); + return viewAndJob.getFirst(); + } else { + return ModernStatusBarMobileView + .constructAndBind( + mobileContext, + mMobileIconsViewModel.getLogger(), + slot, + mMobileIconsViewModel.viewModelForSub(subId, mLocation) + ); + } } protected LinearLayout.LayoutParams onCreateLayoutParams(Shape shape) { @@ -253,6 +300,15 @@ public class IconManager implements DemoModeCommandReceiver { if (mIsInDemoMode) { mDemoStatusIcons.onRemoveIcon((StatusIconDisplayable) mGroup.getChildAt(viewIndex)); } + if (Flags.statusBarMobileIconKairos()) { + View view = mGroup.getChildAt(viewIndex); + if (view instanceof ModernStatusBarMobileView) { + Job bindingJob = mBindingJobs.remove(((ModernStatusBarMobileView) view).getSubId()); + if (bindingJob != null) { + bindingJob.cancel(new CancellationException()); + } + } + } mGroup.removeViewAt(viewIndex); } @@ -326,7 +382,10 @@ public class IconManager implements DemoModeCommandReceiver { (LinearLayout) mGroup, mMobileIconsViewModel, mLocation, - mIconSize + mIconSize, + mMobileUiAdapterKairos, + mKairosNetwork, + mAppScope ); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/TintedIconManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/TintedIconManager.java index e520148e925a..da59fc073c86 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/TintedIconManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/TintedIconManager.java @@ -20,19 +20,30 @@ import android.view.View; import android.view.ViewGroup; 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.statusbar.StatusIconDisplayable; import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider; import com.android.systemui.statusbar.phone.DemoStatusIcons; import com.android.systemui.statusbar.phone.StatusBarIconHolder; import com.android.systemui.statusbar.phone.StatusBarLocation; import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos; import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter; +import dagger.Lazy; + +import kotlin.OptIn; + +import kotlinx.coroutines.CoroutineScope; + import javax.inject.Inject; /** * Version of {@link IconManager} that can tint the icons to a particular color. */ +@OptIn(markerClass = ExperimentalKairosApi.class) public class TintedIconManager extends IconManager { // The main tint, used as the foreground in non layer drawables private int mColor; @@ -44,13 +55,17 @@ public class TintedIconManager extends IconManager { StatusBarLocation location, WifiUiAdapter wifiUiAdapter, MobileUiAdapter mobileUiAdapter, - MobileContextProvider mobileContextProvider + Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos, + MobileContextProvider mobileContextProvider, + KairosNetwork kairosNetwork, + CoroutineScope appScope ) { super(group, location, wifiUiAdapter, mobileUiAdapter, - mobileContextProvider); + mobileUiAdapterKairos, + mobileContextProvider, kairosNetwork, appScope); } @Override @@ -99,16 +114,25 @@ public class TintedIconManager extends IconManager { private final WifiUiAdapter mWifiUiAdapter; private final MobileContextProvider mMobileContextProvider; private final MobileUiAdapter mMobileUiAdapter; + private final Lazy<MobileUiAdapterKairos> mMobileUiAdapterKairos; + private final KairosNetwork mKairosNetwork; + private final CoroutineScope mAppScope; @Inject public Factory( WifiUiAdapter wifiUiAdapter, MobileUiAdapter mobileUiAdapter, - MobileContextProvider mobileContextProvider + MobileContextProvider mobileContextProvider, + Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos, + KairosNetwork kairosNetwork, + @Application CoroutineScope appScope ) { mWifiUiAdapter = wifiUiAdapter; mMobileUiAdapter = mobileUiAdapter; mMobileContextProvider = mobileContextProvider; + mMobileUiAdapterKairos = mobileUiAdapterKairos; + mKairosNetwork = kairosNetwork; + mAppScope = appScope; } /** Creates a new {@link TintedIconManager} for the given view group and location. */ @@ -118,7 +142,10 @@ public class TintedIconManager extends IconManager { location, mWifiUiAdapter, mMobileUiAdapter, - mMobileContextProvider); + mMobileUiAdapterKairos, + mMobileContextProvider, + mKairosNetwork, + mAppScope); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairos.kt new file mode 100644 index 000000000000..9881b354d8d9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairos.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui + +import com.android.systemui.Dumpable +import com.android.systemui.KairosActivatable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.kairos.BuildScope +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.awaitClose +import com.android.systemui.kairos.combine +import com.android.systemui.kairos.launchEffect +import com.android.systemui.shade.carrier.ShadeCarrierGroupController +import com.android.systemui.statusbar.phone.ui.StatusBarIconController +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorKairos +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModelKairos +import java.io.PrintWriter +import javax.inject.Inject + +/** + * This class is intended to provide a context to collect on the + * [MobileIconsInteractorKairos.filteredSubscriptions] data source and supply a state flow that can + * control [StatusBarIconController] to keep the old UI in sync with the new data source. + * + * It also provides a mechanism to create a top-level view model for each IconManager to know about + * the list of available mobile lines of service for which we want to show icons. + */ +@ExperimentalKairosApi +@SysUISingleton +class MobileUiAdapterKairos +@Inject +constructor( + private val iconController: StatusBarIconController, + val mobileIconsViewModel: MobileIconsViewModelKairos, + private val logger: MobileViewLogger, + dumpManager: DumpManager, +) : KairosActivatable, Dumpable { + + init { + dumpManager.registerNormalDumpable(this) + } + + private var isCollecting: Boolean = false + private var lastValue: List<Int>? = null + + private var shadeCarrierGroupController: ShadeCarrierGroupController? = null + + override fun BuildScope.activate() { + launchEffect { + isCollecting = true + awaitClose { isCollecting = false } + } + // Start notifying the icon controller of subscriptions + combine(mobileIconsViewModel.subscriptionIds, mobileIconsViewModel.isStackable) { a, b -> + Pair(a, b) + } + .observe { (subIds, isStackable) -> + logger.logUiAdapterSubIdsSentToIconController(subIds, isStackable) + lastValue = subIds + if (isStackable) { + // Passing an empty list to remove pre-existing mobile icons. + // StackedMobileBindableIcon will show the stacked icon instead. + iconController.setNewMobileIconSubIds(emptyList()) + } else { + iconController.setNewMobileIconSubIds(subIds) + } + shadeCarrierGroupController?.updateModernMobileIcons(subIds) + } + } + + /** Set the [ShadeCarrierGroupController] to notify of subscription updates */ + fun setShadeCarrierGroupController(controller: ShadeCarrierGroupController) { + shadeCarrierGroupController = controller + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println("isCollecting=$isCollecting") + pw.println("Last values sent to icon controller: $lastValue") + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt index 4c2849de34ee..dec26886e063 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt @@ -20,10 +20,12 @@ import android.view.View import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager +import com.android.systemui.kairos.ExperimentalKairosApi import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel import com.android.systemui.statusbar.pipeline.dagger.MobileViewLog import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModelKairos import java.io.PrintWriter import javax.inject.Inject @@ -52,14 +54,17 @@ constructor(@MobileViewLog private val buffer: LogBuffer, dumpManager: DumpManag ) } - fun logNewViewBinding(view: View, viewModel: LocationBasedMobileViewModel) { + fun logNewViewBinding(view: View, viewModel: LocationBasedMobileViewModel) = + logNewViewBinding(view, viewModel, viewModel.location.name) + + fun logNewViewBinding(view: View, viewModel: Any, location: String) { buffer.log( TAG, LogLevel.INFO, { str1 = view.getIdForLogging() str2 = viewModel.getIdForLogging() - str3 = viewModel.location.name + str3 = location }, { "New view binding. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, ) @@ -93,6 +98,36 @@ constructor(@MobileViewLog private val buffer: LogBuffer, dumpManager: DumpManag ) } + @OptIn(ExperimentalKairosApi::class) + fun logCollectionStarted(view: View, viewModel: LocationBasedMobileViewModelKairos) { + collectionStatuses[view.getIdForLogging()] = true + buffer.log( + TAG, + LogLevel.INFO, + { + str1 = view.getIdForLogging() + str2 = viewModel.getIdForLogging() + str3 = viewModel.location.name + }, + { "Collection started. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, + ) + } + + @OptIn(ExperimentalKairosApi::class) + fun logCollectionStopped(view: View, viewModel: LocationBasedMobileViewModelKairos) { + collectionStatuses[view.getIdForLogging()] = false + buffer.log( + TAG, + LogLevel.INFO, + { + str1 = view.getIdForLogging() + str2 = viewModel.getIdForLogging() + str3 = viewModel.location.name + }, + { "Collection stopped. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, + ) + } + override fun dump(pw: PrintWriter, args: Array<out String>) { pw.println("Collection statuses per view:---") collectionStatuses.forEach { viewId, isCollecting -> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt index 32ebe884062d..bda76b72c08a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt @@ -19,15 +19,19 @@ package com.android.systemui.statusbar.pipeline.mobile.ui import android.content.Context import com.android.settingslib.flags.Flags import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.KairosNetwork import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator import com.android.systemui.statusbar.pipeline.mobile.ui.binder.StackedMobileIconBinder import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModelImpl +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModelKairos import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarComposeIconView import javax.inject.Inject +@OptIn(ExperimentalKairosApi::class) @SysUISingleton class StackedMobileBindableIcon @Inject @@ -35,6 +39,8 @@ constructor( context: Context, mobileIconsViewModel: MobileIconsViewModel, viewModelFactory: StackedMobileIconViewModelImpl.Factory, + kairosViewModelFactory: StackedMobileIconViewModelKairos.Factory, + kairosNetwork: KairosNetwork, ) : BindableIcon { override val slot: String = context.getString(com.android.internal.R.string.status_bar_stacked_mobile) @@ -42,7 +48,13 @@ constructor( override val initializer = ModernStatusBarViewCreator { context -> SingleBindableStatusBarComposeIconView.createView(context).also { view -> view.initView(slot) { - StackedMobileIconBinder.bind(view, mobileIconsViewModel, viewModelFactory) + StackedMobileIconBinder.bind( + view, + mobileIconsViewModel, + viewModelFactory, + kairosViewModelFactory, + kairosNetwork, + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt index 0eef2e1ca685..0abd6d8d66b3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt @@ -50,7 +50,7 @@ import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged -private data class Colors(@ColorInt val tint: Int, @ColorInt val contrast: Int) +data class MobileIconColors(@ColorInt val tint: Int, @ColorInt val contrast: Int) object MobileIconBinder { /** Binds the view to the view-model, continuing to update the former based on the latter */ @@ -80,9 +80,9 @@ object MobileIconBinder { @StatusBarIconView.VisibleState val visibilityState: MutableStateFlow<Int> = MutableStateFlow(initialVisibilityState) - val iconTint: MutableStateFlow<Colors> = + val iconTint: MutableStateFlow<MobileIconColors> = MutableStateFlow( - Colors( + MobileIconColors( tint = DarkIconDispatcher.DEFAULT_ICON_TINT, contrast = DarkIconDispatcher.DEFAULT_INVERSE_ICON_TINT, ) @@ -291,7 +291,7 @@ object MobileIconBinder { } override fun onIconTintChanged(newTint: Int, contrastTint: Int) { - iconTint.value = Colors(tint = newTint, contrast = contrastTint) + iconTint.value = MobileIconColors(tint = newTint, contrast = contrastTint) } override fun onDecorTintChanged(newTint: Int) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinderKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinderKairos.kt new file mode 100644 index 000000000000..1078ae343572 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinderKairos.kt @@ -0,0 +1,290 @@ +/* + * 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.binder + +import android.content.res.ColorStateList +import android.graphics.Color +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.Space +import androidx.core.view.isVisible +import com.android.settingslib.graph.SignalDrawable +import com.android.systemui.Flags +import com.android.systemui.common.ui.binder.IconViewBinder +import com.android.systemui.kairos.BuildScope +import com.android.systemui.kairos.BuildSpec +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.KairosNetwork +import com.android.systemui.kairos.MutableState +import com.android.systemui.kairos.effect +import com.android.systemui.lifecycle.repeatWhenAttachedToWindow +import com.android.systemui.lifecycle.repeatWhenWindowIsVisible +import com.android.systemui.plugins.DarkIconDispatcher +import com.android.systemui.res.R +import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModelKairos +import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding +import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper +import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarViewBinderConstants +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.launch + +object MobileIconBinderKairos { + + @ExperimentalKairosApi + fun bind( + view: ViewGroup, + viewModel: BuildSpec<LocationBasedMobileViewModelKairos>, + @StatusBarIconView.VisibleState + initialVisibilityState: Int = StatusBarIconView.STATE_HIDDEN, + logger: MobileViewLogger, + scope: CoroutineScope, + kairosNetwork: KairosNetwork, + ): Pair<ModernStatusBarViewBinding, Job> { + val binding = ModernStatusBarViewBindingKairosImpl(kairosNetwork, initialVisibilityState) + return binding to + scope.launch { + view.repeatWhenAttachedToWindow { + kairosNetwork.activateSpec { + bind( + view = view, + viewModel = viewModel.applySpec(), + logger = logger, + binding = binding, + ) + } + } + } + } + + @ExperimentalKairosApi + private class ModernStatusBarViewBindingKairosImpl( + kairosNetwork: KairosNetwork, + initialVisibilityState: Int, + ) : ModernStatusBarViewBinding { + + @JvmField var shouldIconBeVisible: Boolean = false + @JvmField var isCollecting: Boolean = false + + // TODO(b/238425913): We should log this visibility state. + val visibility = MutableState(kairosNetwork, initialVisibilityState) + val iconTint = + MutableState( + kairosNetwork, + MobileIconColors( + tint = DarkIconDispatcher.DEFAULT_ICON_TINT, + contrast = DarkIconDispatcher.DEFAULT_INVERSE_ICON_TINT, + ), + ) + val decorTint = MutableState(kairosNetwork, Color.WHITE) + + override fun getShouldIconBeVisible(): Boolean = shouldIconBeVisible + + override fun onVisibilityStateChanged(state: Int) { + visibility.setValue(state) + } + + override fun onIconTintChanged(newTint: Int, contrastTint: Int) { + iconTint.setValue(MobileIconColors(tint = newTint, contrast = contrastTint)) + } + + override fun onDecorTintChanged(newTint: Int) { + decorTint.setValue(newTint) + } + + override fun isCollecting(): Boolean = isCollecting + } + + @ExperimentalKairosApi + private fun BuildScope.bind( + view: ViewGroup, + viewModel: LocationBasedMobileViewModelKairos, + logger: MobileViewLogger, + binding: ModernStatusBarViewBindingKairosImpl, + ) { + viewModel.isVisible.observe { binding.shouldIconBeVisible = it } + + val mobileGroupView = view.requireViewById<ViewGroup>(R.id.mobile_group) + val activityContainer = view.requireViewById<View>(R.id.inout_container) + val activityIn = view.requireViewById<ImageView>(R.id.mobile_in) + val activityOut = view.requireViewById<ImageView>(R.id.mobile_out) + val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type) + val networkTypeContainer = view.requireViewById<FrameLayout>(R.id.mobile_type_container) + val iconView = view.requireViewById<ImageView>(R.id.mobile_signal) + val mobileDrawable = SignalDrawable(view.context) + val roamingView = view.requireViewById<ImageView>(R.id.mobile_roaming) + val roamingSpace = view.requireViewById<Space>(R.id.mobile_roaming_space) + val dotView = view.requireViewById<StatusBarIconView>(R.id.status_bar_dot) + + effect { + view.isVisible = viewModel.isVisible.sample() + iconView.isVisible = true + launch { + view.repeatWhenAttachedToWindow { + // isVisible controls the visibility state of the outer group, and thus it needs + // to run in the CREATED lifecycle so it can continue to watch while invisible + // See (b/291031862) for details + kairosNetwork.activateSpec { + viewModel.isVisible.observe { isVisible -> + viewModel.verboseLogger?.logBinderReceivedVisibility( + view, + viewModel.subscriptionId, + isVisible, + ) + view.isVisible = isVisible + // [StatusIconContainer] can get out of sync sometimes. Make sure to + // request another layout when this changes. + view.requestLayout() + } + } + } + } + launch { + view.repeatWhenWindowIsVisible { + logger.logCollectionStarted(view, viewModel) + binding.isCollecting = true + kairosNetwork.activateSpec { + binding.visibility.observe { state -> + ModernStatusBarViewVisibilityHelper.setVisibilityState( + state, + mobileGroupView, + dotView, + ) + view.requestLayout() + } + + // Set the icon for the triangle + viewModel.icon.observe { icon -> + viewModel.verboseLogger?.logBinderReceivedSignalIcon( + view, + viewModel.subscriptionId, + icon, + ) + if (icon is SignalIconModel.Cellular) { + iconView.setImageDrawable(mobileDrawable) + mobileDrawable.level = icon.toSignalDrawableState() + } else if (icon is SignalIconModel.Satellite) { + IconViewBinder.bind(icon.icon, iconView) + } + } + + viewModel.contentDescription.observe { + MobileContentDescriptionViewBinder.bind(it, view) + } + + // Set the network type icon + viewModel.networkTypeIcon.observe { dataTypeId -> + viewModel.verboseLogger?.logBinderReceivedNetworkTypeIcon( + view, + viewModel.subscriptionId, + dataTypeId, + ) + dataTypeId?.let { IconViewBinder.bind(dataTypeId, networkTypeView) } + val prevVis = networkTypeContainer.visibility + networkTypeContainer.visibility = + if (dataTypeId != null) View.VISIBLE else View.GONE + + if (prevVis != networkTypeContainer.visibility) { + view.requestLayout() + } + } + + // Set the network type background + viewModel.networkTypeBackground.observe { background -> + networkTypeContainer.setBackgroundResource(background?.res ?: 0) + + // Tint will invert when this bit changes + if (background?.res != null) { + networkTypeContainer.backgroundTintList = + ColorStateList.valueOf(binding.iconTint.sample().tint) + networkTypeView.imageTintList = + ColorStateList.valueOf(binding.iconTint.sample().contrast) + } else { + networkTypeView.imageTintList = + ColorStateList.valueOf(binding.iconTint.sample().tint) + } + } + + // Set the roaming indicator + viewModel.roaming.observe { isRoaming -> + roamingView.isVisible = isRoaming + roamingSpace.isVisible = isRoaming + } + + if (Flags.statusBarStaticInoutIndicators()) { + // Set the opacity of the activity indicators + viewModel.activityInVisible.observe { visible -> + activityIn.imageAlpha = + (if (visible) StatusBarViewBinderConstants.ALPHA_ACTIVE + else StatusBarViewBinderConstants.ALPHA_INACTIVE) + } + viewModel.activityOutVisible.observe { visible -> + activityOut.imageAlpha = + (if (visible) StatusBarViewBinderConstants.ALPHA_ACTIVE + else StatusBarViewBinderConstants.ALPHA_INACTIVE) + } + } else { + // Set the activity indicators + viewModel.activityInVisible.observe { activityIn.isVisible = it } + viewModel.activityOutVisible.observe { activityOut.isVisible = it } + } + + viewModel.activityContainerVisible.observe { + activityContainer.isVisible = it + } + + // Set the tint + binding.iconTint.observe { colors -> + val tint = ColorStateList.valueOf(colors.tint) + val contrast = ColorStateList.valueOf(colors.contrast) + + iconView.imageTintList = tint + + // If the bg is visible, tint it and use the contrast for the fg + if (viewModel.networkTypeBackground.sample() != null) { + networkTypeContainer.backgroundTintList = tint + networkTypeView.imageTintList = contrast + } else { + networkTypeView.imageTintList = tint + } + + roamingView.imageTintList = tint + activityIn.imageTintList = tint + activityOut.imageTintList = tint + dotView.setDecorColor(colors.tint) + } + + binding.decorTint.observe { tint -> dotView.setDecorColor(tint) } + } + + try { + awaitCancellation() + } finally { + binding.isCollecting = false + logger.logCollectionStopped(view, viewModel) + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt index 5c80fda72373..5238f3ec800a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt @@ -19,10 +19,10 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.binder import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel import com.android.systemui.util.AutoMarqueeTextView -import com.android.app.tracing.coroutines.launchTraced as launch object ShadeCarrierBinder { /** Binds the view to the view-model, continuing to update the former based on the latter */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinderKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinderKairos.kt new file mode 100644 index 000000000000..a782116e669c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinderKairos.kt @@ -0,0 +1,42 @@ +/* + * 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.binder + +import androidx.core.view.isVisible +import com.android.systemui.kairos.BuildSpec +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.KairosNetwork +import com.android.systemui.lifecycle.repeatWhenWindowIsVisible +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModelKairos +import com.android.systemui.util.AutoMarqueeTextView + +object ShadeCarrierBinderKairos { + /** Binds the view to the view-model, continuing to update the former based on the latter */ + @ExperimentalKairosApi + suspend fun bind( + carrierTextView: AutoMarqueeTextView, + viewModel: BuildSpec<ShadeCarrierGroupMobileIconViewModelKairos>, + kairosNetwork: KairosNetwork, + ) { + carrierTextView.isVisible = true + carrierTextView.repeatWhenWindowIsVisible { + kairosNetwork.activateSpec { + viewModel.applySpec().carrierName.observe { carrierTextView.text = it } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt index fef5bfe2b7d8..54cd8e3c46e4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt @@ -22,19 +22,28 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.Flags +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.KairosNetwork import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModelImpl +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModelKairos import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIcon import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarComposeIconView +import com.android.systemui.util.composable.kairos.rememberKairosActivatable object StackedMobileIconBinder { + @OptIn(ExperimentalKairosApi::class) fun bind( view: SingleBindableStatusBarComposeIconView, mobileIconsViewModel: MobileIconsViewModel, viewModelFactory: StackedMobileIconViewModelImpl.Factory, + kairosViewModelFactory: StackedMobileIconViewModelKairos.Factory, + kairosNetwork: KairosNetwork, ): ModernStatusBarViewBinding { return SingleBindableStatusBarComposeIconView.withDefaultBinding( view = view, @@ -47,9 +56,15 @@ object StackedMobileIconBinder { ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed ) setContent { - val viewModel = - rememberViewModel("StackedMobileIconBinder") { - viewModelFactory.create() + val viewModel: StackedMobileIconViewModel = + if (Flags.statusBarMobileIconKairos()) { + rememberKairosActivatable(kairosNetwork) { + kairosViewModelFactory.create() + } + } else { + rememberViewModel("StackedMobileIconBinder") { + viewModelFactory.create() + } } if (viewModel.isIconVisible) { CompositionLocalProvider(LocalContentColor provides Color(tint())) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt index fbd074d5b003..f1c5fee808cf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt @@ -20,22 +20,30 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.widget.LinearLayout +import com.android.systemui.kairos.BuildSpec +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.KairosNetwork import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON +import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder +import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinderKairos import com.android.systemui.statusbar.pipeline.mobile.ui.binder.ShadeCarrierBinder +import com.android.systemui.statusbar.pipeline.mobile.ui.binder.ShadeCarrierBinderKairos import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModelKairos import com.android.systemui.util.AutoMarqueeTextView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch /** * ViewGroup containing a mobile carrier name and icon in the Shade Header. Can be multiple * instances as children under [ShadeCarrierGroup] */ -class ModernShadeCarrierGroupMobileView( - context: Context, - attrs: AttributeSet?, -) : LinearLayout(context, attrs) { +class ModernShadeCarrierGroupMobileView(context: Context, attrs: AttributeSet?) : + LinearLayout(context, attrs) { var subId: Int = -1 @@ -73,5 +81,49 @@ class ModernShadeCarrierGroupMobileView( ShadeCarrierBinder.bind(textView, viewModel) } } + + /** + * Inflates a new instance of [ModernShadeCarrierGroupMobileView], binds it to [viewModel], + * and returns it. + */ + @ExperimentalKairosApi + @JvmStatic + fun constructAndBind( + context: Context, + logger: MobileViewLogger, + slot: String, + viewModel: BuildSpec<ShadeCarrierGroupMobileIconViewModelKairos>, + scope: CoroutineScope, + subscriptionId: Int, + location: StatusBarLocation, + kairosNetwork: KairosNetwork, + ): Pair<ModernShadeCarrierGroupMobileView, Job> { + val view = + (LayoutInflater.from(context).inflate(R.layout.shade_carrier_new, null) + as ModernShadeCarrierGroupMobileView) + .apply { subId = subscriptionId } + return view to + scope.launch { + val iconView = + view.requireViewById<ModernStatusBarMobileView>(R.id.mobile_combo) + iconView.initView(slot) { + val (binding, _) = + MobileIconBinderKairos.bind( + view = iconView, + viewModel = viewModel, + initialVisibilityState = STATE_ICON, + logger = logger, + scope = this, + kairosNetwork = kairosNetwork, + ) + binding + } + logger.logNewViewBinding(view, viewModel, location.name) + + val textView = + view.requireViewById<AutoMarqueeTextView>(R.id.mobile_carrier_text) + launch { ShadeCarrierBinderKairos.bind(textView, viewModel, kairosNetwork) } + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt index 7eda87f8418d..382af7e135ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt @@ -21,13 +21,22 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.widget.FrameLayout import android.widget.ImageView +import com.android.settingslib.flags.Flags.newStatusBarIcons +import com.android.systemui.kairos.BuildSpec +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.KairosNetwork import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView.getVisibleStateString import com.android.systemui.statusbar.core.NewStatusBarIcons +import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder +import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinderKairos import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModelKairos import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job class ModernStatusBarMobileView(context: Context, attrs: AttributeSet?) : ModernStatusBarView(context, attrs) { @@ -98,5 +107,58 @@ class ModernStatusBarMobileView(context: Context, attrs: AttributeSet?) : logger.logNewViewBinding(it, viewModel) } } + + /** + * Inflates a new instance of [ModernStatusBarMobileView], binds it to [viewModel], and + * returns it. + */ + @ExperimentalKairosApi + @JvmStatic + fun constructAndBind( + context: Context, + logger: MobileViewLogger, + slot: String, + viewModel: BuildSpec<LocationBasedMobileViewModelKairos>, + scope: CoroutineScope, + subscriptionId: Int, + location: StatusBarLocation, + kairosNetwork: KairosNetwork, + ): Pair<ModernStatusBarMobileView, Job> { + val view = + (LayoutInflater.from(context) + .inflate(R.layout.status_bar_mobile_signal_group_new, null) + as ModernStatusBarMobileView) + .apply { + // Flag-specific configuration + if (newStatusBarIcons()) { + // New icon (with no embedded whitespace) is slightly shorter + // (but actually taller) + val iconView = requireViewById<ImageView>(R.id.mobile_signal) + val lp = iconView.layoutParams + lp.height = + context.resources.getDimensionPixelSize( + R.dimen.status_bar_mobile_signal_size_updated + ) + } + + subId = subscriptionId + } + + lateinit var jobResult: Job + view.initView(slot) { + val (binding, job) = + MobileIconBinderKairos.bind( + view = view, + viewModel = viewModel, + logger = logger, + scope = scope, + kairosNetwork = kairosNetwork, + ) + jobResult = job + binding + } + logger.logNewViewBinding(view, viewModel, location.name) + return view to jobResult + } } } 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 index 0a0f9640a920..bd42d5bf401f 100644 --- 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 @@ -60,17 +60,12 @@ interface MobileIconViewModelKairosCommon { } /** - * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over - * a single line of service via [MobileIconInteractorKairos] and update the UI based on that - * subscription's information. + * View model for the state of a single mobile icon. Each [MobileIconViewModelKairos] will keep + * watch over a single line of service via [MobileIconInteractorKairos] and update the UI based on + * that subscription's information. * * There will be exactly one [MobileIconViewModelKairos] per filtered subscription offered from * [MobileIconsInteractorKairos.filteredSubscriptions]. - * - * For the sake of keeping log spam in check, every flow funding the - * [MobileIconViewModelKairosCommon] 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. */ @ExperimentalKairosApi class MobileIconViewModelKairos( 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 index ada5500a6f3c..41c72bee9e52 100644 --- 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 @@ -17,7 +17,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import androidx.compose.runtime.State as ComposeState -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.getValue import com.android.systemui.Flags import com.android.systemui.KairosActivatable import com.android.systemui.KairosBuilder @@ -31,7 +31,6 @@ import com.android.systemui.kairos.Incremental import com.android.systemui.kairos.State as KairosState import com.android.systemui.kairos.State import com.android.systemui.kairos.buildSpec -import com.android.systemui.kairos.changes import com.android.systemui.kairos.combine import com.android.systemui.kairos.flatten import com.android.systemui.kairos.map @@ -46,6 +45,7 @@ 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 com.android.systemui.util.composable.kairos.toComposeState import dagger.Provides import dagger.multibindings.ElementsIntoSet import javax.inject.Inject @@ -53,7 +53,7 @@ import javax.inject.Provider /** * 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 + * of [MobileIconViewModelKairos]s which describe the individual icons and can be bound to * [ModernStatusBarMobileView]. */ @ExperimentalKairosApi @@ -145,20 +145,16 @@ constructor( @ExperimentalKairosApi class MobileIconsViewModelKairosComposeWrapper( - val icons: ComposeState<Map<Int, MobileIconViewModelKairos>> -) - -@ExperimentalKairosApi -fun composeWrapper( - viewModel: MobileIconsViewModelKairos -): BuildSpec<MobileIconsViewModelKairosComposeWrapper> = buildSpec { - MobileIconsViewModelKairosComposeWrapper(icons = toComposeState(viewModel.icons)) + icons: ComposeState<Map<Int, MobileIconViewModelKairos>>, + val logger: MobileViewLogger, +) { + val icons: Map<Int, MobileIconViewModelKairos> by icons } @ExperimentalKairosApi -fun <T> BuildScope.toComposeState(state: KairosState<T>): ComposeState<T> { - val initial = state.sample() - val cState = mutableStateOf(initial) - state.changes.observe { cState.value = it } - return cState +fun MobileIconsViewModelKairos.composeWrapper(): BuildSpec<MobileIconsViewModelKairosComposeWrapper> = buildSpec { + MobileIconsViewModelKairosComposeWrapper( + icons = toComposeState(icons), + logger = logger, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt index a052008d4832..cba06e3caa58 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt @@ -33,17 +33,20 @@ class FakeMobileMappingsProxy : MobileMappingsProxy { fun setIconMap(map: Map<String, MobileIconGroup>) { iconMap = map } + override fun mapIconSets(config: Config): Map<String, MobileIconGroup> { if (useRealImpl) { return realImpl.mapIconSets(config) } return iconMap } + fun getIconMap() = iconMap fun setDefaultIcons(group: MobileIconGroup) { defaultIcons = group } + override fun getDefaultIcons(config: Config): MobileIconGroup { if (useRealImpl) { return realImpl.getDefaultIcons(config) @@ -72,3 +75,6 @@ class FakeMobileMappingsProxy : MobileMappingsProxy { return toIconKey(networkType) + "_override" } } + +val MobileMappingsProxy.fake + get() = this as FakeMobileMappingsProxy diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt index 4c6374b75f82..efab21fd9364 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt @@ -37,7 +37,7 @@ protected constructor( protected open val users: List<UserRecord> get() = controller.users.filter { (!controller.isKeyguardShowing || !it.isRestricted) && - (controller.isUserSwitcherEnabled || it.isCurrent) + (controller.isUserSwitcherEnabled || it.isCurrent || it.isSignOut) } init { @@ -109,6 +109,7 @@ protected constructor( item.isAddUser, item.isGuest, item.isAddSupervisedUser, + item.isSignOut, isTablet, item.isManageUsers, ) diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt index 1cc7a3185a5d..5541c50fb650 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyLogger.kt @@ -50,7 +50,7 @@ class DisplaySwitchLatencyLogger { onScreenTurningOnToOnDrawnMs, onDrawnToOnScreenTurnedOnMs, trackingResult, - screenWakelockstatus + screenWakelockStatus, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt index 5800d5ed41c6..336e8d172ad4 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt @@ -36,11 +36,11 @@ import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakefulnessModel import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.shared.system.SysUiStatsLog -import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent import com.android.systemui.unfold.DisplaySwitchLatencyTracker.TrackingResult.CORRUPTED import com.android.systemui.unfold.DisplaySwitchLatencyTracker.TrackingResult.SUCCESS import com.android.systemui.unfold.DisplaySwitchLatencyTracker.TrackingResult.TIMED_OUT import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg +import com.android.systemui.unfold.data.repository.ScreenTimeoutPolicyRepository import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor import com.android.systemui.util.Compile @@ -80,6 +80,7 @@ constructor( private val context: Context, private val deviceStateRepository: DeviceStateRepository, private val powerInteractor: PowerInteractor, + private val screenTimeoutPolicyRepository: ScreenTimeoutPolicyRepository, private val unfoldTransitionInteractor: UnfoldTransitionInteractor, private val animationStatusRepository: AnimationStatusRepository, private val keyguardInteractor: KeyguardInteractor, @@ -287,7 +288,18 @@ constructor( log { "fromFoldableDeviceState=$fromFoldableDeviceState" } instantForTrack(TAG) { "fromFoldableDeviceState=$fromFoldableDeviceState" } - return copy(fromFoldableDeviceState = fromFoldableDeviceState) + val screenTimeoutActive = screenTimeoutPolicyRepository.screenTimeoutActive.value + val screenWakelockStatus = + if (screenTimeoutActive) { + NO_SCREEN_WAKELOCKS + } else { + HAS_SCREEN_WAKELOCKS + } + + return copy( + fromFoldableDeviceState = fromFoldableDeviceState, + screenWakelockStatus = screenWakelockStatus + ) } private fun DisplaySwitchLatencyEvent.withAfterFields( @@ -344,7 +356,7 @@ constructor( val onDrawnToOnScreenTurnedOnMs: Int = VALUE_UNKNOWN, val trackingResult: Int = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TRACKING_RESULT__UNKNOWN_RESULT, - val screenWakelockstatus: Int = + val screenWakelockStatus: Int = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_UNKNOWN, ) @@ -372,5 +384,10 @@ constructor( SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_OPENED private const val FOLDABLE_DEVICE_STATE_FLIPPED = SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_FLIPPED + + private const val HAS_SCREEN_WAKELOCKS = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_HAS_SCREEN_WAKELOCKS + private const val NO_SCREEN_WAKELOCKS = + SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__SCREEN_WAKELOCK_STATUS__SCREEN_WAKELOCK_STATUS_NO_WAKELOCKS } } diff --git a/packages/SystemUI/src/com/android/systemui/unfold/data/repository/ScreenTimeoutPolicyRepository.kt b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/ScreenTimeoutPolicyRepository.kt new file mode 100644 index 000000000000..797939447464 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/ScreenTimeoutPolicyRepository.kt @@ -0,0 +1,61 @@ +/* + * 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.unfold.data.repository + +import android.os.PowerManager +import android.view.Display +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn + +/** Repository to get screen timeout updates */ +@SysUISingleton +class ScreenTimeoutPolicyRepository +@Inject +constructor( + private val powerManager: PowerManager, + @Background private val executor: Executor, + @Background private val scope: CoroutineScope, +) { + + /** Stores true if there is an active screen timeout */ + val screenTimeoutActive: StateFlow<Boolean> = + conflatedCallbackFlow { + val listener = + PowerManager.ScreenTimeoutPolicyListener { screenTimeoutPolicy -> + trySend(screenTimeoutPolicy == PowerManager.SCREEN_TIMEOUT_ACTIVE) + } + powerManager.addScreenTimeoutPolicyListener( + Display.DEFAULT_DISPLAY, + executor, + listener, + ) + awaitClose { + powerManager.removeScreenTimeoutPolicyListener( + Display.DEFAULT_DISPLAY, + listener, + ) + } + } + .stateIn(scope, started = SharingStarted.Eagerly, initialValue = true) +} diff --git a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt index d4fb5634bd1d..e16a51a6f6fa 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt @@ -42,6 +42,8 @@ data class UserRecord( @JvmField val isSwitchToEnabled: Boolean = false, /** Whether this record represents an option to add another supervised user to the device. */ @JvmField val isAddSupervisedUser: Boolean = false, + /** Whether this record represents an option to sign out of the current user. */ + @JvmField val isSignOut: Boolean = false, /** * An enforcing admin, if the user action represented by this record is disabled by the admin. * If not disabled, this is `null`. @@ -49,7 +51,7 @@ data class UserRecord( @JvmField val enforcedAdmin: RestrictedLockUtils.EnforcedAdmin? = null, /** Whether this record is to go to the Settings page to manage users. */ - @JvmField val isManageUsers: Boolean = false + @JvmField val isManageUsers: Boolean = false, ) { /** Returns a new instance of [UserRecord] with its [isCurrent] set to the given value. */ fun copyWithIsCurrent(isCurrent: Boolean): UserRecord { diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt index 163288b25b28..b82aefc1ac1c 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt @@ -38,6 +38,7 @@ import com.android.internal.util.UserIcons import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.Flags.switchUserOnBg +import com.android.systemui.Flags.userSwitcherAddSignOutOption import com.android.systemui.SystemUISecondaryUserService import com.android.systemui.animation.Expandable import com.android.systemui.broadcast.BroadcastDispatcher @@ -110,6 +111,7 @@ constructor( private val uiEventLogger: UiEventLogger, private val userRestrictionChecker: UserRestrictionChecker, private val processWrapper: ProcessWrapper, + private val userLogoutInteractor: UserLogoutInteractor, ) { /** * Defines interface for classes that can be notified when the state of users on the device is @@ -242,6 +244,12 @@ constructor( ) { add(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) } + if ( + userSwitcherAddSignOutOption() && + userLogoutInteractor.isLogoutEnabled.value + ) { + add(UserActionModel.SIGN_OUT) + } } } .flowOn(backgroundDispatcher) @@ -261,7 +269,8 @@ constructor( action = it, selectedUserId = selectedUserInfo.id, isRestricted = - it != UserActionModel.ENTER_GUEST_MODE && + it != UserActionModel.SIGN_OUT && + it != UserActionModel.ENTER_GUEST_MODE && it != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT && !settings.isAddUsersFromLockscreen, ) @@ -499,6 +508,10 @@ constructor( Intent(Settings.ACTION_USER_SETTINGS), /* dismissShade= */ true, ) + UserActionModel.SIGN_OUT -> { + dismissDialog() + applicationScope.launch { userLogoutInteractor.logOut() } + } } } @@ -583,9 +596,10 @@ constructor( actionType = action, isRestricted = isRestricted, isSwitchToEnabled = - canSwitchUsers(selectedUserId = selectedUserId, isAction = true) && - // If the user is auto-created is must not be currently resetting. - !(isGuestUserAutoCreated && isGuestUserResetting), + action == UserActionModel.SIGN_OUT || + (canSwitchUsers(selectedUserId = selectedUserId, isAction = true) && + // If the user is auto-created is must not be currently resetting. + !(isGuestUserAutoCreated && isGuestUserResetting)), userRestrictionChecker = userRestrictionChecker, ) } diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt index 80139bd6ac0c..23ca4ceda2de 100644 --- a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt @@ -74,6 +74,7 @@ object LegacyUserDataHelper { isGuest = actionType == UserActionModel.ENTER_GUEST_MODE, isAddUser = actionType == UserActionModel.ADD_USER, isAddSupervisedUser = actionType == UserActionModel.ADD_SUPERVISED_USER, + isSignOut = actionType == UserActionModel.SIGN_OUT, isRestricted = isRestricted, isSwitchToEnabled = isSwitchToEnabled, enforcedAdmin = @@ -94,6 +95,7 @@ object LegacyUserDataHelper { record.isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER record.isGuest -> UserActionModel.ENTER_GUEST_MODE record.isManageUsers -> UserActionModel.NAVIGATE_TO_USER_MANAGEMENT + record.isSignOut -> UserActionModel.SIGN_OUT else -> error("Not a known action: $record") } } @@ -105,15 +107,14 @@ object LegacyUserDataHelper { private fun getEnforcedAdmin( context: Context, selectedUserId: Int, - userRestrictionChecker: UserRestrictionChecker + userRestrictionChecker: UserRestrictionChecker, ): EnforcedAdmin? { val admin = userRestrictionChecker.checkIfRestrictionEnforced( context, UserManager.DISALLOW_ADD_USER, selectedUserId, - ) - ?: return null + ) ?: return null return if ( !userRestrictionChecker.hasBaseUserRestriction( @@ -145,11 +146,6 @@ object LegacyUserDataHelper { val unscaledOrNull = manager.getUserIcon(userInfo.id) ?: return null val avatarSize = context.resources.getDimensionPixelSize(R.dimen.max_avatar_size) - return Bitmap.createScaledBitmap( - unscaledOrNull, - avatarSize, - avatarSize, - /* filter= */ true, - ) + return Bitmap.createScaledBitmap(unscaledOrNull, avatarSize, avatarSize, /* filter= */ true) } } diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt index 09cef1ed64fc..e7a3c23e9119 100644 --- a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/ui/LegacyUserUiHelper.kt @@ -41,6 +41,7 @@ object LegacyUserUiHelper { isAddUser: Boolean, isGuest: Boolean, isAddSupervisedUser: Boolean, + isSignOut: Boolean, isTablet: Boolean = false, isManageUsers: Boolean, ): Int { @@ -52,6 +53,8 @@ object LegacyUserUiHelper { com.android.settingslib.R.drawable.ic_account_circle } else if (isAddSupervisedUser) { com.android.settingslib.R.drawable.ic_add_supervised_user + } else if (isSignOut) { + com.android.internal.R.drawable.ic_logout } else if (isManageUsers) { R.drawable.ic_manage_users } else { @@ -81,6 +84,7 @@ object LegacyUserUiHelper { isGuestUserResetting = isGuestUserResetting, isAddUser = record.isAddUser, isAddSupervisedUser = record.isAddSupervisedUser, + isSignOut = record.isSignOut, isTablet = isTablet, isManageUsers = record.isManageUsers, ) @@ -111,10 +115,11 @@ object LegacyUserUiHelper { isGuestUserResetting: Boolean, isAddUser: Boolean, isAddSupervisedUser: Boolean, + isSignOut: Boolean, isTablet: Boolean = false, isManageUsers: Boolean, ): Int { - check(isGuest || isAddUser || isAddSupervisedUser || isManageUsers) + check(isGuest || isAddUser || isAddSupervisedUser || isManageUsers || isSignOut) return when { isGuest && isGuestUserAutoCreated && isGuestUserResetting -> @@ -124,6 +129,7 @@ object LegacyUserUiHelper { isGuest -> com.android.internal.R.string.guest_name isAddUser -> com.android.settingslib.R.string.user_add_user isAddSupervisedUser -> R.string.add_user_supervised + isSignOut -> com.android.internal.R.string.global_action_logout isManageUsers -> R.string.manage_users else -> error("This should never happen!") } diff --git a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserActionModel.kt b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserActionModel.kt index 823bf74dc0f0..7f67d7691bf5 100644 --- a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserActionModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserActionModel.kt @@ -22,4 +22,5 @@ enum class UserActionModel { ADD_USER, ADD_SUPERVISED_USER, NAVIGATE_TO_USER_MANAGEMENT, + SIGN_OUT, } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt index 4089889f4b1e..2e3af1c7ad00 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt @@ -129,9 +129,7 @@ constructor( cancelButtonClicked || executedActionFinish || userSwitched } - private fun toViewModel( - model: UserModel, - ): UserViewModel { + private fun toViewModel(model: UserModel): UserViewModel { return UserViewModel( viewKey = model.id, name = @@ -152,14 +150,13 @@ constructor( ) } - private fun toViewModel( - model: UserActionModel, - ): UserActionViewModel { + private fun toViewModel(model: UserActionModel): UserActionViewModel { return UserActionViewModel( viewKey = model.ordinal.toLong(), iconResourceId = LegacyUserUiHelper.getUserSwitcherActionIconResourceId( isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER, + isSignOut = model == UserActionModel.SIGN_OUT, isAddUser = model == UserActionModel.ADD_USER, isGuest = model == UserActionModel.ENTER_GUEST_MODE, isManageUsers = model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, @@ -171,6 +168,7 @@ constructor( isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated, isGuestUserResetting = guestUserInteractor.isGuestUserResetting, isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER, + isSignOut = model == UserActionModel.SIGN_OUT, isAddUser = model == UserActionModel.ADD_USER, isManageUsers = model == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, isTablet = true, diff --git a/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ActivatedKairosSpec.kt b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ActivatedKairosSpec.kt new file mode 100644 index 000000000000..a108b1081cd2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ActivatedKairosSpec.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.composable.kairos + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.android.systemui.kairos.BuildSpec +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.KairosNetwork +import com.android.systemui.kairos.awaitClose +import com.android.systemui.kairos.launchEffect + +/** + * Activates the Kairos [buildSpec] within [kairosNetwork], bound to the current composition. + * + * [block] will be invoked with the result of activating the [buildSpec]. [buildSpec] will be + * deactivated automatically when [ActivatedKairosSpec] leaves the composition. + */ +@ExperimentalKairosApi +@Composable +fun <T> ActivatedKairosSpec( + buildSpec: BuildSpec<T>, + kairosNetwork: KairosNetwork, + block: @Composable (T) -> Unit, +) { + val uninit = Any() + var state by remember { mutableStateOf<Any?>(uninit) } + LaunchedEffect(key1 = Unit) { + kairosNetwork.activateSpec { + val v = buildSpec.applySpec() + launchEffect { + state = v + awaitClose { state = uninit } + } + } + } + state.let { + if (it !== uninit) { + @Suppress("UNCHECKED_CAST") block(it as T) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/composable/kairos/RememberKairosActivatable.kt b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/RememberKairosActivatable.kt new file mode 100644 index 000000000000..03a60bc002e8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/RememberKairosActivatable.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.util.composable.kairos + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import com.android.systemui.KairosActivatable +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.KairosNetwork + +@ExperimentalKairosApi +@Composable +fun <T : KairosActivatable> rememberKairosActivatable( + kairosNetwork: KairosNetwork, + key: Any = Unit, + factory: () -> T, +): T { + val instance = remember(key, factory) { factory() } + LaunchedEffect(instance, kairosNetwork) { + kairosNetwork.activateSpec { instance.run { activate() } } + } + return instance +} diff --git a/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ToComposeState.kt b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ToComposeState.kt new file mode 100644 index 000000000000..b3accb66c179 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ToComposeState.kt @@ -0,0 +1,36 @@ +/* + * 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.util.composable.kairos + +import androidx.compose.runtime.State as ComposeState +import androidx.compose.runtime.mutableStateOf +import com.android.systemui.kairos.BuildScope +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.State as KairosState +import com.android.systemui.kairos.changes + +/** + * Returns a [ComposeState] that is kept hydrated with the current value of [state] within this + * [BuildScope]. + */ +@ExperimentalKairosApi +fun <T> BuildScope.toComposeState(state: KairosState<T>): ComposeState<T> { + val initial = state.sample() + val cState = mutableStateOf(initial) + state.changes.observe { cState.value = it } + return cState +} diff --git a/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt index f1dd374d3e1d..1ae8bc9405dc 100644 --- a/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt @@ -16,13 +16,12 @@ package com.android.systemui.window.data.repository -import com.android.systemui.window.data.repository.WindowRootViewBlurRepository import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow class NoopWindowRootViewBlurRepository @Inject constructor() : WindowRootViewBlurRepository { - override val blurRadius: MutableStateFlow<Int> = MutableStateFlow(0) + override val blurRequestedByShade: MutableStateFlow<Int> = MutableStateFlow(0) override val isBlurOpaque: MutableStateFlow<Boolean> = MutableStateFlow(true) override val isBlurSupported: StateFlow<Boolean> = MutableStateFlow(false) override var blurAppliedListener: BlurAppliedListener? = null diff --git a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt index f98efe1e3c33..9b934bc6c866 100644 --- a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt @@ -39,7 +39,7 @@ typealias BlurAppliedListener = Consumer<Int> /** Repository that maintains state for the window blur effect. */ interface WindowRootViewBlurRepository { - val blurRadius: MutableStateFlow<Int> + val blurRequestedByShade: MutableStateFlow<Int> val isBlurOpaque: MutableStateFlow<Boolean> /** Is blur supported based on settings toggle and battery power saver mode. */ @@ -67,7 +67,7 @@ constructor( @Main private val executor: Executor, @Application private val scope: CoroutineScope, ) : WindowRootViewBlurRepository { - override val blurRadius = MutableStateFlow(0) + override val blurRequestedByShade = MutableStateFlow(0) override val isBlurOpaque = MutableStateFlow(false) diff --git a/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt index 5197242e8079..2994138f8181 100644 --- a/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt @@ -87,7 +87,7 @@ constructor( val isBlurCurrentlySupported: StateFlow<Boolean> = repository.isBlurSupported /** Radius of blur to be applied on the window root view. */ - val blurRadius: StateFlow<Int> = repository.blurRadius.asStateFlow() + val blurRadiusRequestedByShade: StateFlow<Int> = repository.blurRequestedByShade.asStateFlow() /** Whether the blur applied is opaque or transparent. */ val isBlurOpaque: Flow<Boolean> = @@ -128,7 +128,7 @@ constructor( return false } Log.d(TAG, "requestingBlurForShade for $blurRadius $opaque") - repository.blurRadius.value = blurRadius + repository.blurRequestedByShade.value = blurRadius repository.isBlurOpaque.value = opaque return true } diff --git a/packages/SystemUI/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModel.kt index e4b3ec7c40a3..89101c961031 100644 --- a/packages/SystemUI/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModel.kt @@ -62,7 +62,9 @@ constructor( listOf( *bouncerBlurRadiusFlows.toTypedArray(), *glanceableHubBlurRadiusFlows.toTypedArray(), - blurInteractor.blurRadius.map { it.toFloat() }.logIfPossible("ShadeBlur"), + blurInteractor.blurRadiusRequestedByShade + .map { it.toFloat() } + .logIfPossible("ShadeBlur"), ) .merge() diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt index 60345a358bac..6cc8238f2d09 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt @@ -249,10 +249,12 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { var factory = controllerFactory(controller) underTest.register(factory.cookie, factory, testScope) assertEquals(2, testShellTransitions.remotes.size) + assertTrue(testShellTransitions.remotesForTakeover.isEmpty()) factory = controllerFactory(controller) underTest.register(factory.cookie, factory, testScope) assertEquals(4, testShellTransitions.remotes.size) + assertTrue(testShellTransitions.remotesForTakeover.isEmpty()) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 206654abcaaa..9b314f25e02b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -1500,6 +1500,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mKosmos.getKeyguardInteractor(), mKeyguardTransitionBootInteractor, mKosmos::getCommunalSceneInteractor, + mKosmos::getCommunalSettingsInteractor, mock(WindowManagerOcclusionManager.class)); mViewMediator.mUserChangedCallback = mUserTrackerCallback; mViewMediator.start(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt index 86f7966d4ada..d6b778fe2bc2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt @@ -35,8 +35,14 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.activityTransitionAnimator import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.classifier.falsingCollector +import com.android.systemui.common.data.repository.batteryRepository +import com.android.systemui.common.data.repository.fake +import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN +import com.android.systemui.communal.data.model.SuppressionReason import com.android.systemui.communal.data.repository.communalSceneRepository import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.communalSettingsInteractor +import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.ui.viewmodel.communalTransitionViewModel import com.android.systemui.concurrency.fakeExecutor @@ -81,8 +87,11 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.verify /** Kotlin version of KeyguardViewMediatorTest to allow for coroutine testing. */ @SmallTest @@ -152,6 +161,7 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() { keyguardInteractor, keyguardTransitionBootInteractor, { communalSceneInteractor }, + { communalSettingsInteractor }, mock<WindowManagerOcclusionManager>(), ) } @@ -164,6 +174,10 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() { @Test fun doKeyguardTimeout_changesCommunalScene() = kosmos.runTest { + // Hub is enabled and hub condition is active. + setCommunalV2Enabled(true) + enableHubOnCharging() + // doKeyguardTimeout message received. val timeoutOptions = Bundle() timeoutOptions.putBoolean(KeyguardViewMediator.EXTRA_TRIGGER_HUB, true) @@ -174,4 +188,56 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() { assertThat(communalSceneRepository.currentScene.value) .isEqualTo(CommunalScenes.Communal) } + + @Test + fun doKeyguardTimeout_communalNotAvailable_sleeps() = + kosmos.runTest { + // Hub disabled. + setCommunalV2Enabled(false) + + // doKeyguardTimeout message received. + val timeoutOptions = Bundle() + timeoutOptions.putBoolean(KeyguardViewMediator.EXTRA_TRIGGER_HUB, true) + underTest.doKeyguardTimeout(timeoutOptions) + testableLooper.processAllMessages() + + // Sleep is requested. + verify(powerManager) + .goToSleep(anyOrNull(), eq(PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON), eq(0)) + + // Hub scene is not changed. + assertThat(communalSceneRepository.currentScene.value).isEqualTo(CommunalScenes.Blank) + } + + @Test + fun doKeyguardTimeout_hubConditionNotActive_sleeps() = + kosmos.runTest { + // Communal enabled, but hub condition set to never. + setCommunalV2Enabled(true) + disableHubShowingAutomatically() + + // doKeyguardTimeout message received. + val timeoutOptions = Bundle() + timeoutOptions.putBoolean(KeyguardViewMediator.EXTRA_TRIGGER_HUB, true) + underTest.doKeyguardTimeout(timeoutOptions) + testableLooper.processAllMessages() + + // Sleep is requested. + verify(powerManager) + .goToSleep(anyOrNull(), eq(PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON), eq(0)) + + // Hub scene is not changed. + assertThat(communalSceneRepository.currentScene.value).isEqualTo(CommunalScenes.Blank) + } + + private fun Kosmos.enableHubOnCharging() { + communalSettingsInteractor.setSuppressionReasons(emptyList()) + batteryRepository.fake.setDevicePluggedIn(true) + } + + private fun Kosmos.disableHubShowingAutomatically() { + communalSettingsInteractor.setSuppressionReasons( + listOf(SuppressionReason.ReasonUnknown(FEATURE_AUTO_OPEN)) + ) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt index df24bff43c08..78a4fbecabe8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt @@ -31,11 +31,13 @@ import com.android.systemui.flags.Flags import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.shade.NotificationPanelView import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow @@ -68,6 +70,7 @@ class DefaultDeviceEntrySectionTest : SysuiTestCase() { underTest = DefaultDeviceEntrySection( TestScope().backgroundScope, + testKosmos().testDispatcher, authController, windowManager, context, diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt index 8a4f1ad13b78..5596cc7ee7da 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt @@ -38,8 +38,6 @@ import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON import com.android.systemui.settings.FakeUserTracker import com.android.systemui.statusbar.CommandQueue import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.withArgCaptor @@ -52,11 +50,14 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.anyList import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.MockitoAnnotations.initMocks +import org.mockito.kotlin.any +import org.mockito.kotlin.eq /** atest SystemUITests:NoteTaskInitializerTest */ @OptIn(InternalNoteTaskApi::class) @@ -180,6 +181,18 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { @Test @EnableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER) + fun initialize_keyGestureTypeOpenNotes_isRegistered() { + val underTest = createUnderTest(isEnabled = true, bubbles = bubbles) + underTest.initialize() + verify(inputManager) + .registerKeyGestureEventHandler( + eq(listOf(KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES)), + any(), + ) + } + + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER) fun handlesShortcut_keyGestureTypeOpenNotes() { val gestureEvent = KeyGestureEvent.Builder() @@ -189,12 +202,12 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { val underTest = createUnderTest(isEnabled = true, bubbles = bubbles) underTest.initialize() val callback = withArgCaptor { - verify(inputManager).registerKeyGestureEventHandler(capture()) + verify(inputManager).registerKeyGestureEventHandler(anyList(), capture()) } - assertThat(callback.handleKeyGestureEvent(gestureEvent, null)).isTrue() - + callback.handleKeyGestureEvent(gestureEvent, null) executor.runAllReady() + verify(controller).showNoteTask(eq(KEYBOARD_SHORTCUT)) } @@ -203,19 +216,19 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { fun handlesShortcut_stylusTailButton() { val gestureEvent = KeyGestureEvent.Builder() - .setKeycodes(intArrayOf(KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL)) + .setKeycodes(intArrayOf(KEYCODE_STYLUS_BUTTON_TAIL)) .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES) .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE) .build() val underTest = createUnderTest(isEnabled = true, bubbles = bubbles) underTest.initialize() val callback = withArgCaptor { - verify(inputManager).registerKeyGestureEventHandler(capture()) + verify(inputManager).registerKeyGestureEventHandler(anyList(), capture()) } - assertThat(callback.handleKeyGestureEvent(gestureEvent, null)).isTrue() - + callback.handleKeyGestureEvent(gestureEvent, null) executor.runAllReady() + verify(controller).showNoteTask(eq(TAIL_BUTTON)) } @@ -224,19 +237,19 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { fun ignoresUnrelatedShortcuts() { val gestureEvent = KeyGestureEvent.Builder() - .setKeycodes(intArrayOf(KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL)) + .setKeycodes(intArrayOf(KEYCODE_STYLUS_BUTTON_TAIL)) .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE) .build() val underTest = createUnderTest(isEnabled = true, bubbles = bubbles) underTest.initialize() val callback = withArgCaptor { - verify(inputManager).registerKeyGestureEventHandler(capture()) + verify(inputManager).registerKeyGestureEventHandler(anyList(), capture()) } - assertThat(callback.handleKeyGestureEvent(gestureEvent, null)).isFalse() - + callback.handleKeyGestureEvent(gestureEvent, null) executor.runAllReady() + verify(controller, never()).showNoteTask(any()) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt index 7e42ec7e83b1..1551375f6879 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt @@ -37,6 +37,7 @@ import com.android.systemui.common.shared.model.Icon import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.shared.model.SizedTileImpl import com.android.systemui.qs.panels.ui.compose.infinitegrid.DefaultEditTileGrid +import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.shared.model.TileCategory @@ -63,7 +64,7 @@ class DragAndDropTest : SysuiTestCase() { columns = 4, largeTilesSpan = 4, modifier = Modifier.fillMaxSize(), - onAddTile = {}, + onAddTile = { _, _ -> }, onRemoveTile = {}, onSetTiles = onSetTiles, onResize = { _, _ -> }, @@ -84,7 +85,7 @@ class DragAndDropTest : SysuiTestCase() { } composeRule.waitForIdle() - listState.onStarted(TestEditTiles[0], DragType.Add) + listState.onStarted(TestEditTiles[0], DragType.Move) // Tile is being dragged, it should be replaced with a placeholder composeRule.onNodeWithContentDescription("tileA").assertDoesNotExist() @@ -103,6 +104,45 @@ class DragAndDropTest : SysuiTestCase() { } @Test + fun nonRemovableDraggedTile_removeHeaderShouldNotExist() { + val nonRemovableTile = createEditTile("tileA", isRemovable = false) + val listState = EditTileListState(listOf(nonRemovableTile), columns = 4, largeTilesSpan = 2) + composeRule.setContent { EditTileGridUnderTest(listState) {} } + composeRule.waitForIdle() + + listState.onStarted(nonRemovableTile, DragType.Move) + + // Tile is being dragged, it should be replaced with a placeholder + composeRule.onNodeWithContentDescription("tileA").assertDoesNotExist() + + // Remove drop zone should not appear + composeRule.onNodeWithText("Remove").assertDoesNotExist() + } + + @Test + fun droppedNonRemovableDraggedTile_shouldStayInGrid() { + val nonRemovableTile = createEditTile("tileA", isRemovable = false) + val listState = EditTileListState(listOf(nonRemovableTile), columns = 4, largeTilesSpan = 2) + composeRule.setContent { EditTileGridUnderTest(listState) {} } + composeRule.waitForIdle() + + listState.onStarted(nonRemovableTile, DragType.Move) + + // Tile is being dragged, it should be replaced with a placeholder + composeRule.onNodeWithContentDescription("tileA").assertDoesNotExist() + + // Remove drop zone should not appear + composeRule.onNodeWithText("Remove").assertDoesNotExist() + + // Drop tile outside of the grid + listState.movedOutOfBounds() + listState.onDrop() + + // Tile A is still in the grid + composeRule.assertGridContainsExactly(CURRENT_TILES_GRID_TEST_TAG, listOf("tileA")) + } + + @Test fun draggedTile_shouldChangePosition() { var tiles by mutableStateOf(TestEditTiles) val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2) @@ -113,7 +153,11 @@ class DragAndDropTest : SysuiTestCase() { } composeRule.waitForIdle() - listState.onStarted(TestEditTiles[0], DragType.Add) + listState.onStarted(TestEditTiles[0], DragType.Move) + + // Remove drop zone should appear + composeRule.onNodeWithText("Remove").assertExists() + listState.onTargeting(1, false) listState.onDrop() @@ -141,7 +185,11 @@ class DragAndDropTest : SysuiTestCase() { } composeRule.waitForIdle() - listState.onStarted(TestEditTiles[0], DragType.Add) + listState.onStarted(TestEditTiles[0], DragType.Move) + + // Remove drop zone should appear + composeRule.onNodeWithText("Remove").assertExists() + listState.movedOutOfBounds() listState.onDrop() @@ -169,7 +217,11 @@ class DragAndDropTest : SysuiTestCase() { } composeRule.waitForIdle() - listState.onStarted(createEditTile("tile_new"), DragType.Add) + listState.onStarted(createEditTile("tile_new", isRemovable = false), DragType.Add) + + // Remove drop zone should appear + composeRule.onNodeWithText("Remove").assertExists() + // Insert after tileD, which is at index 4 // [ a ] [ b ] [ c ] [ empty ] // [ tile d ] [ e ] @@ -193,7 +245,10 @@ class DragAndDropTest : SysuiTestCase() { private const val CURRENT_TILES_GRID_TEST_TAG = "CurrentTilesGrid" private const val AVAILABLE_TILES_GRID_TEST_TAG = "AvailableTilesGrid" - private fun createEditTile(tileSpec: String): SizedTile<EditTileViewModel> { + private fun createEditTile( + tileSpec: String, + isRemovable: Boolean = true, + ): SizedTile<EditTileViewModel> { return SizedTileImpl( EditTileViewModel( tileSpec = TileSpec.create(tileSpec), @@ -205,7 +260,8 @@ class DragAndDropTest : SysuiTestCase() { label = AnnotatedString(tileSpec), appName = null, isCurrent = true, - availableEditActions = emptySet(), + availableEditActions = + if (isRemovable) setOf(AvailableEditActions.REMOVE) else emptySet(), category = TileCategory.UNKNOWN, ), getWidth(tileSpec), diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt index 9d4a425c678b..acb441c3765d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.test.doubleClick import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onAllNodesWithText @@ -30,6 +31,7 @@ import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.text.AnnotatedString import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -40,6 +42,7 @@ import com.android.systemui.common.shared.model.Icon import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.shared.model.SizedTileImpl import com.android.systemui.qs.panels.ui.compose.infinitegrid.DefaultEditTileGrid +import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.shared.model.TileCategory @@ -53,8 +56,8 @@ class EditModeTest : SysuiTestCase() { @get:Rule val composeRule = createComposeRule() @Composable - private fun EditTileGridUnderTest() { - var tiles by remember { mutableStateOf(TestEditTiles) } + private fun EditTileGridUnderTest(sizedTiles: List<SizedTile<EditTileViewModel>>) { + var tiles by remember { mutableStateOf(sizedTiles) } val (currentTiles, otherTiles) = tiles.partition { it.tile.isCurrent } val listState = EditTileListState(currentTiles, columns = 4, largeTilesSpan = 2) @@ -65,7 +68,7 @@ class EditModeTest : SysuiTestCase() { columns = 4, largeTilesSpan = 4, modifier = Modifier.fillMaxSize(), - onAddTile = { tiles = tiles.add(it) }, + onAddTile = { spec, _ -> tiles = tiles.add(spec) }, onRemoveTile = { tiles = tiles.remove(it) }, onSetTiles = {}, onResize = { _, _ -> }, @@ -77,7 +80,7 @@ class EditModeTest : SysuiTestCase() { @Test fun clickAvailableTile_shouldAdd() { - composeRule.setContent { EditTileGridUnderTest() } + composeRule.setContent { EditTileGridUnderTest(TestEditTiles) } composeRule.waitForIdle() composeRule.onNodeWithContentDescription("tileF").performClick() // Tap to add @@ -93,7 +96,7 @@ class EditModeTest : SysuiTestCase() { @Test fun clickRemoveTarget_shouldRemoveSelection() { - composeRule.setContent { EditTileGridUnderTest() } + composeRule.setContent { EditTileGridUnderTest(TestEditTiles) } composeRule.waitForIdle() // Selects first "tileA", i.e. the one in the current grid @@ -110,6 +113,36 @@ class EditModeTest : SysuiTestCase() { ) } + @Test + fun selectNonRemovableTile_removeTargetShouldHide() { + val nonRemovableTile = createEditTile("tileA", isRemovable = false) + composeRule.setContent { EditTileGridUnderTest(listOf(nonRemovableTile)) } + composeRule.waitForIdle() + + // Selects first "tileA", i.e. the one in the current grid + composeRule.onAllNodesWithText("tileA").onFirst().performClick() + + // Assert the remove target isn't shown + composeRule.onNodeWithText("Remove").assertDoesNotExist() + } + + @Test + fun placementMode_shouldRepositionTile() { + composeRule.setContent { EditTileGridUnderTest(TestEditTiles) } + composeRule.waitForIdle() + + // Double tap first "tileA", i.e. the one in the current grid + composeRule.onAllNodesWithText("tileA").onFirst().performTouchInput { doubleClick() } + + // Tap on tileE to position tileA in its spot + composeRule.onAllNodesWithText("tileE").onFirst().performClick() + + // Assert tileA moved to tileE's position + composeRule.assertCurrentTilesGridContainsExactly( + listOf("tileB", "tileC", "tileD_large", "tileE", "tileA") + ) + } + private fun ComposeContentTestRule.assertCurrentTilesGridContainsExactly(specs: List<String>) = assertGridContainsExactly(CURRENT_TILES_GRID_TEST_TAG, specs) @@ -148,6 +181,7 @@ class EditModeTest : SysuiTestCase() { private fun createEditTile( tileSpec: String, isCurrent: Boolean = true, + isRemovable: Boolean = true, ): SizedTile<EditTileViewModel> { return SizedTileImpl( EditTileViewModel( @@ -160,7 +194,8 @@ class EditModeTest : SysuiTestCase() { label = AnnotatedString(tileSpec), appName = null, isCurrent = isCurrent, - availableEditActions = emptySet(), + availableEditActions = + if (isRemovable) setOf(AvailableEditActions.REMOVE) else emptySet(), category = TileCategory.UNKNOWN, ), getWidth(tileSpec), diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt index 5e76000cc7f0..274c44cef949 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt @@ -69,7 +69,7 @@ class ResizingTest : SysuiTestCase() { columns = 4, largeTilesSpan = 4, modifier = Modifier.fillMaxSize(), - onAddTile = {}, + onAddTile = { _, _ -> }, onRemoveTile = {}, onSetTiles = {}, onResize = onResize, diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt index 997cf417fe10..f4d0c26f12ee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt @@ -18,9 +18,11 @@ package com.android.systemui.reardisplay import android.hardware.devicestate.feature.flags.Flags.FLAG_DEVICE_STATE_RDM_V2 import android.hardware.display.rearDisplay +import android.os.fakeExecutorHandler import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.view.Display +import android.view.accessibility.accessibilityManager import androidx.test.filters.SmallTest import com.android.keyguard.keyguardUpdateMonitor import com.android.systemui.SysuiTestCase @@ -62,6 +64,8 @@ class RearDisplayCoreStartableTest : SysuiTestCase() { kosmos.rearDisplayInnerDialogDelegateFactory, kosmos.testScope, kosmos.keyguardUpdateMonitor, + kosmos.accessibilityManager, + kosmos.fakeExecutorHandler, ) @Before @@ -69,7 +73,7 @@ class RearDisplayCoreStartableTest : SysuiTestCase() { whenever(kosmos.rearDisplay.flags).thenReturn(Display.FLAG_REAR) whenever(kosmos.rearDisplay.displayAdjustments) .thenReturn(mContext.display.displayAdjustments) - whenever(kosmos.rearDisplayInnerDialogDelegateFactory.create(any(), any())) + whenever(kosmos.rearDisplayInnerDialogDelegateFactory.create(any(), any(), any())) .thenReturn(mockDelegate) whenever(mockDelegate.createDialog()).thenReturn(mockDialog) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt index fc7661666825..477e5babdcc3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt @@ -17,7 +17,10 @@ package com.android.systemui.reardisplay import android.testing.TestableLooper +import android.view.View +import android.widget.Button import android.widget.SeekBar +import android.widget.TextView import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.haptics.msdl.msdlPlayer @@ -28,6 +31,7 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.phone.systemUIDialogDotFactory import com.android.systemui.testKosmos import com.android.systemui.util.time.systemClock +import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import org.junit.Test @@ -49,6 +53,7 @@ class RearDisplayInnerDialogDelegateTest : SysuiTestCase() { RearDisplayInnerDialogDelegate( kosmos.systemUIDialogDotFactory, mContext, + false /* touchExplorationEnabled */, kosmos.vibratorHelper, kosmos.msdlPlayer, kosmos.systemClock, @@ -68,6 +73,7 @@ class RearDisplayInnerDialogDelegateTest : SysuiTestCase() { RearDisplayInnerDialogDelegate( kosmos.systemUIDialogDotFactory, mContext, + false /* touchExplorationEnabled */, kosmos.vibratorHelper, kosmos.msdlPlayer, kosmos.systemClock, @@ -78,6 +84,9 @@ class RearDisplayInnerDialogDelegateTest : SysuiTestCase() { .apply { show() val seekbar = findViewById<SeekBar>(R.id.seekbar) + assertThat(seekbar.visibility).isEqualTo(View.VISIBLE) + assertThat(findViewById<TextView>(R.id.seekbar_instructions).visibility) + .isEqualTo(View.VISIBLE) seekbar.progress = 50 seekbar.progress = 100 verify(mockCallback).run() @@ -90,6 +99,7 @@ class RearDisplayInnerDialogDelegateTest : SysuiTestCase() { RearDisplayInnerDialogDelegate( kosmos.systemUIDialogDotFactory, mContext, + false /* touchExplorationEnabled */, kosmos.vibratorHelper, kosmos.msdlPlayer, kosmos.systemClock, @@ -118,4 +128,33 @@ class RearDisplayInnerDialogDelegateTest : SysuiTestCase() { // Progress is reset verify(mockSeekbar).setProgress(eq(0)) } + + @Test + fun testTouchExplorationEnabled() { + val mockCallback = mock<Runnable>() + + RearDisplayInnerDialogDelegate( + kosmos.systemUIDialogDotFactory, + mContext, + true /* touchExplorationEnabled */, + kosmos.vibratorHelper, + kosmos.msdlPlayer, + kosmos.systemClock, + ) { + mockCallback.run() + } + .createDialog() + .apply { + show() + assertThat(findViewById<SeekBar>(R.id.seekbar).visibility).isEqualTo(View.GONE) + assertThat(findViewById<TextView>(R.id.seekbar_instructions).visibility) + .isEqualTo(View.GONE) + + val cancelButton = findViewById<Button>(R.id.cancel_button) + assertThat(cancelButton.visibility).isEqualTo(View.VISIBLE) + + cancelButton.performClick() + verify(mockCallback).run() + } + } } 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 2ea4e7f67b3c..bc7ab9d4fe3c 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 @@ -582,7 +582,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { public void testIconScrollXAfterTranslationAndReset() throws Exception { ExpandableNotificationRow group = mNotificationTestHelper.createGroup(); - group.setDismissUsingRowTranslationX(false); + group.setDismissUsingRowTranslationX(false, false); group.setTranslation(50); assertEquals(50, -group.getEntry().getIcons().getShelfIcon().getScrollX()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 0d99c0e8cab8..320a87e7db17 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -176,6 +176,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { private FakeKeyguardStateController mKeyguardStateController = spy(new FakeKeyguardStateController()); private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); + private final static String TEST_REASON = "reason"; @Mock private ViewRootImpl mViewRootImpl; @@ -272,14 +273,15 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mStatusBarKeyguardViewManager.dismissWithAction( action, cancelAction, false /* afterKeyguardGone */); verify(mPrimaryBouncerInteractor).setDismissAction(eq(action), eq(cancelAction)); - verify(mPrimaryBouncerInteractor).show(eq(true)); + verify(mPrimaryBouncerInteractor).show(eq(true), + eq("StatusBarKeyguardViewManager#dismissWithAction")); } @Test public void showPrimaryBouncer_onlyWhenShowing() { mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */); - mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */); - verify(mPrimaryBouncerInteractor, never()).show(anyBoolean()); + mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */, TEST_REASON); + verify(mPrimaryBouncerInteractor, never()).show(anyBoolean(), eq(TEST_REASON)); verify(mDeviceEntryInteractor, never()).attemptDeviceEntry(); verify(mSceneInteractor, never()).changeScene(any(), any()); } @@ -289,8 +291,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */); when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( KeyguardSecurityModel.SecurityMode.Password); - mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */); - verify(mPrimaryBouncerInteractor, never()).show(anyBoolean()); + mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */, TEST_REASON); + verify(mPrimaryBouncerInteractor, never()).show(anyBoolean(), eq(TEST_REASON)); verify(mDeviceEntryInteractor, never()).attemptDeviceEntry(); verify(mSceneInteractor, never()).changeScene(any(), any()); } @@ -298,8 +300,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Test @DisableSceneContainer public void showBouncer_showsTheBouncer() { - mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */); - verify(mPrimaryBouncerInteractor).show(eq(true)); + mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */, TEST_REASON); + verify(mPrimaryBouncerInteractor).show(eq(true), eq(TEST_REASON)); } @Test @@ -344,19 +346,20 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { public void onPanelExpansionChanged_showsBouncerWhenSwiping() { mKeyguardStateController.setCanDismissLockScreen(false); mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); - verify(mPrimaryBouncerInteractor).show(eq(false)); + verify(mPrimaryBouncerInteractor).show(eq(false), + eq("StatusBarKeyguardViewManager#onPanelExpansionChanged")); // But not when it's already visible reset(mPrimaryBouncerInteractor); when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true); mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); - verify(mPrimaryBouncerInteractor, never()).show(eq(false)); + verify(mPrimaryBouncerInteractor, never()).show(eq(false), eq(TEST_REASON)); // Or animating away reset(mPrimaryBouncerInteractor); when(mPrimaryBouncerInteractor.isAnimatingAway()).thenReturn(true); mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT); - verify(mPrimaryBouncerInteractor, never()).show(eq(false)); + verify(mPrimaryBouncerInteractor, never()).show(eq(false), eq(TEST_REASON)); } @Test @@ -546,7 +549,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); // WHEN showBouncer is called - mStatusBarKeyguardViewManager.showPrimaryBouncer(true); + mStatusBarKeyguardViewManager.showPrimaryBouncer(true, TEST_REASON); // THEN alt bouncer should be hidden verify(mAlternateBouncerInteractor).hide(); @@ -571,10 +574,10 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { // WHEN showGenericBouncer is called final boolean scrimmed = true; - mStatusBarKeyguardViewManager.showBouncer(scrimmed); + mStatusBarKeyguardViewManager.showBouncer(scrimmed, TEST_REASON); // THEN regular bouncer is shown - verify(mPrimaryBouncerInteractor).show(eq(scrimmed)); + verify(mPrimaryBouncerInteractor).show(eq(scrimmed), eq(TEST_REASON)); } @Test @@ -835,7 +838,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(false); // WHEN request to show primary bouncer - mStatusBarKeyguardViewManager.showPrimaryBouncer(true); + mStatusBarKeyguardViewManager.showPrimaryBouncer(true, TEST_REASON); // THEN the scrim isn't updated from StatusBarKeyguardViewManager verify(mCentralSurfaces, never()).updateScrimController(); @@ -847,9 +850,9 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { public void testShowBouncerOrKeyguard_needsFullScreen() { when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( KeyguardSecurityModel.SecurityMode.SimPin); - mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false); + mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false, TEST_REASON); verify(mCentralSurfaces).hideKeyguard(); - verify(mPrimaryBouncerInteractor).show(true); + verify(mPrimaryBouncerInteractor).show(true, TEST_REASON); } @Test @@ -859,7 +862,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( KeyguardSecurityModel.SecurityMode.SimPin); // Returning false means unable to show the bouncer - when(mPrimaryBouncerInteractor.show(true)).thenReturn(false); + when(mPrimaryBouncerInteractor.show(true, TEST_REASON)).thenReturn(false); when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo()) .thenReturn(KeyguardState.LOCKSCREEN); mStatusBarKeyguardViewManager.onStartedWakingUp(); @@ -868,8 +871,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { // Advance past reattempts mStatusBarKeyguardViewManager.setAttemptsToShowBouncer(10); - mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false); - verify(mPrimaryBouncerInteractor).show(true); + mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false, TEST_REASON); + verify(mPrimaryBouncerInteractor).show(true, TEST_REASON); verify(mCentralSurfaces).showKeyguard(); } @@ -884,7 +887,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { reset(mCentralSurfaces); reset(mPrimaryBouncerInteractor); mStatusBarKeyguardViewManager.showBouncerOrKeyguard( - /* hideBouncerWhenShowing= */true, false); + /* hideBouncerWhenShowing= */true, false, TEST_REASON); verify(mCentralSurfaces).showKeyguard(); verify(mPrimaryBouncerInteractor).hide(); } @@ -897,9 +900,9 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( KeyguardSecurityModel.SecurityMode.SimPin); when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true); - mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset); + mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset, TEST_REASON); verify(mCentralSurfaces, never()).hideKeyguard(); - verify(mPrimaryBouncerInteractor).show(true); + verify(mPrimaryBouncerInteractor).show(true, TEST_REASON); } @Test @@ -909,24 +912,24 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( KeyguardSecurityModel.SecurityMode.SimPin); when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true); - mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset); + mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset, TEST_REASON); verify(mCentralSurfaces, never()).hideKeyguard(); // Do not refresh the full screen bouncer if the call is from falsing - verify(mPrimaryBouncerInteractor, never()).show(true); + verify(mPrimaryBouncerInteractor, never()).show(true, TEST_REASON); } @Test @EnableSceneContainer public void showBouncer_attemptDeviceEntry() { - mStatusBarKeyguardViewManager.showBouncer(false); + mStatusBarKeyguardViewManager.showBouncer(false, TEST_REASON); verify(mDeviceEntryInteractor).attemptDeviceEntry(); } @Test @EnableSceneContainer public void showPrimaryBouncer() { - mStatusBarKeyguardViewManager.showPrimaryBouncer(false); + mStatusBarKeyguardViewManager.showPrimaryBouncer(false, TEST_REASON); verify(mSceneInteractor).showOverlay(eq(Overlays.Bouncer), anyString()); } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java index 846db6389d0c..2facc1c01ae1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java @@ -283,6 +283,9 @@ public abstract class SysuiTestCase { } public FakeBroadcastDispatcher getFakeBroadcastDispatcher() { + if (mSysuiDependency == null) { + return null; + } return mSysuiDependency.getFakeBroadcastDispatcher(); } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCaseExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCaseExt.kt index d3dccb021ff8..c86ba6ccf47f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCaseExt.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCaseExt.kt @@ -18,9 +18,22 @@ package com.android.systemui import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testCase +import com.android.systemui.kosmos.useStandardTestDispatcher fun SysuiTestCase.testKosmos(): Kosmos = Kosmos().apply { testCase = this@testKosmos } +/** + * This should not be called directly. Instead, you can use: + * - testKosmos() to use the default dispatcher (which will soon be unconfined, see go/thetiger) + * - testKosmos().useStandardTestDispatcher() to explicitly choose the standard dispatcher + * - testKosmos().useUnconfinedTestDispatcher() to explicitly choose the unconfined dispatcher + * + * For details, see go/thetiger + */ +@Deprecated("Do not call this directly. Use testKosmos() with dispatcher functions if needed.") +fun SysuiTestCase.testKosmosLegacy(): Kosmos = + Kosmos().useStandardTestDispatcher().apply { testCase = this@testKosmosLegacy } + /** Run [f] on the main thread and return its result once completed. */ fun <T : Any> SysuiTestCase.runOnMainThreadAndWaitForIdleSync(f: () -> T): T { lateinit var result: T diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt index 511bede7349b..41dddce77a30 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorKosmos.kt @@ -33,6 +33,7 @@ var Kosmos.fromLockscreenTransitionInteractor by transitionInteractor = keyguardTransitionInteractor, internalTransitionInteractor = internalKeyguardTransitionInteractor, scope = applicationCoroutineScope, + applicationScope = applicationCoroutineScope, bgDispatcher = testDispatcher, mainDispatcher = testDispatcher, keyguardInteractor = keyguardInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt index 7a9b052481cb..349e670a9af3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt @@ -46,7 +46,6 @@ import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInter import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.session.shared.shadeSessionStorage import com.android.systemui.scene.shared.logger.sceneLogger -import com.android.systemui.settings.displayTracker import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor @@ -65,7 +64,6 @@ val Kosmos.sceneContainerStartable by Fixture { bouncerInteractor = bouncerInteractor, keyguardInteractor = keyguardInteractor, sysUiState = sysUiState, - displayId = displayTracker.defaultDisplayId, sceneLogger = sceneLogger, falsingCollector = falsingCollector, falsingManager = falsingManager, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt index 34e5bfde43c9..9a9f3354a7ba 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt @@ -14,10 +14,14 @@ * limitations under the License. */ +@file:OptIn(ExperimentalKairosApi::class) + package com.android.systemui.shade.ui.viewmodel import android.content.applicationContext import com.android.systemui.battery.batteryMeterViewControllerFactory +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.kairos import com.android.systemui.kosmos.Kosmos import com.android.systemui.plugins.activityStarter import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -46,6 +50,8 @@ val Kosmos.shadeHeaderViewModel: ShadeHeaderViewModel by tintedIconManagerFactory = tintedIconManagerFactory, batteryMeterViewControllerFactory = batteryMeterViewControllerFactory, statusBarIconController = mock<StatusBarIconController>(), + kairosNetwork = kairos, + mobileIconsViewModelKairos = mock(), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt index 63085e178e7d..4af4e804ff10 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt @@ -19,7 +19,7 @@ package com.android.systemui.statusbar.notification.data.model import android.app.PendingIntent import android.graphics.drawable.Icon import com.android.systemui.statusbar.StatusBarIconView -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.notification.shared.CallType import com.android.systemui.statusbar.notification.stack.BUCKET_UNKNOWN @@ -29,6 +29,8 @@ fun activeNotificationModel( key: String, groupKey: String? = null, whenTime: Long = 0L, + isForegroundService: Boolean = false, + isOngoingEvent: Boolean = false, isAmbient: Boolean = false, isRowDismissed: Boolean = false, isSilent: Boolean = false, @@ -47,12 +49,14 @@ fun activeNotificationModel( contentIntent: PendingIntent? = null, bucket: Int = BUCKET_UNKNOWN, callType: CallType = CallType.None, - promotedContent: PromotedNotificationContentModel? = null, + promotedContent: PromotedNotificationContentModels? = null, ) = ActiveNotificationModel( key = key, groupKey = groupKey, whenTime = whenTime, + isForegroundService = isForegroundService, + isOngoingEvent = isOngoingEvent, isAmbient = isAmbient, isRowDismissed = isRowDismissed, isSilent = isSilent, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt index 99323dbd7cce..ebe20af51c19 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelKosmos.kt @@ -22,6 +22,7 @@ import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shared.notifications.domain.interactor.notificationSettingsInteractor import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor +import com.android.systemui.window.domain.interactor.windowRootViewBlurInteractor val Kosmos.footerViewModel by Fixture { FooterViewModel( @@ -29,6 +30,7 @@ val Kosmos.footerViewModel by Fixture { notificationSettingsInteractor = notificationSettingsInteractor, seenNotificationsInteractor = seenNotificationsInteractor, shadeInteractor = shadeInteractor, + windowRootViewBlurInteractor = windowRootViewBlurInteractor, ) } val Kosmos.footerViewModelFactory: FooterViewModel.Factory by Fixture { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt index 8fdf5dbf2aeb..aaa86aaaedc6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt @@ -17,21 +17,23 @@ package com.android.systemui.statusbar.notification.promoted import android.app.Notification +import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider import org.junit.Assert class FakePromotedNotificationContentExtractor : PromotedNotificationContentExtractor { @JvmField - val contentForEntry = mutableMapOf<NotificationEntry, PromotedNotificationContentModel?>() + val contentForEntry = mutableMapOf<NotificationEntry, PromotedNotificationContentModels?>() @JvmField val extractCalls = mutableListOf<Pair<NotificationEntry, Notification.Builder>>() override fun extractContent( entry: NotificationEntry, recoveredBuilder: Notification.Builder, + @RedactionType redactionType: Int, imageModelProvider: ImageModelProvider, - ): PromotedNotificationContentModel? { + ): PromotedNotificationContentModels? { extractCalls.add(entry to recoveredBuilder) if (contentForEntry.isEmpty()) { @@ -44,7 +46,7 @@ class FakePromotedNotificationContentExtractor : PromotedNotificationContentExtr } } - fun resetForEntry(entry: NotificationEntry, content: PromotedNotificationContentModel?) { + fun resetForEntry(entry: NotificationEntry, content: PromotedNotificationContentModels?) { contentForEntry.clear() contentForEntry[entry] = content extractCalls.clear() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt index 2b3158da38f9..c4542c4e709b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.promoted import android.app.Notification import android.content.applicationContext import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.row.RowImageInflater import com.android.systemui.statusbar.notification.row.shared.skeletonImageTransform @@ -39,9 +40,10 @@ fun Kosmos.setPromotedContent(entry: NotificationEntry) { promotedNotificationContentExtractor.extractContent( entry, Notification.Builder.recoverBuilder(applicationContext, entry.sbn.notification), + REDACTION_TYPE_NONE, RowImageInflater.newInstance(previousIndex = null, reinflating = false) .useForContentModel(), ) - entry.promotedNotificationContentModel = + entry.promotedNotificationContentModels = requireNotNull(extractedContent) { "extractContent returned null" } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt index 093ec10e2642..8b2c68aa04cd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt @@ -19,13 +19,17 @@ package com.android.systemui.statusbar.notification.promoted.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher import com.android.systemui.statusbar.chips.call.domain.interactor.callChipInteractor +import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor +import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.screenRecordChipInteractor import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor val Kosmos.promotedNotificationsInteractor by Kosmos.Fixture { PromotedNotificationsInteractor( activeNotificationsInteractor = activeNotificationsInteractor, + screenRecordChipInteractor = screenRecordChipInteractor, + mediaProjectionChipInteractor = mediaProjectionChipInteractor, callChipInteractor = callChipInteractor, notifChipsInteractor = statusBarNotificationChipsInteractor, backgroundDispatcher = testDispatcher, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentBuilder.kt new file mode 100644 index 000000000000..6916d560a7ad --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentBuilder.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.promoted.shared.model + +class PromotedNotificationContentBuilder(val key: String) { + private val sharedBuilder = PromotedNotificationContentModel.Builder(key) + + fun applyToShared( + block: PromotedNotificationContentModel.Builder.() -> Unit + ): PromotedNotificationContentBuilder { + sharedBuilder.apply(block) + return this + } + + fun build(): PromotedNotificationContentModels { + val sharedModel = sharedBuilder.build() + return PromotedNotificationContentModels(sharedModel, sharedModel) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/AppIconProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/AppIconProviderKosmos.kt index 0fd0f1469818..277fc62242b1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/AppIconProviderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/AppIconProviderKosmos.kt @@ -19,6 +19,9 @@ package com.android.systemui.statusbar.notification.row.icon import android.content.applicationContext import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos +import org.mockito.kotlin.mock + +val Kosmos.mockAppIconProvider by Kosmos.Fixture { mock<AppIconProvider>() } val Kosmos.appIconProvider by Kosmos.Fixture { AppIconProviderImpl(applicationContext, dumpManager) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProviderKosmos.kt index b4fb7dd9d760..86ff722f99be 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProviderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProviderKosmos.kt @@ -19,6 +19,10 @@ package com.android.systemui.statusbar.notification.row.icon import android.os.userManager import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos +import org.mockito.kotlin.mock + +val Kosmos.mockNotificationIconStyleProvider by + Kosmos.Fixture { mock<NotificationIconStyleProvider>() } val Kosmos.notificationIconStyleProvider by Kosmos.Fixture { NotificationIconStyleProviderImpl(userManager, dumpManager) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt index 3e96fd7c729f..e5e1a830231e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt @@ -26,7 +26,7 @@ import com.android.systemui.statusbar.notification.data.model.activeNotification import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.addNotif import com.android.systemui.statusbar.notification.data.repository.removeNotif -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels import com.android.systemui.statusbar.notification.shared.CallType import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository @@ -39,7 +39,7 @@ fun inCallModel( intent: PendingIntent? = null, notificationKey: String = "test", appName: String = "", - promotedContent: PromotedNotificationContentModel? = null, + promotedContent: PromotedNotificationContentModels? = null, isAppVisible: Boolean = false, ) = OngoingCallModel.InCall( @@ -77,7 +77,7 @@ object OngoingCallTestHelper { key: String = "notif", startTimeMs: Long = 1000L, statusBarChipIconView: StatusBarIconView? = createStatusBarIconViewOrNull(), - promotedContent: PromotedNotificationContentModel? = null, + promotedContent: PromotedNotificationContentModels? = null, contentIntent: PendingIntent? = null, uid: Int = DEFAULT_UID, appName: String = "Fake name", diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ui/TintedIconManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ui/TintedIconManagerKosmos.kt index 8e13b624f5fa..fd63ce28a29d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ui/TintedIconManagerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ui/TintedIconManagerKosmos.kt @@ -16,16 +16,24 @@ package com.android.systemui.statusbar.phone.ui +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.kairos import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.connectivity.ui.mobileContextProvider import com.android.systemui.statusbar.pipeline.mobile.ui.mobileUiAdapter +import com.android.systemui.statusbar.pipeline.mobile.ui.mobileUiAdapterKairos import com.android.systemui.statusbar.pipeline.wifi.ui.wifiUiAdapter +@OptIn(ExperimentalKairosApi::class) val Kosmos.tintedIconManagerFactory by -Kosmos.Fixture { - TintedIconManager.Factory( - wifiUiAdapter, - mobileUiAdapter, - mobileContextProvider, - ) -}
\ No newline at end of file + Kosmos.Fixture { + TintedIconManager.Factory( + wifiUiAdapter, + mobileUiAdapter, + mobileContextProvider, + { mobileUiAdapterKairos }, + kairos, + applicationCoroutineScope, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairosKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairosKosmos.kt new file mode 100644 index 000000000000..3a3f18ad2ede --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairosKosmos.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.systemui.statusbar.pipeline.mobile.ui + +import com.android.systemui.dump.dumpManager +import com.android.systemui.kairos.ActivatedKairosFixture +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.phone.ui.statusBarIconController +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.mobileIconsViewModelKairos + +@ExperimentalKairosApi +val Kosmos.mobileUiAdapterKairos by ActivatedKairosFixture { + MobileUiAdapterKairos( + statusBarIconController, + mobileIconsViewModelKairos, + mobileViewLogger, + dumpManager, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt index 1504df4ef6d0..6767300a22bc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt @@ -55,5 +55,6 @@ val Kosmos.userSwitcherInteractor by uiEventLogger = uiEventLogger, userRestrictionChecker = userRestrictionChecker, processWrapper = processWrapper, + userLogoutInteractor = userLogoutInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt index b619e2d70724..2b518182922d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt @@ -26,7 +26,7 @@ val Kosmos.windowRootViewBlurRepository: WindowRootViewBlurRepository by Kosmos.Fixture { fakeWindowRootViewBlurRepository } class FakeWindowRootViewBlurRepository : WindowRootViewBlurRepository { - override val blurRadius: MutableStateFlow<Int> = MutableStateFlow(0) + override val blurRequestedByShade: MutableStateFlow<Int> = MutableStateFlow(0) override val isBlurOpaque: MutableStateFlow<Boolean> = MutableStateFlow(false) override val isBlurSupported: MutableStateFlow<Boolean> = MutableStateFlow(false) override var blurAppliedListener: BlurAppliedListener? = null diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index 35db3c6f0a6d..a133131a1d3f 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -223,6 +223,16 @@ flag { } flag { + name: "manager_lifecycle_user_change" + namespace: "accessibility" + description: "Use A11yManagerService's Lifecycle to change users, instead of listening for user changed events." + bug: "393626471" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "motion_event_injector_cancel_fix" namespace: "accessibility" description: "Fix the ACTION_CANCEL logic used in MotionEventInjector to avoid InputDispatcher inconsistency" diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 703e37fad5ad..39c1fa73b7ce 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -500,6 +500,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mService = new AccessibilityManagerService(context); } + @VisibleForTesting + public Lifecycle(Context context, AccessibilityManagerService service) { + super(context); + mService = service; + } + @Override public void onStart() { LocalServices.addService(AccessibilityManagerInternal.class, @@ -511,17 +517,19 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub public void onBootPhase(int phase) { mService.onBootPhase(phase); } + + @Override + public void onUserSwitching(@androidx.annotation.Nullable TargetUser from, + @androidx.annotation.NonNull TargetUser to) { + super.onUserSwitching(from, to); + if (Flags.managerLifecycleUserChange()) { + mService.switchUser(to.getUserIdentifier()); + } + } } private InputManager.KeyGestureEventHandler mKeyGestureEventHandler = - new InputManager.KeyGestureEventHandler() { - @Override - public boolean handleKeyGestureEvent( - @NonNull KeyGestureEvent event, - @Nullable IBinder focusedToken) { - return AccessibilityManagerService.this.handleKeyGestureEvent(event); - } - }; + (event, focusedToken) -> AccessibilityManagerService.this.handleKeyGestureEvent(event); @VisibleForTesting AccessibilityManagerService( @@ -637,7 +645,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub new AccessibilityContentObserver(mMainHandler).register( mContext.getContentResolver()); if (enableTalkbackAndMagnifierKeyGestures()) { - mInputManager.registerKeyGestureEventHandler(mKeyGestureEventHandler); + List<Integer> supportedGestures = List.of( + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION, + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK); + mInputManager.registerKeyGestureEventHandler(supportedGestures, + mKeyGestureEventHandler); } if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) { if (mHearingDeviceNotificationController != null) { @@ -686,13 +698,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } @VisibleForTesting - boolean handleKeyGestureEvent(KeyGestureEvent event) { + void handleKeyGestureEvent(KeyGestureEvent event) { final boolean complete = event.getAction() == KeyGestureEvent.ACTION_GESTURE_COMPLETE && !event.isCancelled(); final int gestureType = event.getKeyGestureType(); if (!complete) { - return false; + return; } String targetName; @@ -703,7 +715,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub case KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK: targetName = mContext.getString(R.string.config_defaultSelectToSpeakService); if (targetName.isEmpty()) { - return false; + return; } final ComponentName targetServiceComponent = TextUtils.isEmpty(targetName) @@ -715,7 +727,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub userState.getInstalledServiceInfoLocked(targetServiceComponent); } if (accessibilityServiceInfo == null) { - return false; + return; } // Skip enabling if a warning dialog is required for the feature. @@ -725,11 +737,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub Slog.w(LOG_TAG, "Accessibility warning is required before this service can be " + "activated automatically via KEY_GESTURE shortcut."); - return false; + return; } break; default: - return false; + Slog.w(LOG_TAG, "Received a key gesture " + event + + " that was not registered by this handler"); + return; } List<String> shortcutTargets = getAccessibilityShortcutTargets( @@ -748,14 +762,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub // this will be a separate dialog that appears that requires the user to confirm // which will resolve this race condition. For now, just require two presses the // first time it is activated. - return true; + return; } final int displayId = event.getDisplayId() != INVALID_DISPLAY ? event.getDisplayId() : getLastNonProxyTopFocusedDisplayId(); performAccessibilityShortcutInternal(displayId, KEY_GESTURE, targetName); - - return true; } @Override @@ -1055,7 +1067,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub String action = intent.getAction(); if (Intent.ACTION_USER_SWITCHED.equals(action)) { - switchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0)); + if (!Flags.managerLifecycleUserChange()) { + switchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0)); + } } else if (Intent.ACTION_USER_UNLOCKED.equals(action)) { unlockUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0)); } else if (Intent.ACTION_USER_REMOVED.equals(action)) { 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 0b9c45de6e40..60343e9e81e5 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -152,9 +152,20 @@ public class AutoclickController extends BaseEventStreamTransformation { if (direction == AutoclickScrollPanel.DIRECTION_EXIT) { return; } - // For direction buttons, perform scroll action immediately. - if (hovered && direction != AutoclickScrollPanel.DIRECTION_NONE) { - handleScroll(direction); + + // Handle all non-exit buttons when hovered. + if (hovered) { + // Clear the indicator. + if (mAutoclickIndicatorScheduler != null) { + mAutoclickIndicatorScheduler.cancel(); + if (mAutoclickIndicatorView != null) { + mAutoclickIndicatorView.clearIndicator(); + } + } + // Perform scroll action. + if (direction != DIRECTION_NONE) { + handleScroll(direction); + } } } }; diff --git a/services/core/java/com/android/server/adb/AdbService.java b/services/core/java/com/android/server/adb/AdbService.java index d12a0a2a1e00..c338a1ef15c9 100644 --- a/services/core/java/com/android/server/adb/AdbService.java +++ b/services/core/java/com/android/server/adb/AdbService.java @@ -32,6 +32,7 @@ import android.debug.IAdbTransport; import android.debug.PairDevice; import android.hardware.usb.UsbManager; import android.net.Uri; +import android.net.wifi.WifiManager; import android.os.Binder; import android.os.IBinder; import android.os.ParcelFileDescriptor; @@ -192,6 +193,7 @@ public class AdbService extends IAdbManager.Stub { @Override public void onChange(boolean selfChange, @NonNull Uri uri, @UserIdInt int userId) { + Slog.d("AdbSettingsObserver", "onChange " + uri.toString()); if (mAdbUsbUri.equals(uri)) { boolean shouldEnable = (Settings.Global.getInt(mContentResolver, Settings.Global.ADB_ENABLED, 0) > 0); @@ -417,6 +419,28 @@ public class AdbService extends IAdbManager.Stub { } } + private WifiManager.MulticastLock mAdbMulticastLock = null; + + private void acquireMulticastLock() { + if (mAdbMulticastLock == null) { + WifiManager wifiManager = (WifiManager) + mContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + mAdbMulticastLock = wifiManager.createMulticastLock("AdbMulticastLock"); + } + + if (!mAdbMulticastLock.isHeld()) { + mAdbMulticastLock.acquire(); + Slog.d(TAG, "Acquired multicast lock"); + } + } + + private void releaseMulticastLock() { + if (mAdbMulticastLock != null && mAdbMulticastLock.isHeld()) { + mAdbMulticastLock.release(); + Slog.d(TAG, "Released multicast lock"); + } + } + private void setAdbEnabled(boolean enable, byte transportType) { Slog.d(TAG, "setAdbEnabled(" + enable + "), mIsAdbUsbEnabled=" + mIsAdbUsbEnabled + ", mIsAdbWifiEnabled=" + mIsAdbWifiEnabled + ", transportType=" + transportType); @@ -428,9 +452,11 @@ public class AdbService extends IAdbManager.Stub { if (mIsAdbWifiEnabled) { // Start adb over WiFi. SystemProperties.set(WIFI_PERSISTENT_CONFIG_PROPERTY, "1"); + acquireMulticastLock(); } else { // Stop adb over WiFi. SystemProperties.set(WIFI_PERSISTENT_CONFIG_PROPERTY, "0"); + releaseMulticastLock(); } } else { // No change diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index a61368c4bc36..ad47e67b9332 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -793,6 +793,7 @@ public final class ProcessList { final ProcessMap<ProcessRecord> mDyingProcesses = new ProcessMap<>(); // Self locked with the inner lock within the RemoteCallbackList + @GuardedBy("mProcessObservers") private final RemoteCallbackList<IProcessObserver> mProcessObservers = new RemoteCallbackList<>(); @@ -4980,11 +4981,15 @@ public final class ProcessList { } void registerProcessObserver(IProcessObserver observer) { - mProcessObservers.register(observer); + synchronized (mProcessObservers) { + mProcessObservers.register(observer); + } } void unregisterProcessObserver(IProcessObserver observer) { - mProcessObservers.unregister(observer); + synchronized (mProcessObservers) { + mProcessObservers.unregister(observer); + } } void dispatchProcessesChanged() { @@ -5002,38 +5007,41 @@ public final class ProcessList { } } - int i = mProcessObservers.beginBroadcast(); - while (i > 0) { - i--; - final IProcessObserver observer = mProcessObservers.getBroadcastItem(i); - if (observer != null) { - try { - for (int j = 0; j < numOfChanges; j++) { - ProcessChangeItem item = mActiveProcessChanges[j]; - if ((item.changes & ProcessChangeItem.CHANGE_ACTIVITIES) != 0) { - if (DEBUG_PROCESS_OBSERVERS) { - Slog.i(TAG_PROCESS_OBSERVERS, - "ACTIVITIES CHANGED pid=" + item.pid + " uid=" - + item.uid + ": " + item.foregroundActivities); + synchronized (mProcessObservers) { + int i = mProcessObservers.beginBroadcast(); + while (i > 0) { + i--; + final IProcessObserver observer = mProcessObservers.getBroadcastItem(i); + if (observer != null) { + try { + for (int j = 0; j < numOfChanges; j++) { + ProcessChangeItem item = mActiveProcessChanges[j]; + if ((item.changes & ProcessChangeItem.CHANGE_ACTIVITIES) != 0) { + if (DEBUG_PROCESS_OBSERVERS) { + Slog.i(TAG_PROCESS_OBSERVERS, + "ACTIVITIES CHANGED pid=" + item.pid + " uid=" + + item.uid + ": " + item.foregroundActivities); + } + observer.onForegroundActivitiesChanged(item.pid, item.uid, + item.foregroundActivities); } - observer.onForegroundActivitiesChanged(item.pid, item.uid, - item.foregroundActivities); - } - if ((item.changes & ProcessChangeItem.CHANGE_FOREGROUND_SERVICES) != 0) { - if (DEBUG_PROCESS_OBSERVERS) { - Slog.i(TAG_PROCESS_OBSERVERS, - "FOREGROUND SERVICES CHANGED pid=" + item.pid + " uid=" - + item.uid + ": " + item.foregroundServiceTypes); + if ((item.changes & ProcessChangeItem.CHANGE_FOREGROUND_SERVICES) + != 0) { + if (DEBUG_PROCESS_OBSERVERS) { + Slog.i(TAG_PROCESS_OBSERVERS, + "FOREGROUND SERVICES CHANGED pid=" + item.pid + " uid=" + + item.uid + ": " + item.foregroundServiceTypes); + } + observer.onForegroundServicesChanged(item.pid, item.uid, + item.foregroundServiceTypes); } - observer.onForegroundServicesChanged(item.pid, item.uid, - item.foregroundServiceTypes); } + } catch (RemoteException e) { } - } catch (RemoteException e) { } } + mProcessObservers.finishBroadcast(); } - mProcessObservers.finishBroadcast(); synchronized (mProcessChangeLock) { for (int j = 0; j < numOfChanges; j++) { @@ -5122,22 +5130,42 @@ public final class ProcessList { } void dispatchProcessStarted(ProcessRecord app, int pid) { - // TODO(b/323959187) Add the implementation. + if (!android.app.Flags.enableProcessObserverBroadcastOnProcessStarted()) { + Slog.i(TAG, "ProcessObserver broadcast disabled"); + return; + } + synchronized (mProcessObservers) { + int i = mProcessObservers.beginBroadcast(); + while (i > 0) { + i--; + final IProcessObserver observer = mProcessObservers.getBroadcastItem(i); + if (observer != null) { + try { + observer.onProcessStarted(pid, app.uid, app.info.uid, + app.info.packageName, app.processName); + } catch (RemoteException e) { + } + } + } + mProcessObservers.finishBroadcast(); + } } void dispatchProcessDied(int pid, int uid) { - int i = mProcessObservers.beginBroadcast(); - while (i > 0) { - i--; - final IProcessObserver observer = mProcessObservers.getBroadcastItem(i); - if (observer != null) { - try { - observer.onProcessDied(pid, uid); - } catch (RemoteException e) { + synchronized (mProcessObservers) { + int i = mProcessObservers.beginBroadcast(); + while (i > 0) { + i--; + final IProcessObserver observer = mProcessObservers.getBroadcastItem(i); + if (observer != null) { + try { + observer.onProcessDied(pid, uid); + } catch (RemoteException e) { + } } } + mProcessObservers.finishBroadcast(); } - mProcessObservers.finishBroadcast(); } @GuardedBy(anyOf = {"mService", "mProcLock"}) diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index cb4342f27bc8..c2ed4d557e69 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -256,6 +256,7 @@ public class SettingsToPropertiesMapper { "pixel_vpn", "pixel_watch", "pixel_watch_debug_trace", + "pixel_watch_watchfaces", "pixel_wifi", "platform_compat", "platform_security", diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 6b3661a2a004..a8bb5231d8c0 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -186,6 +186,7 @@ import android.media.audiopolicy.AudioPolicyConfig; import android.media.audiopolicy.AudioProductStrategy; import android.media.audiopolicy.AudioVolumeGroup; import android.media.audiopolicy.IAudioPolicyCallback; +import android.media.audiopolicy.IAudioVolumeChangeDispatcher; import android.media.permission.ClearCallingIdentityContext; import android.media.permission.SafeCloseable; import android.media.projection.IMediaProjection; @@ -1388,6 +1389,7 @@ public class AudioService extends IAudioService.Stub mUseVolumeGroupAliases = mContext.getResources().getBoolean( com.android.internal.R.bool.config_handleVolumeAliasesUsingVolumeGroups); + mAudioVolumeChangeHandler = new AudioVolumeChangeHandler(mAudioSystem); // Initialize volume // Priority 1 - Android Property // Priority 2 - Audio Policy Service @@ -4452,6 +4454,27 @@ public class AudioService extends IAudioService.Stub } } + //================================ + // Audio Volume Change Dispatcher + //================================ + private final AudioVolumeChangeHandler mAudioVolumeChangeHandler; + + /** @see AudioManager#registerVolumeGroupCallback(executor, callback) */ + @android.annotation.EnforcePermission( + android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public void registerAudioVolumeCallback(IAudioVolumeChangeDispatcher callback) { + super.registerAudioVolumeCallback_enforcePermission(); + mAudioVolumeChangeHandler.registerListener(callback); + } + + /** @see AudioManager#unregisterVolumeGroupCallback(callback) */ + @android.annotation.EnforcePermission( + android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public void unregisterAudioVolumeCallback(IAudioVolumeChangeDispatcher callback) { + super.unregisterAudioVolumeCallback_enforcePermission(); + mAudioVolumeChangeHandler.unregisterListener(callback); + } + @Override @android.annotation.EnforcePermission(anyOf = { MODIFY_AUDIO_SETTINGS_PRIVILEGED, MODIFY_AUDIO_ROUTING }) diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index a6267c156fb3..ced5faeeff27 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -23,6 +23,7 @@ import android.media.AudioDeviceAttributes; import android.media.AudioMixerAttributes; import android.media.AudioSystem; import android.media.IDevicesForAttributesCallback; +import android.media.INativeAudioVolumeGroupCallback; import android.media.ISoundDose; import android.media.ISoundDoseCallback; import android.media.audiopolicy.AudioMix; @@ -758,6 +759,29 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, } /** + * Same as {@link AudioSystem#registerAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback)} + * @param callback to register + * @return {@link #SUCCESS} if successfully registered. + * + * @hide + */ + public int registerAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback callback) { + return AudioSystem.registerAudioVolumeGroupCallback(callback); + } + + /** + * Same as + * {@link AudioSystem#unregisterAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback)}. + * @param callback to register + * @return {@link #SUCCESS} if successfully registered. + * + * @hide + */ + public int unregisterAudioVolumeGroupCallback(INativeAudioVolumeGroupCallback callback) { + return AudioSystem.unregisterAudioVolumeGroupCallback(callback); + } + + /** * Part of AudioService dump * @param pw */ diff --git a/services/core/java/com/android/server/audio/AudioVolumeChangeHandler.java b/services/core/java/com/android/server/audio/AudioVolumeChangeHandler.java new file mode 100644 index 000000000000..2bb4301bb8fd --- /dev/null +++ b/services/core/java/com/android/server/audio/AudioVolumeChangeHandler.java @@ -0,0 +1,103 @@ +/* + * 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.audio; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.media.INativeAudioVolumeGroupCallback; +import android.media.audio.common.AudioVolumeGroupChangeEvent; +import android.media.audiopolicy.IAudioVolumeChangeDispatcher; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.Preconditions; + +/** + * The AudioVolumeChangeHandler handles AudioVolume callbacks invoked by native + * {@link INativeAudioVolumeGroupCallback} callback. + */ +/* private package */ class AudioVolumeChangeHandler { + private static final String TAG = "AudioVolumeChangeHandler"; + + private final Object mLock = new Object(); + @GuardedBy("mLock") + private final RemoteCallbackList<IAudioVolumeChangeDispatcher> mListeners = + new RemoteCallbackList<>(); + private final @NonNull AudioSystemAdapter mAudioSystem; + private @Nullable AudioVolumeGroupCallback mAudioVolumeGroupCallback; + + AudioVolumeChangeHandler(@NonNull AudioSystemAdapter asa) { + mAudioSystem = asa; + } + + @GuardedBy("mLock") + private void lazyInitLocked() { + mAudioVolumeGroupCallback = new AudioVolumeGroupCallback(); + mAudioSystem.registerAudioVolumeGroupCallback(mAudioVolumeGroupCallback); + } + + private void sendAudioVolumeGroupChangedToClients(int groupId, int index) { + RemoteCallbackList<IAudioVolumeChangeDispatcher> listeners; + int nbDispatchers; + synchronized (mLock) { + listeners = mListeners; + nbDispatchers = mListeners.beginBroadcast(); + } + for (int i = 0; i < nbDispatchers; i++) { + try { + listeners.getBroadcastItem(i).onAudioVolumeGroupChanged(groupId, index); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to broadcast Volume Changed event"); + } + } + synchronized (mLock) { + mListeners.finishBroadcast(); + } + } + + /** + * @param cb the {@link IAudioVolumeChangeDispatcher} to register + */ + public void registerListener(@NonNull IAudioVolumeChangeDispatcher cb) { + Preconditions.checkNotNull(cb, "Volume group callback must not be null"); + synchronized (mLock) { + if (mAudioVolumeGroupCallback == null) { + lazyInitLocked(); + } + mListeners.register(cb); + } + } + + /** + * @param cb the {@link IAudioVolumeChangeDispatcher} to unregister + */ + public void unregisterListener(@NonNull IAudioVolumeChangeDispatcher cb) { + Preconditions.checkNotNull(cb, "Volume group callback must not be null"); + synchronized (mLock) { + mListeners.unregister(cb); + } + } + + private final class AudioVolumeGroupCallback extends INativeAudioVolumeGroupCallback.Stub { + public void onAudioVolumeGroupChanged(AudioVolumeGroupChangeEvent volumeEvent) { + Slog.v(TAG, "onAudioVolumeGroupChanged volumeEvent=" + volumeEvent); + sendAudioVolumeGroupChangedToClients(volumeEvent.groupId, volumeEvent.flags); + } + } +} diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index a28069bbf050..95e58e1a7300 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -684,8 +684,9 @@ public final class DisplayManagerService extends SystemService { final var backupManager = new BackupManager(mContext); Consumer<Pair<DisplayTopology, DisplayTopologyGraph>> topologyChangedCallback = update -> { - if (mInputManagerInternal != null) { - mInputManagerInternal.setDisplayTopology(update.second); + DisplayTopologyGraph graph = update.second; + if (mInputManagerInternal != null && graph != null) { + mInputManagerInternal.setDisplayTopology(graph); } deliverTopologyUpdate(update.first); }; @@ -3647,7 +3648,7 @@ public final class DisplayManagerService extends SystemService { private void deliverTopologyUpdate(DisplayTopology topology) { if (DEBUG) { - Slog.d(TAG, "Delivering topology update"); + Slog.d(TAG, "Delivering topology update: " + topology); } if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) { Trace.instant(Trace.TRACE_TAG_POWER, "deliverTopologyUpdate"); @@ -4209,13 +4210,18 @@ public final class DisplayManagerService extends SystemService { public boolean mWifiDisplayScanRequested; - // A single pending event. + // A single pending display event. private record Event(int displayId, @DisplayEvent int event) { }; - // The list of pending events. This is null until there is a pending event to be saved. - // This is only used if {@link deferDisplayEventsWhenFrozen()} is true. + // The list of pending display events. This is null until there is a pending event to be + // saved. This is only used if {@link deferDisplayEventsWhenFrozen()} is true. + @GuardedBy("mCallback") + @Nullable + private ArrayList<Event> mPendingDisplayEvents; + @GuardedBy("mCallback") - private ArrayList<Event> mPendingEvents; + @Nullable + private DisplayTopology mPendingTopology; // Process states: a process is ready to receive events if it is neither cached nor // frozen. @@ -4285,7 +4291,10 @@ public final class DisplayManagerService extends SystemService { */ @GuardedBy("mCallback") private boolean hasPendingAndIsReadyLocked() { - return isReadyLocked() && mPendingEvents != null && !mPendingEvents.isEmpty() && mAlive; + boolean pendingDisplayEvents = mPendingDisplayEvents != null + && !mPendingDisplayEvents.isEmpty(); + boolean pendingTopology = mPendingTopology != null; + return isReadyLocked() && (pendingDisplayEvents || pendingTopology) && mAlive; } /** @@ -4366,7 +4375,8 @@ public final class DisplayManagerService extends SystemService { // occurs as the client is transitioning to ready but pending events have not // been dispatched. The new event must be added to the pending list to // preserve event ordering. - if (!isReadyLocked() || (mPendingEvents != null && !mPendingEvents.isEmpty())) { + if (!isReadyLocked() || (mPendingDisplayEvents != null + && !mPendingDisplayEvents.isEmpty())) { // The client is interested in the event but is not ready to receive it. // Put the event on the pending list. addDisplayEvent(displayId, event); @@ -4453,13 +4463,13 @@ public final class DisplayManagerService extends SystemService { // This is only used if {@link deferDisplayEventsWhenFrozen()} is true. @GuardedBy("mCallback") private void addDisplayEvent(int displayId, int event) { - if (mPendingEvents == null) { - mPendingEvents = new ArrayList<>(); + if (mPendingDisplayEvents == null) { + mPendingDisplayEvents = new ArrayList<>(); } - if (!mPendingEvents.isEmpty()) { + if (!mPendingDisplayEvents.isEmpty()) { // Ignore redundant events. Further optimization is possible by merging adjacent // events. - Event last = mPendingEvents.get(mPendingEvents.size() - 1); + Event last = mPendingDisplayEvents.get(mPendingDisplayEvents.size() - 1); if (last.displayId == displayId && last.event == event) { if (DEBUG) { Slog.d(TAG, "Ignore redundant display event " + displayId + "/" + event @@ -4468,12 +4478,13 @@ public final class DisplayManagerService extends SystemService { return; } } - mPendingEvents.add(new Event(displayId, event)); + mPendingDisplayEvents.add(new Event(displayId, event)); } /** * @return {@code false} if RemoteException happens; otherwise {@code true} for - * success. + * success. This returns true even if the update was deferred because the remote client is + * cached or frozen. */ boolean notifyTopologyUpdateAsync(DisplayTopology topology) { if ((mInternalEventFlagsMask.get() @@ -4490,6 +4501,18 @@ public final class DisplayManagerService extends SystemService { // The client is not interested in this event, so do nothing. return true; } + + if (deferDisplayEventsWhenFrozen()) { + synchronized (mCallback) { + // Save the new update if the client frozen or cached (not ready). + if (!isReadyLocked()) { + // The client is interested in the update but is not ready to receive it. + mPendingTopology = topology; + return true; + } + } + } + return transmitTopologyUpdate(topology); } @@ -4514,37 +4537,54 @@ public final class DisplayManagerService extends SystemService { // would be unusual to do so. The method returns true on success. // This is only used if {@link deferDisplayEventsWhenFrozen()} is true. public boolean dispatchPending() { - Event[] pending; + Event[] pendingDisplayEvents = null; + DisplayTopology pendingTopology; synchronized (mCallback) { - if (mPendingEvents == null || mPendingEvents.isEmpty() || !mAlive) { + if (!mAlive) { return true; } if (!isReadyLocked()) { return false; } - pending = new Event[mPendingEvents.size()]; - pending = mPendingEvents.toArray(pending); - mPendingEvents.clear(); + + if (mPendingDisplayEvents != null && !mPendingDisplayEvents.isEmpty()) { + pendingDisplayEvents = new Event[mPendingDisplayEvents.size()]; + pendingDisplayEvents = mPendingDisplayEvents.toArray(pendingDisplayEvents); + mPendingDisplayEvents.clear(); + } + + pendingTopology = mPendingTopology; + mPendingTopology = null; } try { - for (int i = 0; i < pending.length; i++) { - Event displayEvent = pending[i]; - if (DEBUG) { - Slog.d(TAG, "Send pending display event #" + i + " " - + displayEvent.displayId + "/" - + displayEvent.event + " to " + mUid + "/" + mPid); - } + if (pendingDisplayEvents != null) { + for (int i = 0; i < pendingDisplayEvents.length; i++) { + Event displayEvent = pendingDisplayEvents[i]; + if (DEBUG) { + Slog.d(TAG, "Send pending display event #" + i + " " + + displayEvent.displayId + "/" + + displayEvent.event + " to " + mUid + "/" + mPid); + } + + if (!shouldReceiveRefreshRateWithChangeUpdate(displayEvent.event)) { + continue; + } - if (!shouldReceiveRefreshRateWithChangeUpdate(displayEvent.event)) { - continue; + transmitDisplayEvent(displayEvent.displayId, displayEvent.event); } + } - transmitDisplayEvent(displayEvent.displayId, displayEvent.event); + if (pendingTopology != null) { + if (DEBUG) { + Slog.d(TAG, "Send pending topology: " + pendingTopology + + " to " + mUid + "/" + mPid); + } + mCallback.onTopologyChanged(pendingTopology); } + return true; } catch (RemoteException ex) { - Slog.w(TAG, "Failed to notify process " - + mPid + " that display topology changed, assuming it died.", ex); + Slog.w(TAG, "Failed to notify process " + mPid + ", assuming it died.", ex); binderDied(); return false; @@ -4556,11 +4596,12 @@ public final class DisplayManagerService extends SystemService { if (deferDisplayEventsWhenFrozen()) { final String fmt = "mPid=%d mUid=%d mWifiDisplayScanRequested=%s" - + " cached=%s frozen=%s pending=%d"; + + " cached=%s frozen=%s pendingDisplayEvents=%d pendingTopology=%b"; synchronized (mCallback) { return formatSimple(fmt, mPid, mUid, mWifiDisplayScanRequested, mCached, mFrozen, - (mPendingEvents == null) ? 0 : mPendingEvents.size()); + (mPendingDisplayEvents == null) ? 0 : mPendingDisplayEvents.size(), + mPendingTopology != null); } } else { final String fmt = diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java index c37733b05fba..2c90e1919123 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -156,6 +156,8 @@ public class DisplayModeDirector { private SparseArray<Display.Mode> mDefaultModeByDisplay; // a map from display id to display device config private SparseArray<DisplayDeviceConfig> mDisplayDeviceConfigByDisplay = new SparseArray<>(); + // set containing connected external display ids + private final Set<Integer> mExternalDisplaysConnected = new HashSet<>(); private SparseBooleanArray mHasArrSupport; @@ -425,7 +427,7 @@ public class DisplayModeDirector { // Some external displays physical refresh rate modes are slightly above 60hz. // SurfaceFlinger will not enable these display modes unless it is configured to allow // render rate at least at this frame rate. - if (mDisplayObserver.isExternalDisplayLocked(displayId)) { + if (isExternalDisplayLocked(displayId)) { primarySummary.maxRenderFrameRate = Math.max(baseMode.getRefreshRate(), primarySummary.maxRenderFrameRate); appRequestSummary.maxRenderFrameRate = Math.max(baseMode.getRefreshRate(), @@ -653,6 +655,10 @@ public class DisplayModeDirector { } } + boolean isExternalDisplayLocked(int displayId) { + return mExternalDisplaysConnected.contains(displayId); + } + private static String switchingTypeToString(@DisplayManager.SwitchingType int type) { switch (type) { case DisplayManager.SWITCHING_TYPE_NONE: @@ -694,6 +700,11 @@ public class DisplayModeDirector { } @VisibleForTesting + void addExternalDisplayId(int externalDisplayId) { + mExternalDisplaysConnected.add(externalDisplayId); + } + + @VisibleForTesting void injectBrightnessObserver(BrightnessObserver brightnessObserver) { mBrightnessObserver = brightnessObserver; } @@ -1210,7 +1221,7 @@ public class DisplayModeDirector { @GuardedBy("mLock") private void updateRefreshRateSettingLocked(float minRefreshRate, float peakRefreshRate, float defaultRefreshRate, int displayId) { - if (mDisplayObserver.isExternalDisplayLocked(displayId)) { + if (isExternalDisplayLocked(displayId)) { if (mLoggingEnabled) { Slog.d(TAG, "skip updateRefreshRateSettingLocked for external display " + displayId); @@ -1309,20 +1320,25 @@ public class DisplayModeDirector { public void setAppRequest(int displayId, int modeId, float requestedRefreshRate, float requestedMinRefreshRateRange, float requestedMaxRefreshRateRange) { Display.Mode requestedMode; + boolean isExternalDisplay; synchronized (mLock) { requestedMode = findModeLocked(displayId, modeId, requestedRefreshRate); + isExternalDisplay = isExternalDisplayLocked(displayId); } Vote frameRateVote = getFrameRateVote( requestedMinRefreshRateRange, requestedMaxRefreshRateRange); Vote baseModeRefreshRateVote = getBaseModeVote(requestedMode, requestedRefreshRate); - Vote sizeVote = getSizeVote(requestedMode); mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE, frameRateVote); mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE, baseModeRefreshRateVote); - mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_SIZE, sizeVote); + + if (!isExternalDisplay) { + Vote sizeVote = getSizeVote(requestedMode); + mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_SIZE, sizeVote); + } } private Display.Mode findModeLocked(int displayId, int modeId, float requestedRefreshRate) { @@ -1420,7 +1436,6 @@ public class DisplayModeDirector { private int mExternalDisplayPeakHeight; private int mExternalDisplayPeakRefreshRate; private final boolean mRefreshRateSynchronizationEnabled; - private final Set<Integer> mExternalDisplaysConnected = new HashSet<>(); DisplayObserver(Context context, Handler handler, VotesStorage votesStorage, Injector injector) { @@ -1541,10 +1556,6 @@ public class DisplayModeDirector { } } - boolean isExternalDisplayLocked(int displayId) { - return mExternalDisplaysConnected.contains(displayId); - } - @Nullable private DisplayInfo getDisplayInfo(int displayId) { DisplayInfo info = new DisplayInfo(); diff --git a/services/core/java/com/android/server/display/mode/ModeChangeObserver.java b/services/core/java/com/android/server/display/mode/ModeChangeObserver.java index 50782a2f22c8..debfb067b710 100644 --- a/services/core/java/com/android/server/display/mode/ModeChangeObserver.java +++ b/services/core/java/com/android/server/display/mode/ModeChangeObserver.java @@ -25,6 +25,8 @@ import android.view.Display; import android.view.DisplayAddress; import android.view.DisplayEventReceiver; +import androidx.annotation.VisibleForTesting; + import java.util.HashSet; import java.util.Set; @@ -35,8 +37,10 @@ final class ModeChangeObserver { private final DisplayModeDirector.Injector mInjector; @SuppressWarnings("unused") - private DisplayEventReceiver mModeChangeListener; - private DisplayManager.DisplayListener mDisplayListener; + @VisibleForTesting + DisplayEventReceiver mModeChangeListener; + @VisibleForTesting + DisplayManager.DisplayListener mDisplayListener; private final LongSparseArray<Set<Integer>> mRejectedModesMap = new LongSparseArray<>(); private final LongSparseArray<Integer> mPhysicalIdToLogicalIdMap = new LongSparseArray<>(); diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 41b0b4dc716a..a2d065400045 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -252,17 +252,22 @@ public class HdmiControlService extends SystemService { static final AudioDeviceAttributes AUDIO_OUTPUT_DEVICE_HDMI_EARC = new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_HDMI_EARC, ""); + static final AudioDeviceAttributes AUDIO_OUTPUT_DEVICE_LINE_DIGITAL = + new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_LINE_DIGITAL, ""); // Audio output devices used for absolute volume behavior private static final List<AudioDeviceAttributes> AVB_AUDIO_OUTPUT_DEVICES = List.of(AUDIO_OUTPUT_DEVICE_HDMI, AUDIO_OUTPUT_DEVICE_HDMI_ARC, - AUDIO_OUTPUT_DEVICE_HDMI_EARC); + AUDIO_OUTPUT_DEVICE_HDMI_EARC, + AUDIO_OUTPUT_DEVICE_LINE_DIGITAL); // Audio output devices used for absolute volume behavior on TV panels private static final List<AudioDeviceAttributes> TV_AVB_AUDIO_OUTPUT_DEVICES = List.of(AUDIO_OUTPUT_DEVICE_HDMI_ARC, - AUDIO_OUTPUT_DEVICE_HDMI_EARC); + AUDIO_OUTPUT_DEVICE_HDMI_EARC, + AUDIO_OUTPUT_DEVICE_LINE_DIGITAL); // Audio output devices used for absolute volume behavior on Playback devices private static final List<AudioDeviceAttributes> PLAYBACK_AVB_AUDIO_OUTPUT_DEVICES = diff --git a/services/core/java/com/android/server/input/AppLaunchShortcutManager.java b/services/core/java/com/android/server/input/AppLaunchShortcutManager.java index 8c028bc92841..eb102294ac32 100644 --- a/services/core/java/com/android/server/input/AppLaunchShortcutManager.java +++ b/services/core/java/com/android/server/input/AppLaunchShortcutManager.java @@ -111,7 +111,7 @@ final class AppLaunchShortcutManager { mContext = context; } - public void systemRunning() { + public void init() { loadShortcuts(); } diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java index 67e1ccc6a850..e6d71900f106 100644 --- a/services/core/java/com/android/server/input/InputGestureManager.java +++ b/services/core/java/com/android/server/input/InputGestureManager.java @@ -94,9 +94,9 @@ final class InputGestureManager { mContext = context; } - public void systemRunning() { + public void init(List<InputGestureData> bookmarks) { initSystemShortcuts(); - blockListBookmarkedTriggers(); + blockListBookmarkedTriggers(bookmarks); } private void initSystemShortcuts() { @@ -263,10 +263,9 @@ final class InputGestureManager { } } - private void blockListBookmarkedTriggers() { + private void blockListBookmarkedTriggers(List<InputGestureData> bookmarks) { synchronized (mGestureLock) { - InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class)); - for (InputGestureData bookmark : im.getAppLaunchBookmarks()) { + for (InputGestureData bookmark : bookmarks) { mBlockListedTriggers.add(bookmark.getTrigger()); } } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 6e6d00d62819..29e04e744759 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -2751,18 +2751,23 @@ public class InputManagerService extends IInputManager.Stub @SuppressLint("MissingPermission") private void initKeyGestures() { InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class)); - im.registerKeyGestureEventHandler(new InputManager.KeyGestureEventHandler() { - @Override - public boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event, - @Nullable IBinder focussedToken) { - return InputManagerService.this.handleKeyGestureEvent(event); - } - }); + List<Integer> supportedGestures = List.of( + KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP, + KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN, + KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS + ); + im.registerKeyGestureEventHandler(supportedGestures, + (event, focusedToken) -> InputManagerService.this.handleKeyGestureEvent(event)); } @SuppressLint("MissingPermission") @VisibleForTesting - boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event) { + void handleKeyGestureEvent(@NonNull KeyGestureEvent event) { int deviceId = event.getDeviceId(); boolean complete = event.getAction() == KeyGestureEvent.ACTION_GESTURE_COMPLETE && !event.isCancelled(); @@ -2771,20 +2776,20 @@ public class InputManagerService extends IInputManager.Stub if (complete) { mKeyboardBacklightController.incrementKeyboardBacklight(deviceId); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN: if (complete) { mKeyboardBacklightController.decrementKeyboardBacklight(deviceId); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE: // TODO(b/367748270): Add functionality to turn keyboard backlight on/off. - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK: if (complete) { mNative.toggleCapsLock(deviceId); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS: if (complete) { final boolean bounceKeysEnabled = @@ -2792,7 +2797,6 @@ public class InputManagerService extends IInputManager.Stub InputSettings.setAccessibilityBounceKeysThreshold(mContext, bounceKeysEnabled ? 0 : InputSettings.DEFAULT_BOUNCE_KEYS_THRESHOLD_MILLIS); - return true; } break; case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS: @@ -2800,7 +2804,6 @@ public class InputManagerService extends IInputManager.Stub final boolean mouseKeysEnabled = InputSettings.isAccessibilityMouseKeysEnabled( mContext); InputSettings.setAccessibilityMouseKeysEnabled(mContext, !mouseKeysEnabled); - return true; } break; case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS: @@ -2808,7 +2811,6 @@ public class InputManagerService extends IInputManager.Stub final boolean stickyKeysEnabled = InputSettings.isAccessibilityStickyKeysEnabled(mContext); InputSettings.setAccessibilityStickyKeysEnabled(mContext, !stickyKeysEnabled); - return true; } break; case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS: @@ -2817,14 +2819,13 @@ public class InputManagerService extends IInputManager.Stub InputSettings.isAccessibilitySlowKeysEnabled(mContext); InputSettings.setAccessibilitySlowKeysThreshold(mContext, slowKeysEnabled ? 0 : InputSettings.DEFAULT_SLOW_KEYS_THRESHOLD_MILLIS); - return true; } break; default: - return false; - + Log.w(TAG, "Received a key gesture " + event + + " that was not registered by this handler"); + break; } - return false; } // Native callback. @@ -3147,11 +3148,14 @@ public class InputManagerService extends IInputManager.Stub @Override @PermissionManuallyEnforced - public void registerKeyGestureHandler(@NonNull IKeyGestureHandler handler) { + public void registerKeyGestureHandler(int[] keyGesturesToHandle, + @NonNull IKeyGestureHandler handler) { enforceManageKeyGesturePermission(); Objects.requireNonNull(handler); - mKeyGestureController.registerKeyGestureHandler(handler, Binder.getCallingPid()); + Objects.requireNonNull(keyGesturesToHandle); + mKeyGestureController.registerKeyGestureHandler(keyGesturesToHandle, handler, + Binder.getCallingPid()); } @Override diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java index 395c77322c04..5de432e5849b 100644 --- a/services/core/java/com/android/server/input/KeyGestureController.java +++ b/services/core/java/com/android/server/input/KeyGestureController.java @@ -58,6 +58,7 @@ import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseIntArray; import android.view.Display; import android.view.InputDevice; import android.view.KeyCharacterMap; @@ -79,11 +80,11 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayDeque; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; -import java.util.TreeMap; /** * A thread-safe component of {@link InputManagerService} responsible for managing callbacks when a @@ -166,11 +167,14 @@ final class KeyGestureController { private final SparseArray<KeyGestureEventListenerRecord> mKeyGestureEventListenerRecords = new SparseArray<>(); - // List of currently registered key gesture event handler keyed by process pid. The map sorts - // in the order of preference of the handlers, and we prioritize handlers in system server - // over external handlers.. + // Map of currently registered key gesture event handlers keyed by pid. @GuardedBy("mKeyGestureHandlerRecords") - private final TreeMap<Integer, KeyGestureHandlerRecord> mKeyGestureHandlerRecords; + private final SparseArray<KeyGestureHandlerRecord> mKeyGestureHandlerRecords = + new SparseArray<>(); + + // Currently supported key gestures mapped to pid that registered the corresponding handler. + @GuardedBy("mKeyGestureHandlerRecords") + private final SparseIntArray mSupportedKeyGestureToPidMap = new SparseIntArray(); private final ArrayDeque<KeyGestureEvent> mLastHandledEvents = new ArrayDeque<>(); @@ -193,18 +197,6 @@ final class KeyGestureController { mHandler = new Handler(looper, this::handleMessage); mIoHandler = new Handler(ioLooper, this::handleIoMessage); mSystemPid = Process.myPid(); - mKeyGestureHandlerRecords = new TreeMap<>((p1, p2) -> { - if (Objects.equals(p1, p2)) { - return 0; - } - if (p1 == mSystemPid) { - return -1; - } else if (p2 == mSystemPid) { - return 1; - } else { - return Integer.compare(p1, p2); - } - }); mKeyCombinationManager = new KeyCombinationManager(mHandler); mSettingsObserver = new SettingsObserver(mHandler); mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext); @@ -450,8 +442,8 @@ final class KeyGestureController { public void systemRunning() { mSettingsObserver.observe(); - mAppLaunchShortcutManager.systemRunning(); - mInputGestureManager.systemRunning(); + mAppLaunchShortcutManager.init(); + mInputGestureManager.init(mAppLaunchShortcutManager.getBookmarks()); initKeyGestures(); int userId; @@ -465,22 +457,24 @@ final class KeyGestureController { @SuppressLint("MissingPermission") private void initKeyGestures() { InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class)); - im.registerKeyGestureEventHandler((event, focusedToken) -> { - switch (event.getKeyGestureType()) { - case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD: - if (event.getAction() == KeyGestureEvent.ACTION_GESTURE_START) { - mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT); - mHandler.sendMessageDelayed( - mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT), - getAccessibilityShortcutTimeout()); + im.registerKeyGestureEventHandler( + List.of(KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD), + (event, focusedToken) -> { + if (event.getKeyGestureType() + == KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD) { + if (event.getAction() == KeyGestureEvent.ACTION_GESTURE_START) { + mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT), + getAccessibilityShortcutTimeout()); + } else { + mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT); + } } else { - mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT); + Log.w(TAG, "Received a key gesture " + event + + " that was not registered by this handler"); } - return true; - default: - return false; - } - }); + }); } public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) { @@ -590,10 +584,11 @@ final class KeyGestureController { return true; } if (result.appLaunchData() != null) { - return handleKeyGesture(deviceId, new int[]{keyCode}, metaState, + handleKeyGesture(deviceId, new int[]{keyCode}, metaState, KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, - KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0, result.appLaunchData()); + KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken, /* flags = */ + 0, result.appLaunchData()); + return true; } // Handle system shortcuts @@ -601,11 +596,11 @@ final class KeyGestureController { InputGestureData systemShortcut = mInputGestureManager.getSystemShortcutForKeyEvent( event); if (systemShortcut != null) { - return handleKeyGesture(deviceId, new int[]{keyCode}, metaState, + handleKeyGesture(deviceId, new int[]{keyCode}, metaState, systemShortcut.getAction().keyGestureType(), - KeyGestureEvent.ACTION_GESTURE_COMPLETE, - displayId, focusedToken, /* flags = */0, - systemShortcut.getAction().appLaunchData()); + KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, + focusedToken, /* flags = */0, systemShortcut.getAction().appLaunchData()); + return true; } } @@ -687,11 +682,11 @@ final class KeyGestureController { return true; case KeyEvent.KEYCODE_SEARCH: if (firstDown && mSearchKeyBehavior == SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY) { - return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, + handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken, /* flags = */0, /* appLaunchData = */null); - + return true; } break; case KeyEvent.KEYCODE_SETTINGS: @@ -782,11 +777,12 @@ final class KeyGestureController { if (KeyEvent.metaStateHasModifiers( shiftlessModifiers, KeyEvent.META_ALT_ON)) { mPendingHideRecentSwitcher = true; - return handleKeyGesture(deviceId, new int[]{keyCode}, + handleKeyGesture(deviceId, new int[]{keyCode}, KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER, KeyGestureEvent.ACTION_GESTURE_START, displayId, focusedToken, /* flags = */0, /* appLaunchData = */null); + return true; } } } @@ -803,21 +799,23 @@ final class KeyGestureController { } else { if (mPendingHideRecentSwitcher) { mPendingHideRecentSwitcher = false; - return handleKeyGesture(deviceId, new int[]{KeyEvent.KEYCODE_TAB}, + handleKeyGesture(deviceId, new int[]{KeyEvent.KEYCODE_TAB}, KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken, /* flags = */0, /* appLaunchData = */null); + return true; } // Toggle Caps Lock on META-ALT. if (mPendingCapsLockToggle) { mPendingCapsLockToggle = false; - return handleKeyGesture(deviceId, new int[]{KeyEvent.KEYCODE_META_LEFT, + handleKeyGesture(deviceId, new int[]{KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ALT_LEFT}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken, /* flags = */0, /* appLaunchData = */null); + return true; } } break; @@ -885,11 +883,11 @@ final class KeyGestureController { if (customGesture == null) { return false; } - return handleKeyGesture(deviceId, new int[]{keyCode}, metaState, + handleKeyGesture(deviceId, new int[]{keyCode}, metaState, customGesture.getAction().keyGestureType(), - KeyGestureEvent.ACTION_GESTURE_COMPLETE, - displayId, focusedToken, /* flags = */0, - customGesture.getAction().appLaunchData()); + KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken, + /* flags = */0, customGesture.getAction().appLaunchData()); + return true; } return false; } @@ -908,7 +906,7 @@ final class KeyGestureController { // Handle keyboard layout switching. (CTRL + SPACE) if (KeyEvent.metaStateHasModifiers(metaState & ~KeyEvent.META_SHIFT_MASK, KeyEvent.META_CTRL_ON)) { - return handleKeyGesture(deviceId, new int[]{keyCode}, + handleKeyGesture(deviceId, new int[]{keyCode}, KeyEvent.META_CTRL_ON | (event.isShiftPressed() ? KeyEvent.META_SHIFT_ON : 0), KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH, @@ -921,7 +919,7 @@ final class KeyGestureController { if (down && KeyEvent.metaStateHasModifiers(metaState, KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON)) { // Intercept the Accessibility keychord (CTRL + ALT + Z) for keyboard users. - return handleKeyGesture(deviceId, new int[]{keyCode}, + handleKeyGesture(deviceId, new int[]{keyCode}, KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, @@ -930,7 +928,7 @@ final class KeyGestureController { break; case KeyEvent.KEYCODE_SYSRQ: if (down && repeatCount == 0) { - return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, + handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken, /* flags = */0, /* appLaunchData = */null); @@ -938,7 +936,7 @@ final class KeyGestureController { break; case KeyEvent.KEYCODE_ESCAPE: if (down && KeyEvent.metaStateHasNoModifiers(metaState) && repeatCount == 0) { - return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, + handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken, /* flags = */0, /* appLaunchData = */null); @@ -964,29 +962,31 @@ final class KeyGestureController { } @VisibleForTesting - boolean handleKeyGesture(int deviceId, int[] keycodes, int modifierState, + void handleKeyGesture(int deviceId, int[] keycodes, int modifierState, @KeyGestureEvent.KeyGestureType int gestureType, int action, int displayId, @Nullable IBinder focusedToken, int flags, @Nullable AppLaunchData appLaunchData) { - return handleKeyGesture(createKeyGestureEvent(deviceId, keycodes, - modifierState, gestureType, action, displayId, flags, appLaunchData), focusedToken); + handleKeyGesture( + createKeyGestureEvent(deviceId, keycodes, modifierState, gestureType, action, + displayId, flags, appLaunchData), focusedToken); } - private boolean handleKeyGesture(AidlKeyGestureEvent event, @Nullable IBinder focusedToken) { + private void handleKeyGesture(AidlKeyGestureEvent event, @Nullable IBinder focusedToken) { if (mVisibleBackgroundUsersEnabled && event.displayId != DEFAULT_DISPLAY && shouldIgnoreGestureEventForVisibleBackgroundUser(event.gestureType, event.displayId)) { - return false; + return; } synchronized (mKeyGestureHandlerRecords) { - for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) { - if (handler.handleKeyGesture(event, focusedToken)) { - Message msg = Message.obtain(mHandler, MSG_NOTIFY_KEY_GESTURE_EVENT, event); - mHandler.sendMessage(msg); - return true; - } + int index = mSupportedKeyGestureToPidMap.indexOfKey(event.gestureType); + if (index < 0) { + Log.i(TAG, "Key gesture: " + event.gestureType + " is not supported"); + return; } + int pid = mSupportedKeyGestureToPidMap.valueAt(index); + mKeyGestureHandlerRecords.get(pid).handleKeyGesture(event, focusedToken); + Message msg = Message.obtain(mHandler, MSG_NOTIFY_KEY_GESTURE_EVENT, event); + mHandler.sendMessage(msg); } - return false; } private boolean shouldIgnoreGestureEventForVisibleBackgroundUser( @@ -1285,12 +1285,23 @@ final class KeyGestureController { /** Register the key gesture event handler for a process. */ @BinderThread - public void registerKeyGestureHandler(IKeyGestureHandler handler, int pid) { + public void registerKeyGestureHandler(int[] keyGesturesToHandle, IKeyGestureHandler handler, + int pid) { synchronized (mKeyGestureHandlerRecords) { if (mKeyGestureHandlerRecords.get(pid) != null) { throw new IllegalStateException("The calling process has already registered " + "a KeyGestureHandler."); } + if (keyGesturesToHandle.length == 0) { + throw new IllegalArgumentException("No key gestures provided for pid = " + pid); + } + for (int gestureType : keyGesturesToHandle) { + if (mSupportedKeyGestureToPidMap.indexOfKey(gestureType) >= 0) { + throw new IllegalArgumentException( + "Key gesture " + gestureType + " is already registered by pid = " + + mSupportedKeyGestureToPidMap.get(gestureType)); + } + } KeyGestureHandlerRecord record = new KeyGestureHandlerRecord(pid, handler); try { handler.asBinder().linkToDeath(record, 0); @@ -1298,6 +1309,9 @@ final class KeyGestureController { throw new RuntimeException(ex); } mKeyGestureHandlerRecords.put(pid, record); + for (int gestureType : keyGesturesToHandle) { + mSupportedKeyGestureToPidMap.put(gestureType, pid); + } } } @@ -1315,7 +1329,7 @@ final class KeyGestureController { + "KeyGestureHandler."); } record.mKeyGestureHandler.asBinder().unlinkToDeath(record, 0); - mKeyGestureHandlerRecords.remove(pid); + onKeyGestureHandlerRemoved(pid); } } @@ -1328,9 +1342,14 @@ final class KeyGestureController { return mAppLaunchShortcutManager.getBookmarks(); } - private void onKeyGestureHandlerDied(int pid) { + private void onKeyGestureHandlerRemoved(int pid) { synchronized (mKeyGestureHandlerRecords) { mKeyGestureHandlerRecords.remove(pid); + for (int i = mSupportedKeyGestureToPidMap.size() - 1; i >= 0; i--) { + if (mSupportedKeyGestureToPidMap.valueAt(i) == pid) { + mSupportedKeyGestureToPidMap.removeAt(i); + } + } } } @@ -1369,18 +1388,17 @@ final class KeyGestureController { if (DEBUG) { Slog.d(TAG, "Key gesture event handler for pid " + mPid + " died."); } - onKeyGestureHandlerDied(mPid); + onKeyGestureHandlerRemoved(mPid); } - public boolean handleKeyGesture(AidlKeyGestureEvent event, IBinder focusedToken) { + public void handleKeyGesture(AidlKeyGestureEvent event, IBinder focusedToken) { try { - return mKeyGestureHandler.handleKeyGesture(event, focusedToken); + mKeyGestureHandler.handleKeyGesture(event, focusedToken); } catch (RemoteException ex) { Slog.w(TAG, "Failed to send key gesture to process " + mPid + ", assuming it died.", ex); binderDied(); } - return false; } } @@ -1479,18 +1497,21 @@ final class KeyGestureController { } } ipw.println("}"); - ipw.print("mKeyGestureHandlerRecords = {"); synchronized (mKeyGestureHandlerRecords) { - int i = mKeyGestureHandlerRecords.size() - 1; - for (int processId : mKeyGestureHandlerRecords.keySet()) { - ipw.print(processId); - if (i > 0) { + ipw.print("mKeyGestureHandlerRecords = {"); + int size = mKeyGestureHandlerRecords.size(); + for (int i = 0; i < size; i++) { + int pid = mKeyGestureHandlerRecords.keyAt(i); + ipw.print(pid); + if (i < size - 1) { ipw.print(", "); } - i--; } + ipw.println("}"); + ipw.println("mSupportedKeyGestures = " + Arrays.toString( + mSupportedKeyGestureToPidMap.copyKeys())); } - ipw.println("}"); + ipw.decreaseIndent(); ipw.println("Last handled KeyGestureEvents: "); ipw.increaseIndent(); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index fde9165a84c6..2066dbc87a0d 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -1826,8 +1826,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @NonNull UserData userData) { final int userId = userData.mUserId; if (userData.mCurClient == client) { - hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */, - SoftInputShowHideReason.HIDE_REMOVE_CLIENT, userId); + if (Flags.refactorInsetsController()) { + final var statsToken = createStatsTokenForFocusedClient(false /* show */, + SoftInputShowHideReason.HIDE_REMOVE_CLIENT, userId); + setImeVisibilityOnFocusedWindowClient(false, userData, statsToken); + } else { + hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */, + SoftInputShowHideReason.HIDE_REMOVE_CLIENT, userId); + } if (userData.mBoundToMethod) { userData.mBoundToMethod = false; final var userBindingController = userData.mBindingController; @@ -2097,8 +2103,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } if (visibilityStateComputer.getImePolicy().isImeHiddenByDisplayPolicy()) { - hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */, - SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE, userId); + if (Flags.refactorInsetsController()) { + final var statsToken = createStatsTokenForFocusedClient(false /* show */, + SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE, userId); + setImeVisibilityOnFocusedWindowClient(false, userData, statsToken); + } else { + hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */, + SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE, userId); + } return InputBindResult.NO_IME; } @@ -3855,8 +3867,17 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. Slog.w(TAG, "If you need to impersonate a foreground user/profile from" + " a background user, use EditorInfo.targetInputMethodUser with" + " INTERACT_ACROSS_USERS_FULL permission."); - hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, - 0 /* flags */, SoftInputShowHideReason.HIDE_INVALID_USER, userId); + + if (Flags.refactorInsetsController()) { + final var statsToken = createStatsTokenForFocusedClient( + false /* show */, SoftInputShowHideReason.HIDE_INVALID_USER, + userId); + setImeVisibilityOnFocusedWindowClient(false, userData, statsToken); + } else { + hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, + 0 /* flags */, SoftInputShowHideReason.HIDE_INVALID_USER, + userId); + } return InputBindResult.INVALID_USER; } @@ -4993,7 +5014,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. setImeVisibilityOnFocusedWindowClient(false, userData, null /* TODO(b/353463205) check statsToken */); } else { - hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */, reason, userId); } @@ -6688,8 +6708,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); final var userData = getUserData(userId); if (Flags.refactorInsetsController()) { - setImeVisibilityOnFocusedWindowClient(false, userData, - null /* TODO(b329229469) initialize statsToken here? */); + final var statsToken = createStatsTokenForFocusedClient(false /* show */, + SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND, userId); + setImeVisibilityOnFocusedWindowClient(false, userData, statsToken); } else { hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */, diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java index ccb9e3ea5cbe..bbf7732c9596 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java @@ -33,6 +33,7 @@ import android.hardware.contexthub.MessageDeliveryStatus; import android.hardware.contexthub.Reason; import android.hardware.location.ContextHubTransaction; import android.hardware.location.IContextHubTransactionCallback; +import android.hardware.location.NanoAppState; import android.os.Binder; import android.os.IBinder; import android.os.PowerManager; @@ -48,6 +49,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -182,8 +184,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub long expiryMillis = RELIABLE_MESSAGE_DUPLICATE_DETECTION_TIMEOUT.toMillis(); if (nowMillis >= nextEntry.getValue() + expiryMillis) { iterator.remove(); + } else { + // Safe to break since LinkedHashMap is insertion-ordered, so the next entry + // will have a later timestamp and will not be expired. + break; } - break; } return mRxMessageHistoryMap.containsKey(message.getMessageSequenceNumber()); @@ -276,6 +281,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub int sessionId = mEndpointManager.reserveSessionId(); EndpointInfo halEndpointInfo = ContextHubServiceUtil.convertHalEndpointInfo(destination); + Log.d(TAG, "openSession: sessionId=" + sessionId); synchronized (mOpenSessionLock) { try { @@ -301,6 +307,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub throw new IllegalArgumentException( "Unknown session ID in closeSession: id=" + sessionId); } + Log.d(TAG, "closeSession: sessionId=" + sessionId + " reason=" + reason); mEndpointManager.halCloseEndpointSession( sessionId, ContextHubServiceUtil.toHalReason(reason)); } @@ -373,12 +380,43 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub try { mHubInterface.sendMessageToEndpoint(sessionId, halMessage); } catch (RemoteException e) { - Log.w(TAG, "Exception while sending message on session " + sessionId, e); + Log.e( + TAG, + "Exception while sending message on session " + + sessionId + + ", closing session", + e); + notifySessionClosedToBoth(sessionId, Reason.UNSPECIFIED); } } else { + IContextHubTransactionCallback wrappedCallback = + new IContextHubTransactionCallback.Stub() { + @Override + public void onQueryResponse(int result, List<NanoAppState> appStates) + throws RemoteException { + Log.w(TAG, "Unexpected onQueryResponse callback"); + } + + @Override + public void onTransactionComplete(int result) throws RemoteException { + callback.onTransactionComplete(result); + if (result != ContextHubTransaction.RESULT_SUCCESS) { + Log.e( + TAG, + "Failed to send reliable message " + + message + + ", closing session"); + notifySessionClosedToBoth(sessionId, Reason.UNSPECIFIED); + } + } + }; ContextHubServiceTransaction transaction = mTransactionManager.createSessionMessageTransaction( - mHubInterface, sessionId, halMessage, mPackageName, callback); + mHubInterface, + sessionId, + halMessage, + mPackageName, + wrappedCallback); try { mTransactionManager.addTransaction(transaction); info.setReliableMessagePending(transaction.getMessageSequenceNumber()); @@ -445,10 +483,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub int id = mSessionMap.keyAt(i); HubEndpointInfo target = mSessionMap.get(id).getRemoteEndpointInfo(); if (!hasEndpointPermissions(target)) { - mEndpointManager.halCloseEndpointSessionNoThrow( - id, Reason.PERMISSION_DENIED); - onCloseEndpointSession(id, Reason.PERMISSION_DENIED); - // Resource cleanup is done in onCloseEndpointSession + notifySessionClosedToBoth(id, Reason.PERMISSION_DENIED); } } } @@ -532,8 +567,17 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub /* package */ void onMessageReceived(int sessionId, HubMessage message) { byte errorCode = onMessageReceivedInternal(sessionId, message); - if (errorCode != ErrorCode.OK && message.isResponseRequired()) { - sendMessageDeliveryStatus(sessionId, message.getMessageSequenceNumber(), errorCode); + if (errorCode != ErrorCode.OK) { + Log.e(TAG, "Failed to send message to endpoint: " + message + ", closing session"); + if (message.isResponseRequired()) { + sendMessageDeliveryStatus(sessionId, message.getMessageSequenceNumber(), errorCode); + } else { + notifySessionClosedToBoth( + sessionId, + (errorCode == ErrorCode.PERMISSION_DENIED) + ? Reason.PERMISSION_DENIED + : Reason.UNSPECIFIED); + } } } @@ -800,4 +844,16 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub + "-0x" + Long.toHexString(endpoint.getIdentifier().getEndpoint())); } + + /** + * Notifies to both the HAL and the app that a session has been closed. + * + * @param sessionId The ID of the session that was closed + * @param halReason The HAL reason for closing the session + */ + private void notifySessionClosedToBoth(int sessionId, byte halReason) { + Log.d(TAG, "notifySessionClosedToBoth: sessionId=" + sessionId + ", reason=" + halReason); + mEndpointManager.halCloseEndpointSessionNoThrow(sessionId, halReason); + onCloseEndpointSession(sessionId, halReason); + } } diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index 58cf29b59961..c174451e8f5b 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -192,9 +192,15 @@ public class MediaSessionService extends SystemService implements Monitor { private final Map<Integer, Set<MediaSessionRecordImpl>> mUserEngagedSessionsForFgs = new HashMap<>(); - /* Maps uid with all media notifications associated to it */ + /** + * Maps UIDs to their associated media notifications: UID -> (Notification ID -> + * {@link android.service.notification.StatusBarNotification}). + * Each UID maps to a collection of notifications, identified by their + * {@link android.service.notification.StatusBarNotification#getId()}. + */ @GuardedBy("mLock") - private final Map<Integer, Set<StatusBarNotification>> mMediaNotifications = new HashMap<>(); + private final Map<Integer, Map<String, StatusBarNotification>> mMediaNotifications = + new HashMap<>(); // The FullUserRecord of the current users. (i.e. The foreground user that isn't a profile) // It's always not null after the MediaSessionService is started. @@ -737,7 +743,8 @@ public class MediaSessionService extends SystemService implements Monitor { } synchronized (mLock) { int uid = mediaSessionRecord.getUid(); - for (StatusBarNotification sbn : mMediaNotifications.getOrDefault(uid, Set.of())) { + for (StatusBarNotification sbn : mMediaNotifications.getOrDefault(uid, + Map.of()).values()) { if (mediaSessionRecord.isLinkedToNotification(sbn.getNotification())) { setFgsActiveLocked(mediaSessionRecord, sbn); return; @@ -771,7 +778,7 @@ public class MediaSessionService extends SystemService implements Monitor { int uid, MediaSessionRecordImpl record) { synchronized (mLock) { for (StatusBarNotification sbn : - mMediaNotifications.getOrDefault(uid, Set.of())) { + mMediaNotifications.getOrDefault(uid, Map.of()).values()) { if (record.isLinkedToNotification(sbn.getNotification())) { return sbn; } @@ -794,7 +801,8 @@ public class MediaSessionService extends SystemService implements Monitor { for (MediaSessionRecordImpl record : mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) { for (StatusBarNotification sbn : - mMediaNotifications.getOrDefault(uid, Set.of())) { + mMediaNotifications.getOrDefault(uid, Map.of()).values()) { + // if (record.isLinkedToNotification(sbn.getNotification())) { // A user engaged session linked with a media notification is found. // We shouldn't call stop FGS in this case. @@ -3262,8 +3270,12 @@ public class MediaSessionService extends SystemService implements Monitor { return; } synchronized (mLock) { - mMediaNotifications.putIfAbsent(uid, new HashSet<>()); - mMediaNotifications.get(uid).add(sbn); + Map<String, StatusBarNotification> notifications = mMediaNotifications.get(uid); + if (notifications == null) { + notifications = new HashMap<>(); + mMediaNotifications.put(uid, notifications); + } + notifications.put(sbn.getKey(), sbn); MediaSessionRecordImpl userEngagedRecord = getUserEngagedMediaSessionRecordForNotification(uid, postedNotification); if (userEngagedRecord != null) { @@ -3287,10 +3299,10 @@ public class MediaSessionService extends SystemService implements Monitor { return; } synchronized (mLock) { - Set<StatusBarNotification> uidMediaNotifications = mMediaNotifications.get(uid); - if (uidMediaNotifications != null) { - uidMediaNotifications.remove(sbn); - if (uidMediaNotifications.isEmpty()) { + Map<String, StatusBarNotification> notifications = mMediaNotifications.get(uid); + if (notifications != null) { + notifications.remove(sbn.getKey()); + if (notifications.isEmpty()) { mMediaNotifications.remove(uid); } } diff --git a/services/core/java/com/android/server/media/projection/Android.bp b/services/core/java/com/android/server/media/projection/Android.bp new file mode 100644 index 000000000000..114be7d20d5b --- /dev/null +++ b/services/core/java/com/android/server/media/projection/Android.bp @@ -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. + +team { + name: "trendy_team_media_projection", + + // go/trendy/manage/engineers/6362947212640256 + trendy_team_id: "6362947212640256", +} diff --git a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java index cf8b703a2641..05aac5587c2c 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java @@ -18,13 +18,20 @@ package com.android.server.media.quality; import android.content.ContentValues; import android.database.Cursor; +import android.hardware.tv.mediaquality.ColorRange; +import android.hardware.tv.mediaquality.ColorSpace; +import android.hardware.tv.mediaquality.ColorTemperature; import android.hardware.tv.mediaquality.DolbyAudioProcessing; import android.hardware.tv.mediaquality.DtsVirtualX; +import android.hardware.tv.mediaquality.Gamma; import android.hardware.tv.mediaquality.ParameterDefaultValue; import android.hardware.tv.mediaquality.ParameterName; import android.hardware.tv.mediaquality.ParameterRange; import android.hardware.tv.mediaquality.PictureParameter; +import android.hardware.tv.mediaquality.PictureQualityEventType; +import android.hardware.tv.mediaquality.QualityLevel; import android.hardware.tv.mediaquality.SoundParameter; +import android.media.quality.MediaQualityContract; import android.media.quality.MediaQualityContract.BaseParameters; import android.media.quality.MediaQualityContract.PictureQuality; import android.media.quality.MediaQualityContract.SoundQuality; @@ -371,7 +378,7 @@ public final class MediaQualityUtils { } List<PictureParameter> pictureParams = new ArrayList<>(); if (params.containsKey(PictureQuality.PARAMETER_BRIGHTNESS)) { - pictureParams.add(PictureParameter.brightness(params.getLong( + pictureParams.add(PictureParameter.brightness((float) params.getDouble( PictureQuality.PARAMETER_BRIGHTNESS))); params.remove(PictureQuality.PARAMETER_BRIGHTNESS); } @@ -441,28 +448,46 @@ public final class MediaQualityUtils { params.remove(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN); } if (params.containsKey(PictureQuality.PARAMETER_NOISE_REDUCTION)) { - pictureParams.add(PictureParameter.noiseReduction( - (byte) params.getInt(PictureQuality.PARAMETER_NOISE_REDUCTION))); + String noiseReductionString = params.getString( + PictureQuality.PARAMETER_NOISE_REDUCTION); + if (noiseReductionString != null) { + byte noiseReductionByte = mapQualityLevel(noiseReductionString); + pictureParams.add(PictureParameter.noiseReduction(noiseReductionByte)); + } params.remove(PictureQuality.PARAMETER_NOISE_REDUCTION); } if (params.containsKey(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION)) { - pictureParams.add(PictureParameter.mpegNoiseReduction( - (byte) params.getInt(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION))); + String mpegNoiseReductionString = params.getString( + PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION); + if (mpegNoiseReductionString != null) { + byte mpegNoiseReductionByte = mapQualityLevel(mpegNoiseReductionString); + pictureParams.add(PictureParameter.mpegNoiseReduction(mpegNoiseReductionByte)); + } params.remove(PictureQuality.PARAMETER_MPEG_NOISE_REDUCTION); } if (params.containsKey(PictureQuality.PARAMETER_FLESH_TONE)) { - pictureParams.add(PictureParameter.fleshTone( - (byte) params.getInt(PictureQuality.PARAMETER_FLESH_TONE))); + String fleshToneString = params.getString(PictureQuality.PARAMETER_FLESH_TONE); + if (fleshToneString != null) { + byte fleshToneByte = mapQualityLevel(fleshToneString); + pictureParams.add(PictureParameter.fleshTone(fleshToneByte)); + } params.remove(PictureQuality.PARAMETER_FLESH_TONE); } if (params.containsKey(PictureQuality.PARAMETER_DECONTOUR)) { - pictureParams.add(PictureParameter.deContour( - (byte) params.getInt(PictureQuality.PARAMETER_DECONTOUR))); + String decontourString = params.getString(PictureQuality.PARAMETER_DECONTOUR); + if (decontourString != null) { + byte decontourByte = mapQualityLevel(decontourString); + pictureParams.add(PictureParameter.deContour(decontourByte)); + } params.remove(PictureQuality.PARAMETER_DECONTOUR); } if (params.containsKey(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL)) { - pictureParams.add(PictureParameter.dynamicLumaControl( - (byte) params.getInt(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL))); + String dynamicLunaControlString = params.getString( + PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL); + if (dynamicLunaControlString != null) { + byte dynamicLunaControlByte = mapQualityLevel(dynamicLunaControlString); + pictureParams.add(PictureParameter.dynamicLumaControl(dynamicLunaControlByte)); + } params.remove(PictureQuality.PARAMETER_DYNAMIC_LUMA_CONTROL); } if (params.containsKey(PictureQuality.PARAMETER_FILM_MODE)) { @@ -481,9 +506,48 @@ public final class MediaQualityUtils { params.remove(PictureQuality.PARAMETER_COLOR_TUNE); } if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE)) { - pictureParams.add(PictureParameter.colorTemperature( - (byte) params.getInt( - PictureQuality.PARAMETER_COLOR_TEMPERATURE))); + String colorTemperatureString = params.getString( + PictureQuality.PARAMETER_COLOR_TEMPERATURE); + if (colorTemperatureString != null) { + byte colorTemperatureByte; + switch (colorTemperatureString) { + case MediaQualityContract.COLOR_TEMP_USER: + colorTemperatureByte = ColorTemperature.USER; + break; + case MediaQualityContract.COLOR_TEMP_COOL: + colorTemperatureByte = ColorTemperature.COOL; + break; + case MediaQualityContract.COLOR_TEMP_STANDARD: + colorTemperatureByte = ColorTemperature.STANDARD; + break; + case MediaQualityContract.COLOR_TEMP_WARM: + colorTemperatureByte = ColorTemperature.WARM; + break; + case MediaQualityContract.COLOR_TEMP_USER_HDR10PLUS: + colorTemperatureByte = ColorTemperature.USER_HDR10PLUS; + break; + case MediaQualityContract.COLOR_TEMP_COOL_HDR10PLUS: + colorTemperatureByte = ColorTemperature.COOL_HDR10PLUS; + break; + case MediaQualityContract.COLOR_TEMP_STANDARD_HDR10PLUS: + colorTemperatureByte = ColorTemperature.STANDARD_HDR10PLUS; + break; + case MediaQualityContract.COLOR_TEMP_WARM_HDR10PLUS: + colorTemperatureByte = ColorTemperature.WARM_HDR10PLUS; + break; + case MediaQualityContract.COLOR_TEMP_FMMSDR: + colorTemperatureByte = ColorTemperature.FMMSDR; + break; + case MediaQualityContract.COLOR_TEMP_FMMHDR: + colorTemperatureByte = ColorTemperature.FMMHDR; + break; + default: + colorTemperatureByte = ColorTemperature.STANDARD; + Log.e("PictureParams", "Invalid color_temp string: " + + colorTemperatureString); + } + pictureParams.add(PictureParameter.colorTemperature(colorTemperatureByte)); + } params.remove(PictureQuality.PARAMETER_COLOR_TEMPERATURE); } if (params.containsKey(PictureQuality.PARAMETER_GLOBAL_DIMMING)) { @@ -517,8 +581,26 @@ public final class MediaQualityUtils { params.remove(PictureQuality.PARAMETER_COLOR_TUNER_BLUE_GAIN); } if (params.containsKey(PictureQuality.PARAMETER_LEVEL_RANGE)) { - pictureParams.add(PictureParameter.levelRange( - (byte) params.getInt(PictureQuality.PARAMETER_LEVEL_RANGE))); + String levelRangeString = params.getString(PictureQuality.PARAMETER_LEVEL_RANGE); + if (levelRangeString != null) { + byte levelRangeByte; + switch (levelRangeString) { + case "AUTO": + levelRangeByte = ColorRange.AUTO; + break; + case "LIMITED": + levelRangeByte = ColorRange.LIMITED; + break; + case "FULL": + levelRangeByte = ColorRange.FULL; + break; + default: + levelRangeByte = ColorRange.AUTO; + Log.e("PictureParams", "Invalid color_range string: " + + levelRangeString); + } + pictureParams.add(PictureParameter.levelRange(levelRangeByte)); + } params.remove(PictureQuality.PARAMETER_LEVEL_RANGE); } if (params.containsKey(PictureQuality.PARAMETER_GAMUT_MAPPING)) { @@ -547,13 +629,61 @@ public final class MediaQualityUtils { params.remove(PictureQuality.PARAMETER_CVRR); } if (params.containsKey(PictureQuality.PARAMETER_HDMI_RGB_RANGE)) { - pictureParams.add(PictureParameter.hdmiRgbRange( - (byte) params.getInt(PictureQuality.PARAMETER_HDMI_RGB_RANGE))); + String hdmiRgbRangeString = params.getString(PictureQuality.PARAMETER_HDMI_RGB_RANGE); + if (hdmiRgbRangeString != null) { + byte hdmiRgbRangeByte; + switch (hdmiRgbRangeString) { + case "AUTO": + hdmiRgbRangeByte = ColorRange.AUTO; + break; + case "LIMITED": + hdmiRgbRangeByte = ColorRange.LIMITED; + break; + case "FULL": + hdmiRgbRangeByte = ColorRange.FULL; + break; + default: + hdmiRgbRangeByte = ColorRange.AUTO; + Log.e("PictureParams", "Invalid hdmi_rgb_range string: " + + hdmiRgbRangeByte); + } + pictureParams.add(PictureParameter.hdmiRgbRange(hdmiRgbRangeByte)); + } params.remove(PictureQuality.PARAMETER_HDMI_RGB_RANGE); } if (params.containsKey(PictureQuality.PARAMETER_COLOR_SPACE)) { - pictureParams.add(PictureParameter.colorSpace( - (byte) params.getInt(PictureQuality.PARAMETER_COLOR_SPACE))); + String colorSpaceString = params.getString(PictureQuality.PARAMETER_COLOR_SPACE); + if (colorSpaceString != null) { + byte colorSpaceByte; + switch (colorSpaceString) { + case "AUTO": + colorSpaceByte = ColorSpace.AUTO; + break; + case "S_RGB_BT_709": + colorSpaceByte = ColorSpace.S_RGB_BT_709; + break; + case "DCI": + colorSpaceByte = ColorSpace.DCI; + break; + case "ADOBE_RGB": + colorSpaceByte = ColorSpace.ADOBE_RGB; + break; + case "BT2020": + colorSpaceByte = ColorSpace.BT2020; + break; + case "ON": + colorSpaceByte = ColorSpace.ON; + break; + case "OFF": + colorSpaceByte = ColorSpace.OFF; + break; + default: + colorSpaceByte = ColorSpace.OFF; + Log.e("PictureParams", "Invalid color_space string: " + + colorSpaceString); + } + pictureParams.add(PictureParameter.colorSpace(colorSpaceByte)); + } params.remove(PictureQuality.PARAMETER_COLOR_SPACE); } if (params.containsKey(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_NITS)) { @@ -567,8 +697,25 @@ public final class MediaQualityUtils { params.remove(PictureQuality.PARAMETER_PANEL_INIT_MAX_LUMINCE_VALID); } if (params.containsKey(PictureQuality.PARAMETER_GAMMA)) { - pictureParams.add(PictureParameter.gamma( - (byte) params.getInt(PictureQuality.PARAMETER_GAMMA))); + String gammaString = params.getString(PictureQuality.PARAMETER_GAMMA); + if (gammaString != null) { + byte gammaByte; + switch (gammaString) { + case "DARK": + gammaByte = Gamma.DARK; + break; + case "MIDDLE": + gammaByte = Gamma.MIDDLE; + break; + case "BRIGHT": + gammaByte = Gamma.BRIGHT; + break; + default: + gammaByte = Gamma.DARK; + Log.e("PictureParams", "Invalid gamma string: " + gammaString); + } + pictureParams.add(PictureParameter.gamma(gammaByte)); + } params.remove(PictureQuality.PARAMETER_GAMMA); } if (params.containsKey(PictureQuality.PARAMETER_COLOR_TEMPERATURE_RED_OFFSET)) { @@ -602,13 +749,19 @@ public final class MediaQualityUtils { params.remove(PictureQuality.PARAMETER_ELEVEN_POINT_BLUE); } if (params.containsKey(PictureQuality.PARAMETER_LOW_BLUE_LIGHT)) { - pictureParams.add(PictureParameter.lowBlueLight( - (byte) params.getInt(PictureQuality.PARAMETER_LOW_BLUE_LIGHT))); + String lowBlueLightString = params.getString(PictureQuality.PARAMETER_LOW_BLUE_LIGHT); + if (lowBlueLightString != null) { + byte lowBlueLightByte = mapQualityLevel(lowBlueLightString); + pictureParams.add(PictureParameter.lowBlueLight(lowBlueLightByte)); + } params.remove(PictureQuality.PARAMETER_LOW_BLUE_LIGHT); } if (params.containsKey(PictureQuality.PARAMETER_LD_MODE)) { - pictureParams.add(PictureParameter.LdMode( - (byte) params.getInt(PictureQuality.PARAMETER_LD_MODE))); + String ldModeString = params.getString(PictureQuality.PARAMETER_LD_MODE); + if (ldModeString != null) { + byte ldModeByte = mapQualityLevel(ldModeString); + pictureParams.add(PictureParameter.LdMode(ldModeByte)); + } params.remove(PictureQuality.PARAMETER_LD_MODE); } if (params.containsKey(PictureQuality.PARAMETER_OSD_RED_GAIN)) { @@ -767,8 +920,44 @@ public final class MediaQualityUtils { params.remove(PictureQuality.PARAMETER_COLOR_TUNER_LUMINANCE_FLESH); } if (params.containsKey(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE)) { - pictureParams.add(PictureParameter.pictureQualityEventType( - (byte) params.getInt(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE))); + String pictureQualityEventTypeString = params.getString( + PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE); + if (pictureQualityEventTypeString != null) { + byte pictureQualityEventTypeByte; + switch (pictureQualityEventTypeString) { + case "NONE": + pictureQualityEventTypeByte = PictureQualityEventType.NONE; + break; + case "BBD_RESULT": + pictureQualityEventTypeByte = PictureQualityEventType.BBD_RESULT; + break; + case "VIDEO_DELAY_CHANGE": + pictureQualityEventTypeByte = PictureQualityEventType.VIDEO_DELAY_CHANGE; + break; + case "CAPTUREPOINT_INFO_CHANGE": + pictureQualityEventTypeByte = + PictureQualityEventType.CAPTUREPOINT_INFO_CHANGE; + break; + case "VIDEOPATH_CHANGE": + pictureQualityEventTypeByte = PictureQualityEventType.VIDEOPATH_CHANGE; + break; + case "EXTRA_FRAME_CHANGE": + pictureQualityEventTypeByte = PictureQualityEventType.EXTRA_FRAME_CHANGE; + break; + case "DOLBY_IQ_CHANGE": + pictureQualityEventTypeByte = PictureQualityEventType.DOLBY_IQ_CHANGE; + break; + case "DOLBY_APO_CHANGE": + pictureQualityEventTypeByte = PictureQualityEventType.DOLBY_APO_CHANGE; + break; + default: + pictureQualityEventTypeByte = PictureQualityEventType.NONE; + Log.e("PictureParams", "Invalid event type string: " + + pictureQualityEventTypeString); + } + pictureParams.add( + PictureParameter.pictureQualityEventType(pictureQualityEventTypeByte)); + } params.remove(PictureQuality.PARAMETER_PICTURE_QUALITY_EVENT_TYPE); } return pictureParams.toArray(new PictureParameter[0]); @@ -1657,6 +1846,19 @@ public final class MediaQualityUtils { return colIndex != -1 ? cursor.getString(colIndex) : null; } + private static byte mapQualityLevel(String qualityLevel) { + return switch (qualityLevel) { + case MediaQualityContract.LEVEL_OFF -> QualityLevel.OFF; + case MediaQualityContract.LEVEL_LOW -> QualityLevel.LOW; + case MediaQualityContract.LEVEL_MEDIUM -> QualityLevel.MEDIUM; + case MediaQualityContract.LEVEL_HIGH -> QualityLevel.HIGH; + default -> { + Log.e("PictureParams", "Invalid noise_reduction string: " + qualityLevel); + yield QualityLevel.OFF; + } + }; + } + private MediaQualityUtils() { } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 78554bdcf6aa..06fc9b083086 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -14544,23 +14544,23 @@ public class NotificationManagerService extends SystemService { */ private class NotificationTrampolineCallback implements BackgroundActivityStartCallback { @Override - public boolean isActivityStartAllowed(Collection<IBinder> tokens, int uid, - String packageName) { + public BackgroundActivityStartCallbackResult isActivityStartAllowed( + Collection<IBinder> tokens, int uid, String packageName) { checkArgument(!tokens.isEmpty()); for (IBinder token : tokens) { if (token != ALLOWLIST_TOKEN) { // We only block or warn if the start is exclusively due to notification - return true; + return RESULT_TRUE; } } String logcatMessage = "Indirect notification activity start (trampoline) from " + packageName; if (blockTrampoline(uid)) { Slog.e(TAG, logcatMessage + " blocked"); - return false; + return RESULT_FALSE; } else { Slog.w(TAG, logcatMessage + ", this should be avoided for performance reasons"); - return true; + return new BackgroundActivityStartCallbackResult(true, ALLOWLIST_TOKEN); } } diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java index 38aa57f785e5..bbee77ce58ad 100644 --- a/services/core/java/com/android/server/pm/DeletePackageHelper.java +++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java @@ -425,7 +425,7 @@ final class DeletePackageHelper { user == null || user.getIdentifier() == USER_ALL; if ((!deleteSystem || deleteAllUsers) && disabledPs == null) { Slog.w(TAG, "Attempt to delete unknown system package " - + ps.getPkg().getPackageName()); + + ps.getName()); return null; } // Confirmed if the system package has been updated diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index acdc79fb9922..e02ec6a9e3b4 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -3077,7 +3077,8 @@ final class InstallPackageHelper { } if (succeeded) { - Slog.i(TAG, "installation completed:" + packageName); + Slog.i(TAG, "installation completed for package:" + packageName + + ". Final code path: " + pkgSetting.getPath().getPath()); if (Flags.aslInApkAppMetadataSource() && pkgSetting.getAppMetadataSource() == APP_METADATA_SOURCE_APK) { diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java index 3361dbc2df07..72f2068800d2 100644 --- a/services/core/java/com/android/server/pm/InstallRequest.java +++ b/services/core/java/com/android/server/pm/InstallRequest.java @@ -869,6 +869,9 @@ final class InstallRequest { public void setScannedPackageSettingFirstInstallTimeFromReplaced( @Nullable PackageStateInternal replacedPkgSetting, int[] userId) { assertScanResultExists(); + if (replacedPkgSetting == null) { + return; + } mScanResult.mPkgSetting.setFirstInstallTimeFromReplaced(replacedPkgSetting, userId); } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 38d458767015..2744721c3a46 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -115,7 +115,6 @@ import static com.android.server.wm.WindowManagerPolicyProto.SCREEN_ON_FULLY; import static com.android.server.wm.WindowManagerPolicyProto.WINDOW_MANAGER_DRAW_COMPLETE; import android.accessibilityservice.AccessibilityService; -import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; @@ -268,6 +267,7 @@ import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -311,6 +311,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { static final int SHORT_PRESS_POWER_LOCK_OR_SLEEP = 6; static final int SHORT_PRESS_POWER_DREAM_OR_SLEEP = 7; static final int SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP = 8; + static final int SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP = 9; // must match: config_LongPressOnPowerBehavior in config.xml // The config value can be overridden using Settings.Global.POWER_BUTTON_LONG_PRESS @@ -1234,8 +1235,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { break; } case SHORT_PRESS_POWER_DREAM_OR_SLEEP: { - attemptToDreamFromShortPowerButtonPress( - true, + attemptToDreamOrAwakeFromShortPowerButtonPress( + /* isScreenOn */ true, + /* awakeWhenDream */ false, + /* noDreamAction */ () -> sleepDefaultDisplayFromPowerButton(eventTime, 0)); break; } @@ -1269,13 +1272,22 @@ public class PhoneWindowManager implements WindowManagerPolicy { lockNow(options); } else { // If the hub cannot be run, attempt to dream instead. - attemptToDreamFromShortPowerButtonPress( + attemptToDreamOrAwakeFromShortPowerButtonPress( /* isScreenOn */ true, + /* awakeWhenDream */ false, /* noDreamAction */ () -> sleepDefaultDisplayFromPowerButton(eventTime, 0)); } break; } + case SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP: { + attemptToDreamOrAwakeFromShortPowerButtonPress( + /* isScreenOn */ true, + /* awakeWhenDream */ true, + /* noDreamAction */ + () -> sleepDefaultDisplayFromPowerButton(eventTime, 0)); + break; + } } } } @@ -1319,15 +1331,18 @@ public class PhoneWindowManager implements WindowManagerPolicy { } /** - * Attempt to dream from a power button press. + * Attempt to dream, awake or sleep from a power button press. * * @param isScreenOn Whether the screen is currently on. + * @param awakeWhenDream When it's set to {@code true}, awake the device from dreaming. + * Otherwise, go to sleep. * @param noDreamAction The action to perform if dreaming is not possible. */ - private void attemptToDreamFromShortPowerButtonPress( - boolean isScreenOn, Runnable noDreamAction) { + private void attemptToDreamOrAwakeFromShortPowerButtonPress( + boolean isScreenOn, boolean awakeWhenDream, Runnable noDreamAction) { if (mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_SLEEP - && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP) { + && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP + && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP) { // If the power button behavior isn't one that should be able to trigger the dream, give // up. noDreamAction.run(); @@ -1335,9 +1350,24 @@ public class PhoneWindowManager implements WindowManagerPolicy { } final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal(); - if (dreamManagerInternal == null || !dreamManagerInternal.canStartDreaming(isScreenOn)) { - Slog.d(TAG, "Can't start dreaming when attempting to dream from short power" - + " press (isScreenOn=" + isScreenOn + ")"); + if (dreamManagerInternal == null) { + Slog.d(TAG, + "Can't access dream manager dreaming when attempting to start or stop dream " + + "from short power press (isScreenOn=" + + isScreenOn + ", awakeWhenDream=" + awakeWhenDream + ")"); + noDreamAction.run(); + return; + } + + if (!dreamManagerInternal.canStartDreaming(isScreenOn)) { + if (awakeWhenDream && dreamManagerInternal.isDreaming()) { + dreamManagerInternal.stopDream(false /*immediate*/, "short press power" /*reason*/); + return; + } + Slog.d(TAG, + "Can't start dreaming and the device is not dreaming when attempting to start " + + "or stop dream from short power press (isScreenOn=" + + isScreenOn + ", awakeWhenDream=" + awakeWhenDream + ")"); noDreamAction.run(); return; } @@ -2312,6 +2342,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { return ActivityManager.getService(); } + LockPatternUtils getLockPatternUtils() { + return new LockPatternUtils(mContext); + } + ButtonOverridePermissionChecker getButtonOverridePermissionChecker() { return new ButtonOverridePermissionChecker(); } @@ -2360,7 +2394,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { mAccessibilityShortcutController = injector.getAccessibilityShortcutController( mContext, new Handler(), mCurrentUserId); mGlobalActionsFactory = injector.getGlobalActionsFactory(); - mLockPatternUtils = new LockPatternUtils(mContext); + mLockPatternUtils = injector.getLockPatternUtils(); mLogger = new MetricsLogger(); Resources res = mContext.getResources(); @@ -4240,19 +4274,51 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (!useKeyGestureEventHandler()) { return; } - mInputManager.registerKeyGestureEventHandler((event, focusedToken) -> { - boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event, - focusedToken); - if (handled && !event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch( - (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) { - mPowerKeyHandled = true; - } - return handled; - }); + List<Integer> supportedGestures = new ArrayList<>(List.of( + KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, + KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT, + KeyGestureEvent.KEY_GESTURE_TYPE_HOME, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS, + KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL, + KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, + KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT, + KeyGestureEvent.KEY_GESTURE_TYPE_BACK, + KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION, + KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE, + KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT, + KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT, + KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER, + KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP, + KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN, + KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER, + KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS, + KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH, + KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH, + KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT, + KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB, + KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD, + KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD, + KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS, + KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT + )); + if (enableTalkbackAndMagnifierKeyGestures()) { + supportedGestures.add(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK); + } + if (enableVoiceAccessKeyGestures()) { + supportedGestures.add(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS); + } + mInputManager.registerKeyGestureEventHandler(supportedGestures, + PhoneWindowManager.this::handleKeyGestureEvent); } @VisibleForTesting - boolean handleKeyGestureEvent(KeyGestureEvent event, IBinder focusedToken) { + void handleKeyGestureEvent(KeyGestureEvent event, IBinder focusedToken) { boolean start = event.getAction() == KeyGestureEvent.ACTION_GESTURE_START; boolean complete = event.getAction() == KeyGestureEvent.ACTION_GESTURE_COMPLETE && !event.isCancelled(); @@ -4262,12 +4328,16 @@ public class PhoneWindowManager implements WindowManagerPolicy { int modifierState = event.getModifierState(); boolean keyguardOn = keyguardOn(); boolean canLaunchApp = isUserSetupComplete() && !keyguardOn; + if (!event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch( + (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) { + mPowerKeyHandled = true; + } switch (gestureType) { case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS: if (complete) { showRecentApps(false); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH: if (!keyguardOn) { if (start) { @@ -4276,7 +4346,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { toggleRecentApps(); } } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT: case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT: if (complete && canLaunchApp) { @@ -4284,33 +4354,33 @@ public class PhoneWindowManager implements WindowManagerPolicy { deviceId, SystemClock.uptimeMillis(), AssistUtils.INVOCATION_TYPE_UNKNOWN); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_HOME: if (complete) { // Post to main thread to avoid blocking input pipeline. mHandler.post(() -> handleShortPressOnHome(displayId)); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS: if (complete && canLaunchApp) { showSystemSettings(); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN: if (complete) { lockNow(null /* options */); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL: if (complete) { toggleNotificationPanel(); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT: if (complete) { interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT: if (complete && mEnableBugReportKeyboardShortcut) { try { @@ -4321,12 +4391,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { Slog.d(TAG, "Error taking bugreport", e); } } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_BACK: if (complete) { injectBackGesture(SystemClock.uptimeMillis()); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION: if (complete) { StatusBarManagerInternal statusbar = getStatusBarManagerInternal(); @@ -4335,7 +4405,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { getTargetDisplayIdForKeyGestureEvent(event)); } } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE: if (complete) { StatusBarManagerInternal statusbar = getStatusBarManagerInternal(); @@ -4344,24 +4414,24 @@ public class PhoneWindowManager implements WindowManagerPolicy { getTargetDisplayIdForKeyGestureEvent(event)); } } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT: if (complete) { moveFocusedTaskToStageSplit(getTargetDisplayIdForKeyGestureEvent(event), true /* leftOrTop */); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT: if (complete) { moveFocusedTaskToStageSplit(getTargetDisplayIdForKeyGestureEvent(event), false /* leftOrTop */); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER: if (complete) { toggleKeyboardShortcutsMenu(deviceId); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP: case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN: if (complete) { @@ -4369,32 +4439,32 @@ public class PhoneWindowManager implements WindowManagerPolicy { gestureType == KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP ? 1 : -1; changeDisplayBrightnessValue(displayId, direction); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER: if (start) { showRecentApps(true); } else { hideRecentApps(true, false); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS: case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS: if (complete && isKeyEventForCurrentUser(event.getDisplayId(), event.getKeycodes()[0], "launchAllAppsViaA11y")) { launchAllAppsAction(); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH: if (complete && canLaunchApp) { launchTargetSearchActivity(); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH: if (complete) { int direction = (modifierState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1; sendSwitchKeyboardLayout(displayId, focusedToken, direction); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD: if (start) { // Screenshot chord is pressed: Wait for long press delay before taking @@ -4404,14 +4474,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { } else { cancelPendingScreenshotChordAction(); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD: if (start) { interceptRingerToggleChord(); } else { cancelPendingRingerToggleChordAction(); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS: if (start) { performHapticFeedback( @@ -4421,40 +4491,34 @@ public class PhoneWindowManager implements WindowManagerPolicy { } else { cancelGlobalActionsAction(); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT: if (start) { interceptBugreportGestureTv(); } else { cancelBugreportGestureTv(); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT: if (complete && mAccessibilityShortcutController.isAccessibilityShortcutAvailable( isKeyguardLocked())) { mHandler.sendMessage(mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT)); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS: if (complete) { mContext.closeSystemDialogs(); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK: - if (enableTalkbackAndMagnifierKeyGestures()) { - if (complete) { - mTalkbackShortcutController.toggleTalkback(mCurrentUserId, - TalkbackShortcutController.ShortcutSource.KEYBOARD); - } - return true; + if (complete) { + mTalkbackShortcutController.toggleTalkback(mCurrentUserId, + TalkbackShortcutController.ShortcutSource.KEYBOARD); } break; case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS: - if (enableVoiceAccessKeyGestures()) { - if (complete) { - mVoiceAccessShortcutController.toggleVoiceAccess(mCurrentUserId); - } - return true; + if (complete) { + mVoiceAccessShortcutController.toggleVoiceAccess(mCurrentUserId); } break; case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION: @@ -4463,7 +4527,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { && mModifierShortcutManager.launchApplication(data)) { dismissKeyboardShortcutsMenu(); } - return true; + break; case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB: NotificationManager nm = getNotificationService(); if (nm != null) { @@ -4472,9 +4536,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { : Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, "Key gesture DND", true); } - return true; + break; + default: + Log.w(TAG, "Received a key gesture " + event + + " that was not registered by this handler"); + break; } - return false; } private void changeDisplayBrightnessValue(int displayId, int direction) { diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 798c794edaf5..0f6cc24f1fc9 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -87,6 +87,7 @@ import android.service.quicksettings.TileService; import android.text.TextUtils; import android.util.ArrayMap; import android.util.IndentingPrintWriter; +import android.util.IntArray; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; @@ -102,6 +103,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.inputmethod.SoftInputShowHideReason; import com.android.internal.logging.InstanceId; import com.android.internal.os.TransferPipe; +import com.android.internal.statusbar.DisableStates; import com.android.internal.statusbar.IAddTileResultCallback; import com.android.internal.statusbar.ISessionListener; import com.android.internal.statusbar.IStatusBar; @@ -124,6 +126,7 @@ import com.android.server.policy.GlobalActionsProvider; import com.android.server.power.ShutdownCheckPoints; import com.android.server.power.ShutdownThread; import com.android.server.wm.ActivityTaskManagerInternal; +import com.android.systemui.shared.Flags; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -1344,48 +1347,76 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D return mTracingEnabled; } - // TODO(b/117478341): make it aware of multi-display if needed. + /** + * Disable status bar features. Pass the bitwise-or of the {@code #DISABLE_*} flags. + * To re-enable everything, pass {@code #DISABLE_NONE}. + * + * Warning: Only pass {@code #DISABLE_*} flags into this function, do not use + * {@code #DISABLE2_*} flags. + */ @Override public void disable(int what, IBinder token, String pkg) { disableForUser(what, token, pkg, mCurrentUserId); } - // TODO(b/117478341): make it aware of multi-display if needed. + /** + * Disable status bar features for a given user. Pass the bitwise-or of the + * {@code #DISABLE_*} flags. To re-enable everything, pass {@code #DISABLE_NONE}. + * + * Warning: Only pass {@code #DISABLE_*} flags into this function, do not use + * {@code #DISABLE2_*} flags. + */ @Override public void disableForUser(int what, IBinder token, String pkg, int userId) { enforceStatusBar(); enforceValidCallingUser(); synchronized (mLock) { - disableLocked(DEFAULT_DISPLAY, userId, what, token, pkg, 1); + if (Flags.statusBarConnectedDisplays()) { + IntArray displayIds = new IntArray(); + for (int i = 0; i < mDisplayUiState.size(); i++) { + displayIds.add(mDisplayUiState.keyAt(i)); + } + disableAllDisplaysLocked(displayIds, userId, what, token, pkg, /* whichFlag= */ 1); + } else { + disableLocked(DEFAULT_DISPLAY, userId, what, token, pkg, /* whichFlag= */ 1); + } } } - // TODO(b/117478341): make it aware of multi-display if needed. /** - * Disable additional status bar features. Pass the bitwise-or of the DISABLE2_* flags. - * To re-enable everything, pass {@link #DISABLE2_NONE}. + * Disable additional status bar features. Pass the bitwise-or of the {@code #DISABLE2_*} flags. + * To re-enable everything, pass {@code #DISABLE2_NONE}. * - * Warning: Only pass DISABLE2_* flags into this function, do not use DISABLE_* flags. + * Warning: Only pass {@code #DISABLE2_*} flags into this function, do not use + * {@code #DISABLE_*} flags. */ @Override public void disable2(int what, IBinder token, String pkg) { disable2ForUser(what, token, pkg, mCurrentUserId); } - // TODO(b/117478341): make it aware of multi-display if needed. /** - * Disable additional status bar features for a given user. Pass the bitwise-or of the - * DISABLE2_* flags. To re-enable everything, pass {@link #DISABLE_NONE}. + * Disable additional status bar features for a given user. Pass the bitwise-or + * of the {@code #DISABLE2_*} flags. To re-enable everything, pass {@code #DISABLE2_NONE}. * - * Warning: Only pass DISABLE2_* flags into this function, do not use DISABLE_* flags. + * Warning: Only pass {@code #DISABLE2_*} flags into this function, do not use + * {@code #DISABLE_*} flags. */ @Override public void disable2ForUser(int what, IBinder token, String pkg, int userId) { enforceStatusBar(); synchronized (mLock) { - disableLocked(DEFAULT_DISPLAY, userId, what, token, pkg, 2); + if (Flags.statusBarConnectedDisplays()) { + IntArray displayIds = new IntArray(); + for (int i = 0; i < mDisplayUiState.size(); i++) { + displayIds.add(mDisplayUiState.keyAt(i)); + } + disableAllDisplaysLocked(displayIds, userId, what, token, pkg, /* whichFlag= */ 2); + } else { + disableLocked(DEFAULT_DISPLAY, userId, what, token, pkg, /* whichFlag= */ 2); + } } } @@ -1414,6 +1445,42 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } } + // This method batches disable state across all displays into a single remote call + // (IStatusBar#disableForAllDisplays) for efficiency and calls + // NotificationDelegate#onSetDisabled only if any display's disable state changes. + private void disableAllDisplaysLocked(IntArray displayIds, int userId, int what, IBinder token, + String pkg, int whichFlag) { + // It's important that the the callback and the call to mBar get done + // in the same order when multiple threads are calling this function + // so they are paired correctly. The messages on the handler will be + // handled in the order they were enqueued, but will be outside the lock. + manageDisableListLocked(userId, what, token, pkg, whichFlag); + + // Ensure state for the current user is applied, even if passed a non-current user. + final int net1 = gatherDisableActionsLocked(mCurrentUserId, 1); + final int net2 = gatherDisableActionsLocked(mCurrentUserId, 2); + + IStatusBar bar = mBar; + Map<Integer, Pair<Integer, Integer>> displaysWithNewDisableStates = new HashMap<>(); + for (int displayId : displayIds.toArray()) { + final UiState state = getUiState(displayId); + if (!state.disableEquals(net1, net2)) { + state.setDisabled(net1, net2); + displaysWithNewDisableStates.put(displayId, new Pair(net1, net2)); + } + } + if (bar != null) { + try { + bar.disableForAllDisplays(new DisableStates(displaysWithNewDisableStates)); + } catch (RemoteException ex) { + Slog.e(TAG, "Unable to disable Status bar.", ex); + } + } + if (!displaysWithNewDisableStates.isEmpty()) { + mHandler.post(() -> mNotificationDelegate.onSetDisabled(net1)); + } + } + /** * Get the currently applied disable flags, in the form of one Pair<Integer, Integer>. * diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index b76b23161e78..b9ab863a2805 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -158,7 +158,6 @@ import static com.android.server.wm.ActivityRecordProto.FRONT_OF_TASK; import static com.android.server.wm.ActivityRecordProto.IN_SIZE_COMPAT_MODE; import static com.android.server.wm.ActivityRecordProto.IS_ANIMATING; import static com.android.server.wm.ActivityRecordProto.IS_USER_FULLSCREEN_OVERRIDE_ENABLED; -import static com.android.server.wm.ActivityRecordProto.LAST_ALL_DRAWN; import static com.android.server.wm.ActivityRecordProto.LAST_DROP_INPUT_MODE; import static com.android.server.wm.ActivityRecordProto.LAST_SURFACE_SHOWING; import static com.android.server.wm.ActivityRecordProto.MIN_ASPECT_RATIO; @@ -723,7 +722,6 @@ final class ActivityRecord extends WindowToken { private int mNumInterestingWindows; private int mNumDrawnWindows; boolean allDrawn; - private boolean mLastAllDrawn; /** * Solely for reporting to ActivityMetricsLogger. Just tracks whether, the last time this @@ -1148,13 +1146,11 @@ final class ActivityRecord extends WindowToken { if (mAppStopped) { pw.print(prefix); pw.print("mAppStopped="); pw.println(mAppStopped); } - if (mNumInterestingWindows != 0 || mNumDrawnWindows != 0 - || allDrawn || mLastAllDrawn) { + if (mNumInterestingWindows != 0 || mNumDrawnWindows != 0 || allDrawn) { pw.print(prefix); pw.print("mNumInterestingWindows="); pw.print(mNumInterestingWindows); pw.print(" mNumDrawnWindows="); pw.print(mNumDrawnWindows); pw.print(" allDrawn="); pw.print(allDrawn); - pw.print(" lastAllDrawn="); pw.print(mLastAllDrawn); pw.println(")"); } if (mStartingData != null || firstWindowDrawn) { @@ -3665,13 +3661,6 @@ final class ActivityRecord extends WindowToken { if (endTask) { mAtmService.getLockTaskController().clearLockedTask(task); - // This activity was in the top focused root task and this is the last - // activity in that task, give this activity a higher layer so it can stay on - // top before the closing task transition be executed. - if (mayAdjustTop) { - mNeedsZBoost = true; - mDisplayContent.assignWindowLayers(false /* setLayoutNeeded */); - } } } else if (!isState(PAUSING)) { if (mVisibleRequested) { @@ -5155,7 +5144,6 @@ final class ActivityRecord extends WindowToken { void clearAllDrawn() { allDrawn = false; - mLastAllDrawn = false; } /** @@ -6599,35 +6587,6 @@ final class ActivityRecord extends WindowToken { nowVisible = false; } - @Override - void checkAppWindowsReadyToShow() { - if (allDrawn == mLastAllDrawn) { - return; - } - - mLastAllDrawn = allDrawn; - if (!allDrawn) { - return; - } - - setAppLayoutChanges(FINISH_LAYOUT_REDO_ANIM, "checkAppWindowsReadyToShow"); - - // We can now show all of the drawn windows! - if (canShowWindows()) { - showAllWindowsLocked(); - } - } - - /** - * This must be called while inside a transaction. - */ - void showAllWindowsLocked() { - forAllWindows(windowState -> { - if (DEBUG_VISIBILITY) Slog.v(TAG, "performing show on: " + windowState); - windowState.performShowLocked(); - }, false /* traverseTopToBottom */); - } - void updateReportedVisibilityLocked() { if (DEBUG_VISIBILITY) Slog.v(TAG, "Update reported visibility: " + this); final int count = mChildren.size(); @@ -7241,11 +7200,6 @@ final class ActivityRecord extends WindowToken { } @Override - boolean needsZBoost() { - return mNeedsZBoost || super.needsZBoost(); - } - - @Override public SurfaceControl getAnimationLeashParent() { // For transitions in the root pinned task (menu activity) we just let them occur as a child // of the root pinned task. @@ -9393,7 +9347,6 @@ final class ActivityRecord extends WindowToken { proto.write(NUM_INTERESTING_WINDOWS, mNumInterestingWindows); proto.write(NUM_DRAWN_WINDOWS, mNumDrawnWindows); proto.write(ALL_DRAWN, allDrawn); - proto.write(LAST_ALL_DRAWN, mLastAllDrawn); if (mStartingWindow != null) { mStartingWindow.writeIdentifierToProto(proto, STARTING_WINDOW); } diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java index d3fd0e3199a3..f75b17fa1569 100644 --- a/services/core/java/com/android/server/wm/AsyncRotationController.java +++ b/services/core/java/com/android/server/wm/AsyncRotationController.java @@ -234,7 +234,7 @@ class AsyncRotationController extends FadeAnimationController implements Consume } for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) { final Operation op = mTargetWindowTokens.valueAt(i); - if (op.mIsCompletionPending || op.mAction == Operation.ACTION_SEAMLESS) { + if (op.mIsCompletionPending || op.mActions == Operation.ACTION_SEAMLESS) { // Skip completed target. And seamless windows use the signal from blast sync. continue; } @@ -264,17 +264,18 @@ class AsyncRotationController extends FadeAnimationController implements Consume op.mDrawTransaction = null; if (DEBUG) Slog.d(TAG, "finishOp merge transaction " + windowToken.getTopChild()); } - if (op.mAction == Operation.ACTION_TOGGLE_IME) { + if (op.mActions == Operation.ACTION_TOGGLE_IME) { if (DEBUG) Slog.d(TAG, "finishOp fade-in IME " + windowToken.getTopChild()); fadeWindowToken(true /* show */, windowToken, ANIMATION_TYPE_TOKEN_TRANSFORM, (type, anim) -> mDisplayContent.getInsetsStateController() .getImeSourceProvider().reportImeDrawnForOrganizer()); - } else if (op.mAction == Operation.ACTION_FADE) { + } else if ((op.mActions & Operation.ACTION_FADE) != 0) { if (DEBUG) Slog.d(TAG, "finishOp fade-in " + windowToken.getTopChild()); // The previous animation leash will be dropped when preparing fade-in animation, so // simply apply new animation without restoring the transformation. fadeWindowToken(true /* show */, windowToken, ANIMATION_TYPE_TOKEN_TRANSFORM); - } else if (op.isValidSeamless()) { + } + if (op.isValidSeamless()) { if (DEBUG) Slog.d(TAG, "finishOp undo seamless " + windowToken.getTopChild()); final SurfaceControl.Transaction t = windowToken.getSyncTransaction(); clearTransform(t, op.mLeash); @@ -339,7 +340,7 @@ class AsyncRotationController extends FadeAnimationController implements Consume } if (mTransitionOp == OP_APP_SWITCH && token.mTransitionController.inTransition()) { final Operation op = mTargetWindowTokens.get(token); - if (op != null && op.mAction == Operation.ACTION_FADE) { + if (op != null && op.mActions == Operation.ACTION_FADE) { // Defer showing to onTransitionFinished(). if (DEBUG) Slog.d(TAG, "Defer completion " + token.getTopChild()); return false; @@ -367,11 +368,12 @@ class AsyncRotationController extends FadeAnimationController implements Consume for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) { final WindowToken windowToken = mTargetWindowTokens.keyAt(i); final Operation op = mTargetWindowTokens.valueAt(i); - if (op.mAction == Operation.ACTION_FADE || op.mAction == Operation.ACTION_TOGGLE_IME) { + if ((op.mActions & Operation.ACTION_FADE) != 0 + || op.mActions == Operation.ACTION_TOGGLE_IME) { fadeWindowToken(false /* show */, windowToken, ANIMATION_TYPE_TOKEN_TRANSFORM); op.mLeash = windowToken.getAnimationLeash(); if (DEBUG) Slog.d(TAG, "Start fade-out " + windowToken.getTopChild()); - } else if (op.mAction == Operation.ACTION_SEAMLESS) { + } else if (op.mActions == Operation.ACTION_SEAMLESS) { op.mLeash = windowToken.mSurfaceControl; if (DEBUG) Slog.d(TAG, "Start seamless " + windowToken.getTopChild()); } @@ -481,13 +483,13 @@ class AsyncRotationController extends FadeAnimationController implements Consume /** Returns {@code true} if the controller will run fade animations on the window. */ boolean hasFadeOperation(WindowToken token) { final Operation op = mTargetWindowTokens.get(token); - return op != null && op.mAction == Operation.ACTION_FADE; + return op != null && (op.mActions & Operation.ACTION_FADE) != 0; } /** Returns {@code true} if the window is un-rotated to original rotation. */ boolean hasSeamlessOperation(WindowToken token) { final Operation op = mTargetWindowTokens.get(token); - return op != null && op.mAction == Operation.ACTION_SEAMLESS; + return op != null && (op.mActions & Operation.ACTION_SEAMLESS) != 0; } /** @@ -541,7 +543,7 @@ class AsyncRotationController extends FadeAnimationController implements Consume final Operation op = mTargetWindowTokens.valueAt(i); final SurfaceControl leash = op.mLeash; if (leash == null || !leash.isValid()) continue; - if (mHasScreenRotationAnimation && op.mAction == Operation.ACTION_FADE) { + if (mHasScreenRotationAnimation && op.mActions == Operation.ACTION_FADE) { // Hide the windows immediately because a screenshot layer should cover the screen. t.setAlpha(leash, 0f); if (DEBUG) { @@ -707,7 +709,7 @@ class AsyncRotationController extends FadeAnimationController implements Consume * start transaction of rotation transition is applied. */ private boolean canDrawBeforeStartTransaction(Operation op) { - return op.mAction != Operation.ACTION_SEAMLESS; + return (op.mActions & Operation.ACTION_SEAMLESS) == 0; } void dump(PrintWriter pw, String prefix) { @@ -723,14 +725,14 @@ class AsyncRotationController extends FadeAnimationController implements Consume /** The operation to control the rotation appearance associated with window token. */ private static class Operation { @Retention(RetentionPolicy.SOURCE) - @IntDef(value = { ACTION_SEAMLESS, ACTION_FADE, ACTION_TOGGLE_IME }) + @IntDef(flag = true, value = { ACTION_SEAMLESS, ACTION_FADE, ACTION_TOGGLE_IME }) @interface Action {} static final int ACTION_SEAMLESS = 1; - static final int ACTION_FADE = 2; - /** The action to toggle the IME window appearance */ - static final int ACTION_TOGGLE_IME = 3; - final @Action int mAction; + static final int ACTION_FADE = 1 << 1; + /** The action to toggle the IME window appearance. It can only be used exclusively. */ + static final int ACTION_TOGGLE_IME = 1 << 2; + final @Action int mActions; /** The leash of window token. It can be animation leash or the token itself. */ SurfaceControl mLeash; /** Whether the window is drawn before the transition starts. */ @@ -744,17 +746,17 @@ class AsyncRotationController extends FadeAnimationController implements Consume */ SurfaceControl.Transaction mDrawTransaction; - Operation(@Action int action) { - mAction = action; + Operation(@Action int actions) { + mActions = actions; } boolean isValidSeamless() { - return mAction == ACTION_SEAMLESS && mLeash != null && mLeash.isValid(); + return (mActions & ACTION_SEAMLESS) != 0 && mLeash != null && mLeash.isValid(); } @Override public String toString() { - return "Operation{a=" + mAction + " pending=" + mIsCompletionPending + '}'; + return "Operation{a=" + mActions + " pending=" + mIsCompletionPending + '}'; } } } diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartCallback.java b/services/core/java/com/android/server/wm/BackgroundActivityStartCallback.java index eec648dae24a..826d5fb1c333 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartCallback.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartCallback.java @@ -24,6 +24,16 @@ import java.util.Collection; * Callback to decide activity starts and related operations based on originating tokens. */ public interface BackgroundActivityStartCallback { + BackgroundActivityStartCallbackResult RESULT_FALSE = + new BackgroundActivityStartCallbackResult(false, null); + BackgroundActivityStartCallbackResult RESULT_TRUE = + new BackgroundActivityStartCallbackResult(true, null); + + record BackgroundActivityStartCallbackResult( + boolean allowed, + IBinder token + ) {} + /** * Returns true if the background activity start originating from {@code tokens} should be * allowed or not. @@ -34,7 +44,8 @@ public interface BackgroundActivityStartCallback { * This will be called holding the WM and local lock, don't do anything costly or invoke AM/WM * methods here directly. */ - boolean isActivityStartAllowed(Collection<IBinder> tokens, int uid, String packageName); + BackgroundActivityStartCallbackResult isActivityStartAllowed(Collection<IBinder> tokens, + int uid, String packageName); /** * Returns whether {@code uid} can send {@link android.content.Intent diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index f50a68cc5389..f8a50b3fda04 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -51,6 +51,7 @@ import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreat import static com.android.window.flags.Flags.balShowToastsBlocked; import static com.android.window.flags.Flags.balStrictModeGracePeriod; import static com.android.window.flags.Flags.balStrictModeRo; +import static com.android.window.flags.Flags.balAdditionalLogging; import static java.lang.annotation.RetentionPolicy.SOURCE; import static java.util.Objects.requireNonNull; @@ -1939,6 +1940,7 @@ public class BackgroundActivityStartController { } } logIfOnlyAllowedBy(finalVerdict, state, BAL_ALLOW_NON_APP_VISIBLE_WINDOW); + logIfOnlyAllowedBy(finalVerdict, state, BAL_ALLOW_TOKEN); if (balImprovedMetrics()) { if (shouldLogStats(finalVerdict, state)) { @@ -1998,8 +2000,10 @@ public class BackgroundActivityStartController { return false; } else { // log to determine grace period length distribution - Slog.wtf(TAG, "Activity start ONLY allowed by " + balCodeToString(balCode) + " " - + finalVerdict.mMessage + ": " + state); + if (balAdditionalLogging()) { + Slog.wtf(TAG, "Activity start ONLY allowed by " + balCodeToString(balCode) + " " + + finalVerdict.mMessage + ": " + state); + } return true; } } diff --git a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java index 31b239421baf..2605310db6f4 100644 --- a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java +++ b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java @@ -128,11 +128,16 @@ class BackgroundLaunchProcessController { return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ "process instrumenting with background activity starts privileges"); } - // Allow if the flag was explicitly set. - if (checkConfiguration.checkOtherExemptions && isBackgroundStartAllowedByToken(uid, - packageName, checkConfiguration.isCheckingForFgsStart)) { - return new BalVerdict(balImprovedMetrics() ? BAL_ALLOW_TOKEN : BAL_ALLOW_PERMISSION, - /*background*/ "process allowed by token"); + // Allow if the token is explicitly allowed. + if (checkConfiguration.checkOtherExemptions) { + BalVerdict tokenVerdict = isBackgroundStartAllowedByToken(uid, + packageName, checkConfiguration.isCheckingForFgsStart); + if (tokenVerdict.allows()) { + if (!balImprovedMetrics()) { + return new BalVerdict(BAL_ALLOW_PERMISSION, tokenVerdict.toString()); + } + return tokenVerdict; + } } // Allow if the caller is bound by a UID that's currently foreground. // But still respect the appSwitchState. @@ -174,42 +179,53 @@ class BackgroundLaunchProcessController { * isCheckingForFgsStart is false, we ask the callback if the start is allowed for these tokens, * otherwise if there is no callback we allow. */ - private boolean isBackgroundStartAllowedByToken(int uid, String packageName, + private BalVerdict isBackgroundStartAllowedByToken(int uid, String packageName, boolean isCheckingForFgsStart) { synchronized (this) { if (mBackgroundStartPrivileges == null || mBackgroundStartPrivileges.isEmpty()) { // no tokens to allow anything - return false; + return BalVerdict.BLOCK; } if (isCheckingForFgsStart) { // check if any token allows foreground service starts for (int i = mBackgroundStartPrivileges.size(); i-- > 0; ) { if (mBackgroundStartPrivileges.valueAt(i).allowsBackgroundFgsStarts()) { - return true; + return new BalVerdict(BAL_ALLOW_TOKEN, "process allowed by token"); } } - return false; + return BalVerdict.BLOCK; } if (mBackgroundActivityStartCallback == null) { // without a callback just check if any token allows background activity starts for (int i = mBackgroundStartPrivileges.size(); i-- > 0; ) { if (mBackgroundStartPrivileges.valueAt(i) .allowsBackgroundActivityStarts()) { - return true; + return new BalVerdict(BAL_ALLOW_TOKEN, "process allowed by token"); } } - return false; + return BalVerdict.BLOCK; } List<IBinder> binderTokens = getOriginatingTokensThatAllowBal(); if (binderTokens.isEmpty()) { // no tokens to allow anything - return false; + return BalVerdict.BLOCK; } // The callback will decide. - return mBackgroundActivityStartCallback.isActivityStartAllowed( + BackgroundActivityStartCallback.BackgroundActivityStartCallbackResult + activityStartAllowed = mBackgroundActivityStartCallback.isActivityStartAllowed( binderTokens, uid, packageName); + if (!activityStartAllowed.allowed()) { + return BalVerdict.BLOCK; + } + if (activityStartAllowed.token() == null) { + return new BalVerdict(BAL_ALLOW_TOKEN, + "process allowed by callback (token ignored) tokens: " + binderTokens); + } + return new BalVerdict(BAL_ALLOW_TOKEN, + "process allowed by callback (token: " + activityStartAllowed.token() + + ") tokens: " + binderTokens); } } diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java index 6718ae435cd9..d7d5b44ed210 100644 --- a/services/core/java/com/android/server/wm/DisplayArea.java +++ b/services/core/java/com/android/server/wm/DisplayArea.java @@ -332,12 +332,6 @@ public class DisplayArea<T extends WindowContainer> extends WindowContainer<T> { } @Override - boolean needsZBoost() { - // Z Boost should only happen at or below the ActivityStack level. - return false; - } - - @Override boolean fillsParent() { return true; } diff --git a/services/core/java/com/android/server/wm/OWNERS b/services/core/java/com/android/server/wm/OWNERS index 243a5326b545..0989fc05e0bb 100644 --- a/services/core/java/com/android/server/wm/OWNERS +++ b/services/core/java/com/android/server/wm/OWNERS @@ -26,7 +26,7 @@ mcarli@google.com per-file Background*Start* = set noparent per-file Background*Start* = file:/BAL_OWNERS per-file Background*Start* = ogunwale@google.com, louischang@google.com -per-file BackgroundLaunchProcessController.java = file:/BAL_OWNERS +per-file BackgroundLaunchProcessController*.java = file:/BAL_OWNERS # File related to activity callers per-file ActivityCallerState.java = file:/core/java/android/app/COMPONENT_CALLER_OWNERS diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 0531828be6d4..ec17d131958b 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -35,8 +35,6 @@ import static android.content.Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS; import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME; import static android.content.pm.ActivityInfo.FLAG_RELINQUISH_TASK_IDENTITY; import static android.content.pm.ActivityInfo.FLAG_SHOW_FOR_ALL_USERS; -import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP; -import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP; import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_LANDSCAPE_ONLY; import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_PORTRAIT_ONLY; import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_PRESERVE_ORIENTATION; @@ -51,7 +49,6 @@ import static android.view.Display.INVALID_DISPLAY; import static android.view.SurfaceControl.METADATA_TASK_ID; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; -import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FLAG_APP_CRASHED; import static android.view.WindowManager.TRANSIT_OPEN; @@ -132,7 +129,6 @@ import android.app.IActivityController; import android.app.PictureInPictureParams; import android.app.TaskInfo; import android.app.WindowConfiguration; -import android.app.compat.CompatChanges; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -514,10 +510,16 @@ class Task extends TaskFragment { boolean mIsPerceptible = false; /** - * Whether the compatibility overrides that change the resizability of the app should be allowed - * for the specific app. + * Whether the task has been forced resizable, which is determined by the + * activity that started this task. */ - boolean mAllowForceResizeOverride = true; + private boolean mForceResizeOverride; + + /** + * Whether the task has been forced non-resizable, which is determined by + * the activity that started this task. + */ + private boolean mForceNonResizeOverride; private static final int TRANSLUCENT_TIMEOUT_MSG = FIRST_ACTIVITY_TASK_MSG + 1; @@ -675,7 +677,6 @@ class Task extends TaskFragment { intent = _intent; mMinWidth = minWidth; mMinHeight = minHeight; - updateAllowForceResizeOverride(); } mAtmService.getTaskChangeNotificationController().notifyTaskCreated(_taskId, realActivity); mHandler = new ActivityTaskHandler(mTaskSupervisor.mLooper); @@ -946,6 +947,7 @@ class Task extends TaskFragment { mCallingPackage = r.launchedFromPackage; mCallingFeatureId = r.launchedFromFeatureId; setIntent(intent != null ? intent : r.intent, info != null ? info : r.info); + updateForceResizeOverrides(r); } setLockTaskAuth(r); } @@ -1038,7 +1040,6 @@ class Task extends TaskFragment { mTaskSupervisor.mRecentTasks.remove(this); mTaskSupervisor.mRecentTasks.add(this); } - updateAllowForceResizeOverride(); } /** Sets the original minimal width and height. */ @@ -1855,15 +1856,14 @@ class Task extends TaskFragment { -1 /* don't check PID */, -1 /* don't check UID */, this); } - private void updateAllowForceResizeOverride() { - try { - mAllowForceResizeOverride = mAtmService.mContext.getPackageManager().getPropertyAsUser( - PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES, - getBasePackageName(), null /* className */, mUserId).getBoolean(); - } catch (PackageManager.NameNotFoundException e) { - // Package not found or property not defined, reset to default value. - mAllowForceResizeOverride = true; - } + private void updateForceResizeOverrides(@NonNull ActivityRecord r) { + final AppCompatResizeOverrides resizeOverrides = r.mAppCompatController + .getResizeOverrides(); + mForceResizeOverride = resizeOverrides.shouldOverrideForceResizeApp() + || r.isUniversalResizeable() + || r.mAppCompatController.getAspectRatioOverrides() + .hasFullscreenOverride(); + mForceNonResizeOverride = resizeOverrides.shouldOverrideForceNonResizeApp(); } /** @@ -2882,17 +2882,8 @@ class Task extends TaskFragment { final boolean forceResizable = mAtmService.mForceResizableActivities && getActivityType() == ACTIVITY_TYPE_STANDARD; if (forceResizable) return true; - - final UserHandle userHandle = UserHandle.getUserHandleForUid(mUserId); - final boolean forceResizableOverride = mAllowForceResizeOverride - && CompatChanges.isChangeEnabled( - FORCE_RESIZE_APP, getBasePackageName(), userHandle); - final boolean forceNonResizableOverride = mAllowForceResizeOverride - && CompatChanges.isChangeEnabled( - FORCE_NON_RESIZE_APP, getBasePackageName(), userHandle); - - if (forceNonResizableOverride) return false; - return forceResizableOverride || ActivityInfo.isResizeableMode(mResizeMode) + if (mForceNonResizeOverride) return false; + return mForceResizeOverride || ActivityInfo.isResizeableMode(mResizeMode) || (mSupportsPictureInPicture && checkPictureInPictureSupport); } @@ -3633,43 +3624,39 @@ class Task extends TaskFragment { int layer = 0; boolean decorSurfacePlaced = false; - // We use two passes as a way to promote children which - // need Z-boosting to the end of the list. for (int j = 0; j < mChildren.size(); ++j) { final WindowContainer wc = mChildren.get(j); wc.assignChildLayers(t); - if (!wc.needsZBoost()) { - // Place the decor surface under any untrusted content. - if (mDecorSurfaceContainer != null - && !mDecorSurfaceContainer.mIsBoosted - && !decorSurfacePlaced - && shouldPlaceDecorSurfaceBelowContainer(wc)) { - mDecorSurfaceContainer.assignLayer(t, layer++); - decorSurfacePlaced = true; - } - wc.assignLayer(t, layer++); - - // Boost the adjacent TaskFragment for dimmer if needed. - final TaskFragment taskFragment = wc.asTaskFragment(); - if (taskFragment != null && taskFragment.isEmbedded() - && taskFragment.hasAdjacentTaskFragment()) { - final int[] nextLayer = { layer }; - taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> { - if (adjacentTf.shouldBoostDimmer()) { - adjacentTf.assignLayer(t, nextLayer[0]++); - } - }); - layer = nextLayer[0]; - } + // Place the decor surface under any untrusted content. + if (mDecorSurfaceContainer != null + && !mDecorSurfaceContainer.mIsBoosted + && !decorSurfacePlaced + && shouldPlaceDecorSurfaceBelowContainer(wc)) { + mDecorSurfaceContainer.assignLayer(t, layer++); + decorSurfacePlaced = true; + } + wc.assignLayer(t, layer++); + + // Boost the adjacent TaskFragment for dimmer if needed. + final TaskFragment taskFragment = wc.asTaskFragment(); + if (taskFragment != null && taskFragment.isEmbedded() + && taskFragment.hasAdjacentTaskFragment()) { + final int[] nextLayer = { layer }; + taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> { + if (adjacentTf.shouldBoostDimmer()) { + adjacentTf.assignLayer(t, nextLayer[0]++); + } + }); + layer = nextLayer[0]; + } - // Place the decor surface just above the owner TaskFragment. - if (mDecorSurfaceContainer != null - && !mDecorSurfaceContainer.mIsBoosted - && !decorSurfacePlaced - && wc == mDecorSurfaceContainer.mOwnerTaskFragment) { - mDecorSurfaceContainer.assignLayer(t, layer++); - decorSurfacePlaced = true; - } + // Place the decor surface just above the owner TaskFragment. + if (mDecorSurfaceContainer != null + && !mDecorSurfaceContainer.mIsBoosted + && !decorSurfacePlaced + && wc == mDecorSurfaceContainer.mOwnerTaskFragment) { + mDecorSurfaceContainer.assignLayer(t, layer++); + decorSurfacePlaced = true; } } @@ -3679,12 +3666,6 @@ class Task extends TaskFragment { mDecorSurfaceContainer.assignLayer(t, layer++); } - for (int j = 0; j < mChildren.size(); ++j) { - final WindowContainer wc = mChildren.get(j); - if (wc.needsZBoost()) { - wc.assignLayer(t, layer++); - } - } if (mOverlayHost != null) { mOverlayHost.setLayer(t, layer++); } diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index fb7bab4b3e26..1de139696c07 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -48,7 +48,6 @@ import android.content.pm.ActivityInfo.ScreenOrientation; import android.content.res.Configuration; import android.graphics.Color; import android.os.UserHandle; -import android.util.IntArray; import android.util.Slog; import android.view.SurfaceControl; import android.view.WindowManager; @@ -102,9 +101,6 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { private final ArrayList<WindowContainer> mTmpAlwaysOnTopChildren = new ArrayList<>(); private final ArrayList<WindowContainer> mTmpNormalChildren = new ArrayList<>(); private final ArrayList<WindowContainer> mTmpHomeChildren = new ArrayList<>(); - private final IntArray mTmpNeedsZBoostIndexes = new IntArray(); - - private ArrayList<Task> mTmpTasks = new ArrayList<>(); private ActivityTaskManagerService mAtmService; @@ -740,40 +736,14 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { */ private int adjustRootTaskLayer(SurfaceControl.Transaction t, ArrayList<WindowContainer> children, int startLayer) { - mTmpNeedsZBoostIndexes.clear(); final int childCount = children.size(); - boolean hasAdjacentTask = false; for (int i = 0; i < childCount; i++) { final WindowContainer child = children.get(i); - final TaskDisplayArea childTda = child.asTaskDisplayArea(); - final boolean childNeedsZBoost = childTda != null - ? childTda.childrenNeedZBoost() - : child.needsZBoost(); - - if (childNeedsZBoost) { - mTmpNeedsZBoostIndexes.add(i); - continue; - } - - child.assignLayer(t, startLayer++); - } - - final int zBoostSize = mTmpNeedsZBoostIndexes.size(); - for (int i = 0; i < zBoostSize; i++) { - final WindowContainer child = children.get(mTmpNeedsZBoostIndexes.get(i)); child.assignLayer(t, startLayer++); } return startLayer; } - private boolean childrenNeedZBoost() { - final boolean[] needsZBoost = new boolean[1]; - forAllRootTasks(task -> { - needsZBoost[0] |= task.needsZBoost(); - }); - return needsZBoost[0]; - } - void setBackgroundColor(@ColorInt int colorInt) { setBackgroundColor(colorInt, false /* restore */); } diff --git a/services/core/java/com/android/server/wm/TaskFpsCallbackController.java b/services/core/java/com/android/server/wm/TaskFpsCallbackController.java index 8c798759c890..665c5cffd9ff 100644 --- a/services/core/java/com/android/server/wm/TaskFpsCallbackController.java +++ b/services/core/java/com/android/server/wm/TaskFpsCallbackController.java @@ -16,7 +16,6 @@ package com.android.server.wm; -import android.content.Context; import android.os.IBinder; import android.os.RemoteException; import android.window.ITaskFpsCallback; @@ -25,12 +24,10 @@ import java.util.HashMap; final class TaskFpsCallbackController { - private final Context mContext; private final HashMap<IBinder, Long> mTaskFpsCallbacks; private final HashMap<IBinder, IBinder.DeathRecipient> mDeathRecipients; - TaskFpsCallbackController(Context context) { - mContext = context; + TaskFpsCallbackController() { mTaskFpsCallbacks = new HashMap<>(); mDeathRecipients = new HashMap<>(); } diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java index 3f2b40c1d7c9..e50545d41655 100644 --- a/services/core/java/com/android/server/wm/WindowAnimator.java +++ b/services/core/java/com/android/server/wm/WindowAnimator.java @@ -18,10 +18,7 @@ package com.android.server.wm; import static com.android.internal.protolog.WmProtoLogGroups.WM_SHOW_TRANSACTIONS; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_SCREEN_ROTATION; import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN; -import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WINDOW_TRACE; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; @@ -57,9 +54,6 @@ public class WindowAnimator { /** Is any window animating? */ private boolean mLastRootAnimating; - /** True if we are running any animations that require expensive composition. */ - private boolean mRunningExpensiveAnimations; - final Choreographer.FrameCallback mAnimationFrameCallback; /** Time of current animation step. Reset on each iteration */ @@ -138,8 +132,6 @@ public class WindowAnimator { scheduleAnimation(); final RootWindowContainer root = mService.mRoot; - final boolean useShellTransition = root.mTransitionController.isShellTransitionsEnabled(); - final int animationFlags = useShellTransition ? CHILDREN : (TRANSITION | CHILDREN); boolean rootAnimating = false; mCurrentTime = frameTimeNs / TimeUtils.NANOS_PER_MS; if (DEBUG_WINDOW_TRACE) { @@ -164,17 +156,13 @@ public class WindowAnimator { for (int i = 0; i < numDisplays; i++) { final DisplayContent dc = root.getChildAt(i); - - if (!useShellTransition) { - dc.checkAppWindowsReadyToShow(); - } if (accessibilityController.hasCallbacks()) { accessibilityController .recomputeMagnifiedRegionAndDrawMagnifiedRegionBorderIfNeeded( dc.mDisplayId); } - if (dc.isAnimating(animationFlags, ANIMATION_TYPE_ALL)) { + if (dc.isAnimating(CHILDREN, ANIMATION_TYPE_ALL)) { rootAnimating = true; if (!dc.mLastContainsRunningSurfaceAnimator) { dc.mLastContainsRunningSurfaceAnimator = true; @@ -211,11 +199,6 @@ public class WindowAnimator { } mLastRootAnimating = rootAnimating; - // APP_TRANSITION, SCREEN_ROTATION, TYPE_RECENTS are handled by shell transition. - if (!useShellTransition) { - updateRunningExpensiveAnimationsLegacy(); - } - final ArrayList<Runnable> afterPrepareSurfacesRunnables = mAfterPrepareSurfacesRunnables; if (!afterPrepareSurfacesRunnables.isEmpty()) { mAfterPrepareSurfacesRunnables = new ArrayList<>(); @@ -244,21 +227,6 @@ public class WindowAnimator { } } - private void updateRunningExpensiveAnimationsLegacy() { - final boolean runningExpensiveAnimations = - mService.mRoot.isAnimating(TRANSITION | CHILDREN /* flags */, - ANIMATION_TYPE_APP_TRANSITION - | ANIMATION_TYPE_SCREEN_ROTATION /* typesToCheck */); - if (runningExpensiveAnimations && !mRunningExpensiveAnimations) { - mService.mSnapshotController.setPause(true); - mTransaction.setEarlyWakeupStart(); - } else if (!runningExpensiveAnimations && mRunningExpensiveAnimations) { - mService.mSnapshotController.setPause(false); - mTransaction.setEarlyWakeupEnd(); - } - mRunningExpensiveAnimations = runningExpensiveAnimations; - } - public void dumpLocked(PrintWriter pw, String prefix, boolean dumpAll) { final String subPrefix = " " + prefix; diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 5cbba355a06f..5b4870b0c0c7 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -279,9 +279,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< */ int mTransitFlags; - /** Whether this container should be boosted at the top of all its siblings. */ - @VisibleForTesting boolean mNeedsZBoost; - /** Layer used to constrain the animation to a container's stack bounds. */ SurfaceControl mAnimationBoundsLayer; @@ -1476,14 +1473,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return stillDeferringRemoval; } - /** Checks if all windows in an app are all drawn and shows them if needed. */ - void checkAppWindowsReadyToShow() { - for (int i = mChildren.size() - 1; i >= 0; --i) { - final WindowContainer wc = mChildren.get(i); - wc.checkAppWindowsReadyToShow(); - } - } - /** * Called when this container or one of its descendants changed its requested orientation, and * wants this container to handle it or pass it to its parent. @@ -2744,15 +2733,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< for (int j = 0; j < mChildren.size(); ++j) { final WindowContainer wc = mChildren.get(j); wc.assignChildLayers(t); - if (!wc.needsZBoost()) { - wc.assignLayer(t, layer++); - } - } - for (int j = 0; j < mChildren.size(); ++j) { - final WindowContainer wc = mChildren.get(j); - if (wc.needsZBoost()) { - wc.assignLayer(t, layer++); - } + wc.assignLayer(t, layer++); } if (mOverlayHost != null) { mOverlayHost.setLayer(t, layer++); @@ -2764,16 +2745,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< scheduleAnimation(); } - boolean needsZBoost() { - if (mNeedsZBoost) return true; - for (int i = 0; i < mChildren.size(); i++) { - if (mChildren.get(i).needsZBoost()) { - return true; - } - } - return false; - } - /** * Write to a protocol buffer output stream. Protocol buffer message definition is at * {@link com.android.server.wm.WindowContainerProto}. @@ -3114,7 +3085,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< public void onAnimationLeashLost(Transaction t) { mLastLayer = -1; mAnimationLeash = null; - mNeedsZBoost = false; reassignLayer(t); updateSurfacePosition(t); } @@ -3140,7 +3110,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< protected void onAnimationFinished(@AnimationType int type, AnimationAdapter anim) { doAnimationFinished(type, anim); mWmService.onAnimationFinished(); - mNeedsZBoost = false; } /** diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index fb38c581d222..a9bb690d4e53 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -1449,7 +1449,7 @@ public class WindowManagerService extends IWindowManager.Stub mPresentationController = new PresentationController(); mBlurController = new BlurController(mContext, mPowerManager); - mTaskFpsCallbackController = new TaskFpsCallbackController(mContext); + mTaskFpsCallbackController = new TaskFpsCallbackController(); mAccessibilityController = new AccessibilityController(this); mScreenRecordingCallbackController = new ScreenRecordingCallbackController(this); mSystemPerformanceHinter = new SystemPerformanceHinter(mContext, displayId -> { @@ -2602,6 +2602,14 @@ public class WindowManagerService extends IWindowManager.Stub // in the new out values right now we need to force a layout. mWindowPlacerLocked.performSurfacePlacement(true /* force */); + if (!win.mHaveFrame && displayContent.mWaitingForConfig) { + // We just forcibly triggered the layout, but this could still be intercepted by + // mWaitingForConfig. Here, we are forcefully marking a value for mLayoutSeq to + // ensure that the resize can occur properly later. Otherwise, the window's frame + // will remain empty forever. + win.mLayoutSeq = displayContent.mLayoutSeq; + } + if (shouldRelayout) { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "relayoutWindow: viewVisibility_1"); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index af5200102fc0..22ddd5f39b24 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -4995,18 +4995,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return true; } - @Override - boolean needsZBoost() { - final InsetsControlTarget target = getDisplayContent().getImeTarget(IME_TARGET_LAYERING); - if (mIsImWindow && target != null) { - final ActivityRecord activity = target.getWindow().mActivityRecord; - if (activity != null) { - return activity.needsZBoost(); - } - } - return false; - } - private boolean isStartingWindowAssociatedToTask() { return mStartingData != null && mStartingData.mAssociatedTask != null; } diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java index ac4aac694c3a..11edb93dffea 100644 --- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java +++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java @@ -383,7 +383,9 @@ public class MetricUtilities { /* api_name */ initialPhaseMetric.getApiName(), /* primary_candidates_indicated */ - candidatePrimaryProviderList + candidatePrimaryProviderList, + /* api_prepared */ + initialPhaseMetric.hasApiUsedPrepareFlow() ); } catch (Exception e) { Slog.w(TAG, "Unexpected error during candidate provider uid metric emit: " + e); @@ -442,7 +444,9 @@ public class MetricUtilities { /* autofill_session_id */ initialPhaseMetric.getAutofillSessionId(), /* autofill_request_id */ - initialPhaseMetric.getAutofillRequestId() + initialPhaseMetric.getAutofillRequestId(), + /* api_prepared */ + initialPhaseMetric.hasApiUsedPrepareFlow() ); } catch (Exception e) { Slog.w(TAG, "Unexpected error during initial metric emit: " + e); diff --git a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java index d60807c7b001..2d4360edf3a8 100644 --- a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java @@ -27,6 +27,7 @@ import android.credentials.GetCredentialRequest; import android.credentials.IGetCredentialCallback; import android.credentials.IPrepareGetCredentialCallback; import android.credentials.PrepareGetCredentialResponseInternal; +import android.credentials.flags.Flags; import android.credentials.selection.GetCredentialProviderData; import android.credentials.selection.ProviderData; import android.credentials.selection.RequestInfo; @@ -60,7 +61,12 @@ public class PrepareGetRequestSession extends GetRequestSession { int numTypes = (request.getCredentialOptions().stream() .map(CredentialOption::getType).collect( Collectors.toSet())).size(); // Dedupe type strings - mRequestSessionMetric.collectGetFlowInitialMetricInfo(request); + if (!Flags.fixMetricDuplicationEmits()) { + mRequestSessionMetric.collectGetFlowInitialMetricInfo(request); + } else { + mRequestSessionMetric.collectGetFlowInitialMetricInfo(request, + /*isApiPrepared=*/ true); + } mPrepareGetCredentialCallback = prepareGetCredentialCallback; Slog.i(TAG, "PrepareGetRequestSession constructed."); diff --git a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java index 8a4e86c440b3..811b97a5bf03 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java @@ -55,6 +55,9 @@ public class InitialPhaseMetric { // The request id of autofill if the request is from autofill, defaults to -1 private int mAutofillRequestId = -1; + // Indicates if this API call used the prepare flow, defaults to false + private boolean mApiUsedPrepareFlow = false; + public InitialPhaseMetric(int sessionIdTrackOne) { mSessionIdCaller = sessionIdTrackOne; @@ -173,4 +176,17 @@ public class InitialPhaseMetric { public int[] getUniqueRequestCounts() { return mRequestCounts.values().stream().mapToInt(Integer::intValue).toArray(); } + + /* ------ API Prepared ------ */ + + public void setApiUsedPrepareFlow(boolean apiUsedPrepareFlow) { + mApiUsedPrepareFlow = apiUsedPrepareFlow; + } + + /** + * @return a boolean indicating if this API call utilized a prepare flow + */ + public boolean hasApiUsedPrepareFlow() { + return mApiUsedPrepareFlow; + } } diff --git a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java index 619a56846e95..dc1747f803ea 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java @@ -225,6 +225,22 @@ public class RequestSessionMetric { } /** + * Collects initializations for Get flow metrics. + * + * @param request the get credential request containing information to parse for metrics + * @param isApiPrepared indicates this API flow utilized the 'prepare' flow + */ + public void collectGetFlowInitialMetricInfo(GetCredentialRequest request, + boolean isApiPrepared) { + try { + collectGetFlowInitialMetricInfo(request); + mInitialPhaseMetric.setApiUsedPrepareFlow(isApiPrepared); + } catch (Exception e) { + Slog.i(TAG, "Unexpected error collecting get flow initial metric: " + e); + } + } + + /** * During browsing, where multiple entries can be selected, this collects the browsing phase * metric information. This is emitted together with the final phase, and the recursive path * with authentication entries, which may occur in rare circumstances, are captured. diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 788b3b883160..56ec27a0ba87 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -121,6 +121,7 @@ import com.android.internal.util.EmergencyAffordanceManager; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.widget.ILockSettings; import com.android.internal.widget.LockSettingsInternal; +import com.android.modules.utils.build.SdkLevel; import com.android.server.accessibility.AccessibilityManagerService; import com.android.server.accounts.AccountManagerService; import com.android.server.adb.AdbService; @@ -2265,19 +2266,27 @@ public final class SystemServer implements Dumpable { Slog.i(TAG, "Not starting VpnManagerService"); } - t.traceBegin("StartVcnManagementService"); - try { - if (VcnLocation.IS_VCN_IN_MAINLINE) { - mSystemServiceManager.startServiceFromJar( - CONNECTIVITY_SERVICE_INITIALIZER_B_CLASS, - CONNECTIVITY_SERVICE_APEX_PATH); - } else { - mSystemServiceManager.startService(CONNECTIVITY_SERVICE_INITIALIZER_B_CLASS); + // TODO: b/374174952 In the end state, VCN registration will be moved to Tethering + // module. Thus the following code block should be removed after Baklava is released + if (!VcnLocation.IS_VCN_IN_MAINLINE || !SdkLevel.isAtLeastB()) { + t.traceBegin("StartVcnManagementService"); + + try { + if (!VcnLocation.IS_VCN_IN_MAINLINE) { + mSystemServiceManager.startService( + CONNECTIVITY_SERVICE_INITIALIZER_B_CLASS); + } else { + // When VCN is in mainline but the SDK level is B-, start the service with + // the apex path. This path can only be hit on an unfinalized B platform + mSystemServiceManager.startServiceFromJar( + CONNECTIVITY_SERVICE_INITIALIZER_B_CLASS, + CONNECTIVITY_SERVICE_APEX_PATH); + } + } catch (Throwable e) { + reportWtf("starting VCN Management Service", e); } - } catch (Throwable e) { - reportWtf("starting VCN Management Service", e); + t.traceEnd(); } - t.traceEnd(); t.traceBegin("StartSystemUpdateManagerService"); try { diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt index 232bb83fdf9f..5a140d53a4d8 100644 --- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt @@ -1753,6 +1753,13 @@ class AppIdPermissionPolicy : SchemePolicy() { } val appIdPermissionFlags = newState.mutateUserState(userId)!!.mutateAppIdPermissionFlags() val permissionFlags = appIdPermissionFlags.mutateOrPut(appId) { MutableIndexedMap() } + // for debugging possible races TODO(b/401768134) + oldState.userStates[userId]?.appIdPermissionFlags[appId]?.map?.let { + if (permissionFlags.map === it) { + throw IllegalStateException("Unexpected sharing between old/new state") + } + } + permissionFlags.putWithDefault(permissionName, newFlags, 0) if (permissionFlags.isEmpty()) { appIdPermissionFlags -= appId diff --git a/services/tests/displayservicetests/Android.bp b/services/tests/displayservicetests/Android.bp index 36ea24195789..c85053d13e68 100644 --- a/services/tests/displayservicetests/Android.bp +++ b/services/tests/displayservicetests/Android.bp @@ -51,6 +51,7 @@ android_test { data: [ ":DisplayManagerTestApp", + ":TopologyTestApp", ], certificate: "platform", diff --git a/services/tests/displayservicetests/AndroidManifest.xml b/services/tests/displayservicetests/AndroidManifest.xml index 205ff058275a..76f219b7433b 100644 --- a/services/tests/displayservicetests/AndroidManifest.xml +++ b/services/tests/displayservicetests/AndroidManifest.xml @@ -29,6 +29,7 @@ <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" /> <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> <uses-permission android:name="android.permission.MANAGE_USB" /> + <uses-permission android:name="android.permission.MANAGE_DISPLAYS" /> <!-- Permissions needed for DisplayTransformManagerTest --> <uses-permission android:name="android.permission.CHANGE_CONFIGURATION" /> diff --git a/services/tests/displayservicetests/AndroidTest.xml b/services/tests/displayservicetests/AndroidTest.xml index f3697bbffd5c..2fe37233870f 100644 --- a/services/tests/displayservicetests/AndroidTest.xml +++ b/services/tests/displayservicetests/AndroidTest.xml @@ -28,6 +28,7 @@ <option name="cleanup-apks" value="true" /> <option name="install-arg" value="-t" /> <option name="test-file-name" value="DisplayManagerTestApp.apk" /> + <option name="test-file-name" value="TopologyTestApp.apk" /> </target_preparer> <option name="test-tag" value="DisplayServiceTests" /> diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java index 1f45792e5097..bf4b61347bab 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java @@ -16,7 +16,6 @@ package com.android.server.display; -import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; import static android.util.DisplayMetrics.DENSITY_HIGH; @@ -27,19 +26,11 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; -import android.app.ActivityManager; -import android.app.Instrumentation; -import android.content.Context; import android.content.Intent; -import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; -import android.os.BinderProxy; import android.os.Handler; -import android.os.HandlerThread; import android.os.Looper; import android.os.Message; -import android.os.Messenger; -import android.platform.test.annotations.AppModeSdkSandbox; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -48,10 +39,7 @@ import android.util.SparseArray; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; -import androidx.test.platform.app.InstrumentationRegistry; -import com.android.compatibility.common.util.SystemUtil; -import com.android.compatibility.common.util.TestUtils; import com.android.server.am.Flags; import org.junit.After; @@ -63,9 +51,7 @@ import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; -import java.io.IOException; import java.util.Arrays; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -73,8 +59,7 @@ import java.util.concurrent.TimeUnit; * Tests that applications can receive display events correctly. */ @RunWith(Parameterized.class) -@AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).") -public class DisplayEventDeliveryTest { +public class DisplayEventDeliveryTest extends EventDeliveryTestBase { private static final String TAG = "DisplayEventDeliveryTest"; @Rule @@ -85,37 +70,17 @@ public class DisplayEventDeliveryTest { private static final int WIDTH = 720; private static final int HEIGHT = 480; - private static final int MESSAGE_LAUNCHED = 1; - private static final int MESSAGE_CALLBACK = 2; - private static final int DISPLAY_ADDED = 1; private static final int DISPLAY_CHANGED = 2; private static final int DISPLAY_REMOVED = 3; - private static final long DISPLAY_EVENT_TIMEOUT_MSEC = 100; - private static final long TEST_FAILURE_TIMEOUT_MSEC = 10000; - private static final String TEST_PACKAGE = "com.android.servicestests.apps.displaymanagertestapp"; private static final String TEST_ACTIVITY = TEST_PACKAGE + ".DisplayEventActivity"; private static final String TEST_DISPLAYS = "DISPLAYS"; - private static final String TEST_MESSENGER = "MESSENGER"; private final Object mLock = new Object(); - private Instrumentation mInstrumentation; - private Context mContext; - private DisplayManager mDisplayManager; - private ActivityManager mActivityManager; - private ActivityManager.OnUidImportanceListener mUidImportanceListener; - private CountDownLatch mLatchActivityLaunch; - private CountDownLatch mLatchActivityCached; - private HandlerThread mHandlerThread; - private Handler mHandler; - private Messenger mMessenger; - private int mPid; - private int mUid; - /** * Array of DisplayBundle. The test handler uses it to check if certain display events have * been sent to DisplayEventActivity. @@ -167,7 +132,7 @@ public class DisplayEventDeliveryTest { */ public void assertNoDisplayEvents() { try { - assertNull(mExpectations.poll(DISPLAY_EVENT_TIMEOUT_MSEC, TimeUnit.MILLISECONDS)); + assertNull(mExpectations.poll(EVENT_TIMEOUT_MSEC, TimeUnit.MILLISECONDS)); } catch (InterruptedException e) { throw new RuntimeException(e); } @@ -239,37 +204,17 @@ public class DisplayEventDeliveryTest { } @Before - public void setUp() throws Exception { - mInstrumentation = InstrumentationRegistry.getInstrumentation(); - mContext = mInstrumentation.getContext(); - mDisplayManager = mContext.getSystemService(DisplayManager.class); - mLatchActivityLaunch = new CountDownLatch(1); - mLatchActivityCached = new CountDownLatch(1); - mActivityManager = mContext.getSystemService(ActivityManager.class); - mUidImportanceListener = (uid, importance) -> { - if (uid == mUid && importance == IMPORTANCE_CACHED) { - Log.d(TAG, "Listener " + uid + " becomes " + importance); - mLatchActivityCached.countDown(); - } - }; - SystemUtil.runWithShellPermissionIdentity(() -> - mActivityManager.addOnUidImportanceListener(mUidImportanceListener, - IMPORTANCE_CACHED)); + public void setUp() { + super.setUp(); // The lock is not functionally necessary but eliminates lint error messages. synchronized (mLock) { mDisplayBundles = new SparseArray<>(); } - mHandlerThread = new HandlerThread("handler"); - mHandlerThread.start(); - mHandler = new TestHandler(mHandlerThread.getLooper()); - mMessenger = new Messenger(mHandler); - mPid = 0; } @After public void tearDown() throws Exception { - mActivityManager.removeOnUidImportanceListener(mUidImportanceListener); - mHandlerThread.quitSafely(); + super.tearDown(); synchronized (mLock) { for (int i = 0; i < mDisplayBundles.size(); i++) { DisplayBundle bundle = mDisplayBundles.valueAt(i); @@ -278,7 +223,31 @@ public class DisplayEventDeliveryTest { } mDisplayBundles.clear(); } - SystemUtil.runShellCommand(mInstrumentation, "am force-stop " + TEST_PACKAGE); + } + + @Override + protected String getTag() { + return TAG; + } + + @Override + protected Handler getHandler(Looper looper) { + return new TestHandler(looper); + } + + @Override + protected String getTestPackage() { + return TEST_PACKAGE; + } + + @Override + protected String getTestActivity() { + return TEST_ACTIVITY; + } + + @Override + protected void putExtra(Intent intent) { + intent.putExtra(TEST_DISPLAYS, mDisplayCount); } /** @@ -291,42 +260,8 @@ public class DisplayEventDeliveryTest { } /** - * Return true if the freezer is enabled on this platform and if freezer notifications are - * supported. It is not enough to test that the freezer notification feature is enabled - * because some devices do not have the necessary kernel support. - */ - private boolean isAppFreezerEnabled() { - try { - return mActivityManager.getService().isAppFreezerEnabled() - && android.os.Flags.binderFrozenStateChangeCallback() - && BinderProxy.isFrozenStateChangeCallbackSupported(); - } catch (Exception e) { - Log.e(TAG, "isAppFreezerEnabled() failed: " + e); - return false; - } - } - - private void waitForProcessFreeze(int pid, long timeoutMs) { - // TODO: Add a listener to monitor freezer state changes. - SystemUtil.runWithShellPermissionIdentity(() -> { - TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid, - (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs), - () -> mActivityManager.isProcessFrozen(pid)); - }); - } - - private void waitForProcessUnfreeze(int pid, long timeoutMs) { - // TODO: Add a listener to monitor freezer state changes. - SystemUtil.runWithShellPermissionIdentity(() -> { - TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid, - (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs), - () -> !mActivityManager.isProcessFrozen(pid)); - }); - } - - /** - * Create virtual displays, change their configurations and release them. The number of - * displays is set by the {@link #mDisplays} variable. + * Create virtual displays, change their configurations and release them. The number of + * displays is set by the {@link #data()} parameter. */ private void testDisplayEventsInternal(boolean cached, boolean frozen) { Log.d(TAG, "Start test testDisplayEvents " + mDisplayCount + " " + cached + " " + frozen); @@ -445,110 +380,6 @@ public class DisplayEventDeliveryTest { } /** - * Launch the test activity that would listen to display events. Return its process ID. - */ - private int launchTestActivity() { - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setClassName(TEST_PACKAGE, TEST_ACTIVITY); - intent.putExtra(TEST_MESSENGER, mMessenger); - intent.putExtra(TEST_DISPLAYS, mDisplayCount); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - SystemUtil.runWithShellPermissionIdentity( - () -> { - mContext.startActivity(intent); - }, - android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX); - waitLatch(mLatchActivityLaunch); - - try { - String cmd = "pidof " + TEST_PACKAGE; - String result = SystemUtil.runShellCommand(mInstrumentation, cmd); - return Integer.parseInt(result.trim()); - } catch (IOException e) { - fail("failed to get pid of test package"); - return 0; - } catch (NumberFormatException e) { - fail("failed to parse pid " + e); - return 0; - } - } - - /** - * Bring the test activity back to top - */ - private void bringTestActivityTop() { - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setClassName(TEST_PACKAGE, TEST_ACTIVITY); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - SystemUtil.runWithShellPermissionIdentity( - () -> { - mContext.startActivity(intent); - }, - android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX); - } - - /** - * Bring the test activity into cached mode by launching another 2 apps - */ - private void makeTestActivityCached() { - // Launch another activity to bring the test activity into background - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setClass(mContext, SimpleActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - - // Launch another activity to bring the test activity into cached mode - Intent intent2 = new Intent(Intent.ACTION_MAIN); - intent2.setClass(mContext, SimpleActivity2.class); - intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - SystemUtil.runWithShellPermissionIdentity( - () -> { - mInstrumentation.startActivitySync(intent); - mInstrumentation.startActivitySync(intent2); - }, - android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX); - waitLatch(mLatchActivityCached); - } - - // Sleep, ignoring interrupts. - private void pause(int s) { - try { Thread.sleep(s * 1000); } catch (Exception e) { } - } - - /** - * Freeze the test activity. - */ - private void makeTestActivityFrozen(int pid) { - // The delay here is meant to allow pending binder transactions to drain. A process - // cannot be frozen if it has pending binder transactions, and attempting to freeze such a - // process more than a few times will result in the system killing the process. - pause(5); - try { - String cmd = "am freeze --sticky "; - SystemUtil.runShellCommand(mInstrumentation, cmd + TEST_PACKAGE); - } catch (IOException e) { - fail(e.toString()); - } - // Wait for the freeze to complete in the kernel and for the frozen process - // notification to settle out. - waitForProcessFreeze(pid, 5 * 1000); - } - - /** - * Freeze the test activity. - */ - private void makeTestActivityUnfrozen(int pid) { - try { - String cmd = "am unfreeze --sticky "; - SystemUtil.runShellCommand(mInstrumentation, cmd + TEST_PACKAGE); - } catch (IOException e) { - fail(e.toString()); - } - // Wait for the freeze to complete in the kernel and for the frozen process - // notification to settle out. - waitForProcessUnfreeze(pid, 5 * 1000); - } - - /** * Create a virtual display * * @param name The name of the new virtual display @@ -560,15 +391,4 @@ public class DisplayEventDeliveryTest { VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY /* flags: a public virtual display that another app can access */); } - - /** - * Wait for CountDownLatch with timeout - */ - private void waitLatch(CountDownLatch latch) { - try { - latch.await(TEST_FAILURE_TIMEOUT_MSEC, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/EventDeliveryTestBase.java b/services/tests/displayservicetests/src/com/android/server/display/EventDeliveryTestBase.java new file mode 100644 index 000000000000..2911b9bb35c7 --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/EventDeliveryTestBase.java @@ -0,0 +1,259 @@ +/* + * 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.display; + +import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED; + +import static org.junit.Assert.fail; + +import android.app.ActivityManager; +import android.app.Instrumentation; +import android.content.Context; +import android.content.Intent; +import android.hardware.display.DisplayManager; +import android.os.BinderProxy; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Messenger; +import android.platform.test.annotations.AppModeSdkSandbox; +import android.util.Log; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.compatibility.common.util.SystemUtil; +import com.android.compatibility.common.util.TestUtils; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@AppModeSdkSandbox(reason = "Allow test in the SDK sandbox (does not prevent other modes).") +public abstract class EventDeliveryTestBase { + protected static final int MESSAGE_LAUNCHED = 1; + protected static final int MESSAGE_CALLBACK = 2; + + protected static final long EVENT_TIMEOUT_MSEC = 100; + protected static final long TEST_FAILURE_TIMEOUT_MSEC = 10000; + + private static final String TEST_MESSENGER = "MESSENGER"; + + private Instrumentation mInstrumentation; + private Context mContext; + protected DisplayManager mDisplayManager; + private ActivityManager mActivityManager; + private ActivityManager.OnUidImportanceListener mUidImportanceListener; + protected CountDownLatch mLatchActivityLaunch; + private CountDownLatch mLatchActivityCached; + private HandlerThread mHandlerThread; + private Handler mHandler; + private Messenger mMessenger; + protected int mPid; + protected int mUid; + + protected abstract String getTag(); + + protected abstract Handler getHandler(Looper looper); + + protected abstract String getTestPackage(); + + protected abstract String getTestActivity(); + + protected abstract void putExtra(Intent intent); + + protected void setUp() { + mInstrumentation = InstrumentationRegistry.getInstrumentation(); + mContext = mInstrumentation.getContext(); + mDisplayManager = mContext.getSystemService(DisplayManager.class); + mLatchActivityLaunch = new CountDownLatch(1); + mLatchActivityCached = new CountDownLatch(1); + mActivityManager = mContext.getSystemService(ActivityManager.class); + mUidImportanceListener = (uid, importance) -> { + if (uid == mUid && importance == IMPORTANCE_CACHED) { + Log.d(getTag(), "Listener " + uid + " becomes " + importance); + mLatchActivityCached.countDown(); + } + }; + SystemUtil.runWithShellPermissionIdentity(() -> + mActivityManager.addOnUidImportanceListener(mUidImportanceListener, + IMPORTANCE_CACHED)); + mHandlerThread = new HandlerThread("handler"); + mHandlerThread.start(); + mHandler = getHandler(mHandlerThread.getLooper()); + mMessenger = new Messenger(mHandler); + mPid = 0; + } + + protected void tearDown() throws Exception { + mActivityManager.removeOnUidImportanceListener(mUidImportanceListener); + mHandlerThread.quitSafely(); + SystemUtil.runShellCommand(mInstrumentation, "am force-stop " + getTestPackage()); + } + + /** + * Return true if the freezer is enabled on this platform and if freezer notifications are + * supported. It is not enough to test that the freezer notification feature is enabled + * because some devices do not have the necessary kernel support. + */ + protected boolean isAppFreezerEnabled() { + try { + return ActivityManager.getService().isAppFreezerEnabled() + && android.os.Flags.binderFrozenStateChangeCallback() + && BinderProxy.isFrozenStateChangeCallbackSupported(); + } catch (Exception e) { + Log.e(getTag(), "isAppFreezerEnabled() failed: " + e); + return false; + } + } + + private void waitForProcessFreeze(int pid, long timeoutMs) { + // TODO: Add a listener to monitor freezer state changes. + SystemUtil.runWithShellPermissionIdentity(() -> { + TestUtils.waitUntil( + "Timed out waiting for test process to be frozen; pid=" + pid, + (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs), + () -> mActivityManager.isProcessFrozen(pid)); + }); + } + + private void waitForProcessUnfreeze(int pid, long timeoutMs) { + // TODO: Add a listener to monitor freezer state changes. + SystemUtil.runWithShellPermissionIdentity(() -> { + TestUtils.waitUntil("Timed out waiting for test process to be frozen; pid=" + pid, + (int) TimeUnit.MILLISECONDS.toSeconds(timeoutMs), + () -> !mActivityManager.isProcessFrozen(pid)); + }); + } + + /** + * Launch the test activity that would listen to events. Return its process ID. + */ + protected int launchTestActivity() { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClassName(getTestPackage(), getTestActivity()); + intent.putExtra(TEST_MESSENGER, mMessenger); + putExtra(intent); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + SystemUtil.runWithShellPermissionIdentity( + () -> { + mContext.startActivity(intent); + }, + android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX); + waitLatch(mLatchActivityLaunch); + + try { + String cmd = "pidof " + getTestPackage(); + String result = SystemUtil.runShellCommand(mInstrumentation, cmd); + return Integer.parseInt(result.trim()); + } catch (IOException e) { + fail("failed to get pid of test package"); + return 0; + } catch (NumberFormatException e) { + fail("failed to parse pid " + e); + return 0; + } + } + + /** + * Bring the test activity back to top + */ + protected void bringTestActivityTop() { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClassName(getTestPackage(), getTestActivity()); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + SystemUtil.runWithShellPermissionIdentity( + () -> { + mContext.startActivity(intent); + }, + android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX); + } + + + /** + * Bring the test activity into cached mode by launching another 2 apps + */ + protected void makeTestActivityCached() { + // Launch another activity to bring the test activity into background + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClass(mContext, SimpleActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + + // Launch another activity to bring the test activity into cached mode + Intent intent2 = new Intent(Intent.ACTION_MAIN); + intent2.setClass(mContext, SimpleActivity2.class); + intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + SystemUtil.runWithShellPermissionIdentity( + () -> { + mInstrumentation.startActivitySync(intent); + mInstrumentation.startActivitySync(intent2); + }, + android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX); + waitLatch(mLatchActivityCached); + } + + // Sleep, ignoring interrupts. + private void pause(int s) { + try { + Thread.sleep(s * 1000L); + } catch (Exception ignored) { } + } + + /** + * Freeze the test activity. + */ + protected void makeTestActivityFrozen(int pid) { + // The delay here is meant to allow pending binder transactions to drain. A process + // cannot be frozen if it has pending binder transactions, and attempting to freeze such a + // process more than a few times will result in the system killing the process. + pause(5); + try { + String cmd = "am freeze --sticky "; + SystemUtil.runShellCommand(mInstrumentation, cmd + getTestPackage()); + } catch (IOException e) { + fail(e.toString()); + } + // Wait for the freeze to complete in the kernel and for the frozen process + // notification to settle out. + waitForProcessFreeze(pid, 5 * 1000); + } + + /** + * Freeze the test activity. + */ + protected void makeTestActivityUnfrozen(int pid) { + try { + String cmd = "am unfreeze --sticky "; + SystemUtil.runShellCommand(mInstrumentation, cmd + getTestPackage()); + } catch (IOException e) { + fail(e.toString()); + } + // Wait for the freeze to complete in the kernel and for the frozen process + // notification to settle out. + waitForProcessUnfreeze(pid, 5 * 1000); + } + + /** + * Wait for CountDownLatch with timeout + */ + private void waitLatch(CountDownLatch latch) { + try { + latch.await(TEST_FAILURE_TIMEOUT_MSEC, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/services/tests/displayservicetests/src/com/android/server/display/TopologyUpdateDeliveryTest.java b/services/tests/displayservicetests/src/com/android/server/display/TopologyUpdateDeliveryTest.java new file mode 100644 index 000000000000..5fd248dba53f --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/TopologyUpdateDeliveryTest.java @@ -0,0 +1,243 @@ +/* + * 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.display; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import android.content.Intent; +import android.hardware.display.DisplayTopology; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.util.Log; + +import androidx.annotation.NonNull; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * Tests that applications can receive topology updates correctly. + */ +public class TopologyUpdateDeliveryTest extends EventDeliveryTestBase { + private static final String TAG = TopologyUpdateDeliveryTest.class.getSimpleName(); + + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + + private static final String TEST_PACKAGE = "com.android.servicestests.apps.topologytestapp"; + private static final String TEST_ACTIVITY = TEST_PACKAGE + ".TopologyUpdateActivity"; + + // Topology updates we expect to receive before timeout + private final LinkedBlockingQueue<DisplayTopology> mExpectations = new LinkedBlockingQueue<>(); + + /** + * Add the received topology update from the test activity to the queue + * + * @param topology The corresponding topology update + */ + private void addTopologyUpdate(DisplayTopology topology) { + Log.d(TAG, "Received " + topology); + mExpectations.offer(topology); + } + + /** + * Assert that there isn't any unexpected display event from the test activity + */ + private void assertNoTopologyUpdates() { + try { + assertNull(mExpectations.poll(EVENT_TIMEOUT_MSEC, TimeUnit.MILLISECONDS)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + /** + * Wait for the expected topology update from the test activity + * + * @param expect The expected topology update + */ + private void waitTopologyUpdate(DisplayTopology expect) { + while (true) { + try { + DisplayTopology update = mExpectations.poll(TEST_FAILURE_TIMEOUT_MSEC, + TimeUnit.MILLISECONDS); + assertNotNull(update); + if (expect.equals(update)) { + Log.d(TAG, "Found " + update); + return; + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private class TestHandler extends Handler { + TestHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(@NonNull Message msg) { + switch (msg.what) { + case MESSAGE_LAUNCHED: + mPid = msg.arg1; + mUid = msg.arg2; + Log.d(TAG, "Launched " + mPid + " " + mUid); + mLatchActivityLaunch.countDown(); + break; + case MESSAGE_CALLBACK: + DisplayTopology topology = (DisplayTopology) msg.obj; + Log.d(TAG, "Callback " + topology); + addTopologyUpdate(topology); + break; + default: + fail("Unexpected value: " + msg.what); + break; + } + } + } + + @Before + public void setUp() { + super.setUp(); + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + } + + @Override + protected String getTag() { + return TAG; + } + + @Override + protected Handler getHandler(Looper looper) { + return new TestHandler(looper); + } + + @Override + protected String getTestPackage() { + return TEST_PACKAGE; + } + + @Override + protected String getTestActivity() { + return TEST_ACTIVITY; + } + + @Override + protected void putExtra(Intent intent) { } + + private void testTopologyUpdateInternal(boolean cached, boolean frozen) { + Log.d(TAG, "Start test testTopologyUpdate " + cached + " " + frozen); + // Launch activity and start listening to topology updates + int pid = launchTestActivity(); + + // The test activity in cached or frozen mode won't receive the pending topology updates. + if (cached) { + makeTestActivityCached(); + } + if (frozen) { + makeTestActivityFrozen(pid); + } + + // Change the topology + int primaryDisplayId = 3; + DisplayTopology.TreeNode root = new DisplayTopology.TreeNode(primaryDisplayId, + /* width= */ 600, /* height= */ 400, DisplayTopology.TreeNode.POSITION_LEFT, + /* offset= */ 0); + DisplayTopology.TreeNode child = new DisplayTopology.TreeNode(/* displayId= */ 1, + /* width= */ 800, /* height= */ 600, DisplayTopology.TreeNode.POSITION_LEFT, + /* offset= */ 0); + root.addChild(child); + DisplayTopology topology = new DisplayTopology(root, primaryDisplayId); + mDisplayManager.setDisplayTopology(topology); + + if (cached || frozen) { + assertNoTopologyUpdates(); + } else { + waitTopologyUpdate(topology); + } + + // Unfreeze the test activity, if it was frozen. + if (frozen) { + makeTestActivityUnfrozen(pid); + } + + if (cached || frozen) { + // Always ensure the test activity is not cached. + bringTestActivityTop(); + + // The test activity becomes non-cached and should receive the pending topology updates + waitTopologyUpdate(topology); + } + } + + @Test + @RequiresFlagsEnabled(com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_TOPOLOGY) + public void testTopologyUpdate() { + testTopologyUpdateInternal(false, false); + } + + /** + * The app is moved to cached and the test verifies that no updates are delivered to the cached + * app. + */ + @Test + @RequiresFlagsEnabled(com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_TOPOLOGY) + public void testTopologyUpdateCached() { + testTopologyUpdateInternal(true, false); + } + + /** + * The app is frozen and the test verifies that no updates are delivered to the frozen app. + */ + @RequiresFlagsEnabled({com.android.server.am.Flags.FLAG_DEFER_DISPLAY_EVENTS_WHEN_FROZEN, + com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_TOPOLOGY}) + @Test + public void testTopologyUpdateFrozen() { + assumeTrue(isAppFreezerEnabled()); + testTopologyUpdateInternal(false, true); + } + + /** + * The app is cached and frozen and the test verifies that no updates are delivered to the app. + */ + @RequiresFlagsEnabled({com.android.server.am.Flags.FLAG_DEFER_DISPLAY_EVENTS_WHEN_FROZEN, + com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_TOPOLOGY}) + @Test + public void testTopologyUpdateCachedFrozen() { + assumeTrue(isAppFreezerEnabled()); + testTopologyUpdateInternal(true, true); + } +} diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt index 1f3f19fa3ea8..218728541774 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt @@ -89,6 +89,39 @@ class AppRequestObserverTest { assertThat(renderRateVote).isEqualTo(testCase.expectedRenderRateVote) } + @Test + fun testAppRequestVote_externalDisplay() { + val displayModeDirector = DisplayModeDirector( + context, testHandler, mockInjector, mockFlags, mockDisplayDeviceConfigProvider) + val modes = arrayOf( + Display.Mode(1, 1000, 1000, 60f), + Display.Mode(2, 1000, 1000, 90f), + ) + + displayModeDirector.injectAppSupportedModesByDisplay( + SparseArray<Array<Display.Mode>>().apply { + append(Display.DEFAULT_DISPLAY, modes) + }) + displayModeDirector.injectDefaultModeByDisplay(SparseArray<Display.Mode>().apply { + append(Display.DEFAULT_DISPLAY, modes[0]) + }) + displayModeDirector.addExternalDisplayId(Display.DEFAULT_DISPLAY) + + displayModeDirector.appRequestObserver.setAppRequest(Display.DEFAULT_DISPLAY, 1, 0f, 0f, 0f) + + val baseModeVote = displayModeDirector.getVote(Display.DEFAULT_DISPLAY, + Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE) + assertThat(baseModeVote).isEqualTo(BaseModeRefreshRateVote(60f)) + + val sizeVote = displayModeDirector.getVote(Display.DEFAULT_DISPLAY, + Vote.PRIORITY_APP_REQUEST_SIZE) + assertThat(sizeVote).isNull() + + val renderRateVote = displayModeDirector.getVote(Display.DEFAULT_DISPLAY, + Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE) + assertThat(renderRateVote).isNull() + } + enum class AppRequestTestCase( val ignoreRefreshRateRequest: Boolean, val modeId: Int, diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/ModeChangeObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/ModeChangeObserverTest.kt new file mode 100644 index 000000000000..a5fb6cdc8d4f --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/ModeChangeObserverTest.kt @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display.mode + +import android.os.Looper +import android.view.Display +import android.view.DisplayAddress +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +private const val PHYSICAL_DISPLAY_ID_1 = 1L +private const val PHYSICAL_DISPLAY_ID_2 = 2L +private const val MODE_ID_1 = 3 +private const val MODE_ID_2 = 4 +private const val LOGICAL_DISPLAY_ID = 5 +private val physicalAddress1 = DisplayAddress.fromPhysicalDisplayId(PHYSICAL_DISPLAY_ID_1) +private val physicalAddress2 = DisplayAddress.fromPhysicalDisplayId(PHYSICAL_DISPLAY_ID_2) + +/** + * Tests for ModeChangeObserver, comply with changes in b/31925610 + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +public class ModeChangeObserverTest { + @get:Rule + val mockitoRule = MockitoJUnit.rule() + + // System Under Test + private lateinit var modeChangeObserver: ModeChangeObserver + + // Non Mocks + private val looper = Looper.getMainLooper() + private val votesStorage = VotesStorage({}, null) + + // Mocks + private val mockInjector = mock<DisplayModeDirector.Injector>() + private val mockDisplay1 = mock<Display>() + private val mockDisplay2 = mock<Display>() + + @Before + fun setUp() { + whenever(mockInjector.getDisplay(LOGICAL_DISPLAY_ID)).thenReturn(mockDisplay1) + whenever(mockDisplay1.getAddress()).thenReturn(physicalAddress1) + whenever(mockInjector.getDisplays()).thenReturn(arrayOf<Display>()) + modeChangeObserver = ModeChangeObserver(votesStorage, mockInjector, looper) + modeChangeObserver.observe() + } + + @Test + fun testOnModeRejectedBeforeDisplayAdded() { + val rejectedModes = HashSet<Int>() + rejectedModes.add(MODE_ID_1) + rejectedModes.add(MODE_ID_2) + + // ModeRejected is called before display is mapped, hence votes are null + modeChangeObserver.mModeChangeListener.onModeRejected(PHYSICAL_DISPLAY_ID_1, MODE_ID_1) + modeChangeObserver.mModeChangeListener.onModeRejected(PHYSICAL_DISPLAY_ID_1, MODE_ID_2) + val votes = votesStorage.getVotes(LOGICAL_DISPLAY_ID) + assertThat(votes.size()).isEqualTo(0) + + // Display is mapped to a Logical Display Id, now the Rejected Mode Votes get updated + modeChangeObserver.mDisplayListener.onDisplayAdded(LOGICAL_DISPLAY_ID) + val newVotes = votesStorage.getVotes(LOGICAL_DISPLAY_ID) + assertThat(newVotes.size()).isEqualTo(1) + val vote = newVotes.get(Vote.PRIORITY_REJECTED_MODES) + assertThat(vote).isInstanceOf(RejectedModesVote::class.java) + val rejectedModesVote = vote as RejectedModesVote + assertThat(rejectedModesVote.mModeIds.size).isEqualTo(rejectedModes.size) + assertThat(rejectedModesVote.mModeIds).contains(MODE_ID_1) + assertThat(rejectedModesVote.mModeIds).contains(MODE_ID_2) + } + + @Test + fun testOnDisplayAddedBeforeOnModeRejected() { + // Display is mapped to the corresponding Logical Id, but Mode Rejected no received yet + // Verify that the Vote is still Null + modeChangeObserver.mDisplayListener.onDisplayAdded(LOGICAL_DISPLAY_ID) + val votes = votesStorage.getVotes(LOGICAL_DISPLAY_ID) + assertThat(votes.size()).isEqualTo(0) + + // ModeRejected Event received for the mapped display + modeChangeObserver.mModeChangeListener.onModeRejected(PHYSICAL_DISPLAY_ID_1, MODE_ID_1) + val newVotes = votesStorage.getVotes(LOGICAL_DISPLAY_ID) + assertThat(newVotes.size()).isEqualTo(1) + val vote = newVotes.get(Vote.PRIORITY_REJECTED_MODES) + assertThat(vote).isInstanceOf(RejectedModesVote::class.java) + val rejectedModesVote = vote as RejectedModesVote + assertThat(rejectedModesVote.mModeIds.size).isEqualTo(1) + assertThat(rejectedModesVote.mModeIds).contains(MODE_ID_1) + } + + @Test + fun testOnDisplayAddedThenRejectedThenRemoved() { + // Display is mapped to its Logical Display Id + modeChangeObserver.mDisplayListener.onDisplayAdded(LOGICAL_DISPLAY_ID) + val votes = votesStorage.getVotes(LOGICAL_DISPLAY_ID) + assertThat(votes.size()).isEqualTo(0) + + // ModeRejected Event is received for mapped display + modeChangeObserver.mModeChangeListener.onModeRejected(PHYSICAL_DISPLAY_ID_1, MODE_ID_1) + val newVotes = votesStorage.getVotes(LOGICAL_DISPLAY_ID) + assertThat(newVotes.size()).isEqualTo(1) + val vote = newVotes.get(Vote.PRIORITY_REJECTED_MODES) + assertThat(vote).isInstanceOf(RejectedModesVote::class.java) + val rejectedModesVote = vote as RejectedModesVote + assertThat(rejectedModesVote.mModeIds.size).isEqualTo(1) + assertThat(rejectedModesVote.mModeIds).contains(MODE_ID_1) + + // Display mapping is removed, hence remove the votes + modeChangeObserver.mDisplayListener.onDisplayRemoved(LOGICAL_DISPLAY_ID) + val finalVotes = votesStorage.getVotes(LOGICAL_DISPLAY_ID) + assertThat(finalVotes.size()).isEqualTo(0) + } + + @Test + fun testForModesRejectedAfterDisplayChanged() { + // Mock Display 1 is mapped to logicalId + modeChangeObserver.mDisplayListener.onDisplayAdded(LOGICAL_DISPLAY_ID) + val votes = votesStorage.getVotes(LOGICAL_DISPLAY_ID) + assertThat(votes.size()).isEqualTo(0) + + // Mode Rejected received for PhysicalId2 not mapped yet, so votes are null + whenever(mockInjector.getDisplay(LOGICAL_DISPLAY_ID)).thenReturn(mockDisplay2) + whenever(mockDisplay2.getAddress()).thenReturn(physicalAddress2) + modeChangeObserver.mModeChangeListener.onModeRejected(PHYSICAL_DISPLAY_ID_2, MODE_ID_2) + val changedVotes = votesStorage.getVotes(LOGICAL_DISPLAY_ID) + assertThat(changedVotes.size()).isEqualTo(0) + + // Display mapping changed, now PhysicalId2 is mapped to the LogicalId, votes get updated + modeChangeObserver.mDisplayListener.onDisplayChanged(LOGICAL_DISPLAY_ID) + val finalVotes = votesStorage.getVotes(LOGICAL_DISPLAY_ID) + assertThat(finalVotes.size()).isEqualTo(1) + val finalVote = finalVotes.get(Vote.PRIORITY_REJECTED_MODES) + assertThat(finalVote).isInstanceOf(RejectedModesVote::class.java) + val newRejectedModesVote = finalVote as RejectedModesVote + assertThat(newRejectedModesVote.mModeIds.size).isEqualTo(1) + assertThat(newRejectedModesVote.mModeIds).contains(MODE_ID_2) + } + + @Test + fun testForModesNotRejectedAfterDisplayChanged() { + // Mock Display 1 is added + modeChangeObserver.mDisplayListener.onDisplayAdded(LOGICAL_DISPLAY_ID) + val votes = votesStorage.getVotes(LOGICAL_DISPLAY_ID) + assertThat(votes.size()).isEqualTo(0) + + // Mode Rejected received for Display 1, votes added for rejected mode + modeChangeObserver.mModeChangeListener.onModeRejected(PHYSICAL_DISPLAY_ID_1, MODE_ID_1) + val newVotes = votesStorage.getVotes(LOGICAL_DISPLAY_ID) + assertThat(newVotes.size()).isEqualTo(1) + val vote = newVotes.get(Vote.PRIORITY_REJECTED_MODES) + assertThat(vote).isInstanceOf(RejectedModesVote::class.java) + val rejectedModesVote = vote as RejectedModesVote + assertThat(rejectedModesVote.mModeIds.size).isEqualTo(1) + assertThat(rejectedModesVote.mModeIds).contains(MODE_ID_1) + + // Display Changed such that logical Id corresponds to PhysicalDisplayId2 + // Rejected Modes Vote is removed + whenever(mockInjector.getDisplay(LOGICAL_DISPLAY_ID)).thenReturn(mockDisplay2) + whenever(mockDisplay2.getAddress()).thenReturn(physicalAddress2) + modeChangeObserver.mDisplayListener.onDisplayChanged(LOGICAL_DISPLAY_ID) + val finalVotes = votesStorage.getVotes(LOGICAL_DISPLAY_ID) + assertThat(finalVotes.size()).isEqualTo(0) + } +}
\ No newline at end of file diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java index 43becc59c3cb..558ea29d85fc 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java @@ -42,6 +42,8 @@ import android.os.Binder; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.Log; import androidx.test.filters.MediumTest; @@ -79,6 +81,8 @@ public class ProcessObserverTest { @Rule public final ApplicationExitInfoTest.ServiceThreadRule mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private Context mContext; private HandlerThread mHandlerThread; @@ -239,8 +243,8 @@ public class ProcessObserverTest { /** * Verify that a process start event is dispatched to process observers. */ - @Ignore("b/323959187") @Test + @EnableFlags(android.app.Flags.FLAG_ENABLE_PROCESS_OBSERVER_BROADCAST_ON_PROCESS_STARTED) public void testNormal() throws Exception { ProcessRecord app = startProcess(); verify(mProcessObserver).onProcessStarted( diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index d702cae248a9..067fba9893e5 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -31,6 +31,7 @@ android_test { "test-apps/SuspendTestApp/src/**/*.java", "test-apps/DisplayManagerTestApp/src/**/*.java", + "test-apps/TopologyTestApp/src/**/*.java", ], static_libs: [ @@ -141,6 +142,7 @@ android_test { data: [ ":DisplayManagerTestApp", + ":TopologyTestApp", ":SimpleServiceTestApp1", ":SimpleServiceTestApp2", ":SimpleServiceTestApp3", diff --git a/services/tests/servicestests/AndroidTest.xml b/services/tests/servicestests/AndroidTest.xml index 5298251b79f7..9a4983482522 100644 --- a/services/tests/servicestests/AndroidTest.xml +++ b/services/tests/servicestests/AndroidTest.xml @@ -36,6 +36,7 @@ <option name="cleanup-apks" value="true" /> <option name="install-arg" value="-t" /> <option name="test-file-name" value="DisplayManagerTestApp.apk" /> + <option name="test-file-name" value="TopologyTestApp.apk" /> <option name="test-file-name" value="FrameworksServicesTests.apk" /> <option name="test-file-name" value="SuspendTestApp.apk" /> <option name="test-file-name" value="SimpleServiceTestApp1.apk" /> diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 2ccd33648c3e..e0bc3e76f31d 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -82,6 +82,7 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.content.pm.UserInfo; import android.content.res.XmlResourceParser; import android.graphics.drawable.Icon; import android.hardware.display.DisplayManager; @@ -131,6 +132,7 @@ import com.android.internal.accessibility.util.ShortcutUtils; import com.android.internal.compat.IPlatformCompat; import com.android.internal.content.PackageMonitor; import com.android.server.LocalServices; +import com.android.server.SystemService; import com.android.server.accessibility.AccessibilityManagerService.AccessibilityDisplayListener; import com.android.server.accessibility.magnification.FullScreenMagnificationController; import com.android.server.accessibility.magnification.MagnificationConnectionManager; @@ -2122,6 +2124,36 @@ public class AccessibilityManagerServiceTest { } @Test + @EnableFlags(Flags.FLAG_MANAGER_LIFECYCLE_USER_CHANGE) + public void lifecycle_onUserSwitching_switchesUser() throws RemoteException { + mA11yms.mUserInitializationCompleteCallbacks.add(mUserInitializationCompleteCallback); + AccessibilityManagerService.Lifecycle lifecycle = + new AccessibilityManagerService.Lifecycle(mTestableContext, mA11yms); + int newUserId = mA11yms.getCurrentUserIdLocked() + 1; + + lifecycle.onUserSwitching( + new SystemService.TargetUser(new UserInfo(0, "USER", 0)), + new SystemService.TargetUser(new UserInfo(newUserId, "USER", 0))); + mTestableLooper.processAllMessages(); + + verify(mUserInitializationCompleteCallback).onUserInitializationComplete(newUserId); + } + + @Test + @DisableFlags(Flags.FLAG_MANAGER_LIFECYCLE_USER_CHANGE) + public void intent_user_switched_switchesUser() throws RemoteException { + mA11yms.mUserInitializationCompleteCallbacks.add(mUserInitializationCompleteCallback); + int newUserId = mA11yms.getCurrentUserIdLocked() + 1; + final Intent intent = new Intent(Intent.ACTION_USER_SWITCHED); + intent.putExtra(Intent.EXTRA_USER_HANDLE, newUserId); + + sendBroadcastToAccessibilityManagerService(intent, mA11yms.getCurrentUserIdLocked()); + mTestableLooper.processAllMessages(); + + verify(mUserInitializationCompleteCallback).onUserInitializationComplete(newUserId); + } + + @Test @DisableFlags(android.provider.Flags.FLAG_A11Y_STANDALONE_GESTURE_ENABLED) public void getShortcutTypeForGenericShortcutCalls_softwareType() { final AccessibilityUserState userState = new AccessibilityUserState( 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 99c922ca30c4..df77866b5e7f 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 @@ -25,6 +25,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -866,6 +867,23 @@ public class AutoclickControllerTest { @Test @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void scrollPanelController_directionalButtonsHideIndicator() { + injectFakeMouseActionHoverMoveEvent(); + + // Create a spy on the real object to verify method calls. + AutoclickIndicatorView spyIndicatorView = spy(mController.mAutoclickIndicatorView); + mController.mAutoclickIndicatorView = spyIndicatorView; + + // Simulate hover on direction button. + mController.mScrollPanelController.onHoverButtonChange( + AutoclickScrollPanel.DIRECTION_UP, true); + + // Verify clearIndicator was called. + verify(spyIndicatorView).clearIndicator(); + } + + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) public void hoverOnAutoclickPanel_rightClickType_forceTriggerLeftClick() { MotionEventCaptor motionEventCaptor = new MotionEventCaptor(); mController.setNext(motionEventCaptor); diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioVolumeChangeHandlerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioVolumeChangeHandlerTest.java new file mode 100644 index 000000000000..f252a9848bbf --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/audio/AudioVolumeChangeHandlerTest.java @@ -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.server.audio; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.media.INativeAudioVolumeGroupCallback; +import android.media.audio.common.AudioVolumeGroupChangeEvent; +import android.media.audiopolicy.IAudioVolumeChangeDispatcher; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.MediumTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +import java.util.ArrayList; +import java.util.List; + +@MediumTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class AudioVolumeChangeHandlerTest { + private static final long DEFAULT_TIMEOUT_MS = 1000; + + private AudioSystemAdapter mSpyAudioSystem; + + AudioVolumeChangeHandler mAudioVolumeChangedHandler; + + private final IAudioVolumeChangeDispatcher.Stub mMockDispatcher = + mock(IAudioVolumeChangeDispatcher.Stub.class); + + @Before + public void setUp() { + mSpyAudioSystem = spy(new NoOpAudioSystemAdapter()); + when(mMockDispatcher.asBinder()).thenReturn(mock(IBinder.class)); + mAudioVolumeChangedHandler = new AudioVolumeChangeHandler(mSpyAudioSystem); + } + + @Test + public void registerListener_withInvalidCallback() { + IAudioVolumeChangeDispatcher.Stub nullCb = null; + NullPointerException thrown = assertThrows(NullPointerException.class, () -> { + mAudioVolumeChangedHandler.registerListener(nullCb); + }); + + assertWithMessage("Exception for invalid registration").that(thrown).hasMessageThat() + .contains("Volume group callback"); + } + + @Test + public void unregisterListener_withInvalidCallback() { + IAudioVolumeChangeDispatcher.Stub nullCb = null; + mAudioVolumeChangedHandler.registerListener(mMockDispatcher); + + NullPointerException thrown = assertThrows(NullPointerException.class, () -> { + mAudioVolumeChangedHandler.unregisterListener(nullCb); + }); + + assertWithMessage("Exception for invalid un-registration").that(thrown).hasMessageThat() + .contains("Volume group callback"); + } + + @Test + public void registerListener() { + mAudioVolumeChangedHandler.registerListener(mMockDispatcher); + + verify(mSpyAudioSystem).registerAudioVolumeGroupCallback(any()); + } + + @Test + public void onAudioVolumeGroupChanged() throws Exception { + mAudioVolumeChangedHandler.registerListener(mMockDispatcher); + AudioVolumeGroupChangeEvent volEvent = new AudioVolumeGroupChangeEvent(); + volEvent.groupId = 666; + volEvent.flags = AudioVolumeGroupChangeEvent.VOLUME_FLAG_FROM_KEY; + + captureRegisteredNativeCallback().onAudioVolumeGroupChanged(volEvent); + + verify(mMockDispatcher, timeout(DEFAULT_TIMEOUT_MS)).onAudioVolumeGroupChanged( + eq(volEvent.groupId), eq(volEvent.flags)); + } + + @Test + public void onAudioVolumeGroupChanged_withMultipleCallback() throws Exception { + int callbackCount = 10; + List<IAudioVolumeChangeDispatcher.Stub> validCbs = + new ArrayList<IAudioVolumeChangeDispatcher.Stub>(); + for (int i = 0; i < callbackCount; i++) { + IAudioVolumeChangeDispatcher.Stub cb = mock(IAudioVolumeChangeDispatcher.Stub.class); + when(cb.asBinder()).thenReturn(mock(IBinder.class)); + validCbs.add(cb); + } + for (IAudioVolumeChangeDispatcher.Stub cb : validCbs) { + mAudioVolumeChangedHandler.registerListener(cb); + } + AudioVolumeGroupChangeEvent volEvent = new AudioVolumeGroupChangeEvent(); + volEvent.groupId = 666; + volEvent.flags = AudioVolumeGroupChangeEvent.VOLUME_FLAG_FROM_KEY; + captureRegisteredNativeCallback().onAudioVolumeGroupChanged(volEvent); + + for (IAudioVolumeChangeDispatcher.Stub cb : validCbs) { + verify(cb, timeout(DEFAULT_TIMEOUT_MS)).onAudioVolumeGroupChanged( + eq(volEvent.groupId), eq(volEvent.flags)); + } + } + + private INativeAudioVolumeGroupCallback captureRegisteredNativeCallback() { + ArgumentCaptor<INativeAudioVolumeGroupCallback> nativeAudioVolumeGroupCallbackCaptor = + ArgumentCaptor.forClass(INativeAudioVolumeGroupCallback.class); + verify(mSpyAudioSystem, timeout(DEFAULT_TIMEOUT_MS)) + .registerAudioVolumeGroupCallback(nativeAudioVolumeGroupCallbackCaptor.capture()); + return nativeAudioVolumeGroupCallbackCaptor.getValue(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java index 43b1ec393bf6..87cd1560509c 100644 --- a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java +++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java @@ -19,7 +19,9 @@ package com.android.server.location.contexthub; 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.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; @@ -29,6 +31,7 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.hardware.contexthub.EndpointInfo; import android.hardware.contexthub.ErrorCode; +import android.hardware.contexthub.HubEndpoint; import android.hardware.contexthub.HubEndpointInfo; import android.hardware.contexthub.HubEndpointInfo.HubEndpointIdentifier; import android.hardware.contexthub.HubMessage; @@ -385,6 +388,49 @@ public class ContextHubEndpointTest { unregisterExampleEndpoint(endpoint); } + @Test + public void testUnreliableMessageFailureClosesSession() throws RemoteException { + IContextHubEndpoint endpoint = registerExampleEndpoint(); + int sessionId = openTestSession(endpoint); + + doThrow(new RemoteException("Intended exception in test")) + .when(mMockCallback) + .onMessageReceived(anyInt(), any(HubMessage.class)); + mEndpointManager.onMessageReceived(sessionId, SAMPLE_UNRELIABLE_MESSAGE); + ArgumentCaptor<HubMessage> messageCaptor = ArgumentCaptor.forClass(HubMessage.class); + verify(mMockCallback).onMessageReceived(eq(sessionId), messageCaptor.capture()); + assertThat(messageCaptor.getValue()).isEqualTo(SAMPLE_UNRELIABLE_MESSAGE); + + verify(mMockEndpointCommunications).closeEndpointSession(sessionId, Reason.UNSPECIFIED); + verify(mMockCallback).onSessionClosed(sessionId, HubEndpoint.REASON_FAILURE); + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE); + + unregisterExampleEndpoint(endpoint); + } + + @Test + public void testSendUnreliableMessageFailureClosesSession() throws RemoteException { + IContextHubEndpoint endpoint = registerExampleEndpoint(); + int sessionId = openTestSession(endpoint); + + doThrow(new RemoteException("Intended exception in test")) + .when(mMockEndpointCommunications) + .sendMessageToEndpoint(anyInt(), any(Message.class)); + endpoint.sendMessage(sessionId, SAMPLE_UNRELIABLE_MESSAGE, /* callback= */ null); + ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class); + verify(mMockEndpointCommunications) + .sendMessageToEndpoint(eq(sessionId), messageCaptor.capture()); + Message message = messageCaptor.getValue(); + assertThat(message.type).isEqualTo(SAMPLE_UNRELIABLE_MESSAGE.getMessageType()); + assertThat(message.content).isEqualTo(SAMPLE_UNRELIABLE_MESSAGE.getMessageBody()); + + verify(mMockEndpointCommunications).closeEndpointSession(sessionId, Reason.UNSPECIFIED); + verify(mMockCallback).onSessionClosed(sessionId, HubEndpoint.REASON_FAILURE); + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE); + + unregisterExampleEndpoint(endpoint); + } + /** A helper method to create a session and validates reliable message sending. */ private void testMessageTransactionInternal( IContextHubEndpoint endpoint, boolean deliverMessageStatus) throws RemoteException { diff --git a/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java index 148c96850d34..6d682ccef98d 100644 --- a/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java @@ -36,6 +36,7 @@ import static android.app.StatusBarManager.DISABLE_SYSTEM_INFO; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.nullable; @@ -69,16 +70,20 @@ import android.os.Binder; import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; +import android.platform.test.annotations.EnableFlags; import android.service.quicksettings.TileService; import android.testing.TestableContext; +import android.util.Pair; import androidx.test.InstrumentationRegistry; +import com.android.internal.statusbar.DisableStates; import com.android.internal.statusbar.IAddTileResultCallback; import com.android.internal.statusbar.IStatusBar; import com.android.server.LocalServices; import com.android.server.policy.GlobalActionsProvider; import com.android.server.wm.ActivityTaskManagerInternal; +import com.android.systemui.shared.Flags; import libcore.junit.util.compat.CoreCompatChangeRule; @@ -105,6 +110,7 @@ public class StatusBarManagerServiceTest { TEST_SERVICE); private static final CharSequence APP_NAME = "AppName"; private static final CharSequence TILE_LABEL = "Tile label"; + private static final int SECONDARY_DISPLAY_ID = 2; @Rule public final TestableContext mContext = @@ -749,6 +755,40 @@ public class StatusBarManagerServiceTest { } @Test + @EnableFlags(Flags.FLAG_STATUS_BAR_CONNECTED_DISPLAYS) + public void testDisableForAllDisplays() throws Exception { + int user1Id = 0; + mockUidCheck(); + mockCurrentUserCheck(user1Id); + + mStatusBarManagerService.onDisplayAdded(SECONDARY_DISPLAY_ID); + + int expectedFlags = DISABLE_MASK & DISABLE_BACK; + String pkg = mContext.getPackageName(); + + // before disabling + assertEquals(DISABLE_NONE, + mStatusBarManagerService.getDisableFlags(mMockStatusBar, user1Id)[0]); + + // disable + mStatusBarManagerService.disable(expectedFlags, mMockStatusBar, pkg); + + ArgumentCaptor<DisableStates> disableStatesCaptor = ArgumentCaptor.forClass( + DisableStates.class); + verify(mMockStatusBar).disableForAllDisplays(disableStatesCaptor.capture()); + DisableStates capturedDisableStates = disableStatesCaptor.getValue(); + assertTrue(capturedDisableStates.animate); + assertEquals(capturedDisableStates.displaysWithStates.size(), 2); + Pair<Integer, Integer> display0States = capturedDisableStates.displaysWithStates.get(0); + assertEquals((int) display0States.first, expectedFlags); + assertEquals((int) display0States.second, 0); + Pair<Integer, Integer> display2States = capturedDisableStates.displaysWithStates.get( + SECONDARY_DISPLAY_ID); + assertEquals((int) display2States.first, expectedFlags); + assertEquals((int) display2States.second, 0); + } + + @Test public void testSetHomeDisabled() throws Exception { int expectedFlags = DISABLE_MASK & DISABLE_HOME; String pkg = mContext.getPackageName(); @@ -851,6 +891,40 @@ public class StatusBarManagerServiceTest { } @Test + @EnableFlags(Flags.FLAG_STATUS_BAR_CONNECTED_DISPLAYS) + public void testDisable2ForAllDisplays() throws Exception { + int user1Id = 0; + mockUidCheck(); + mockCurrentUserCheck(user1Id); + + mStatusBarManagerService.onDisplayAdded(SECONDARY_DISPLAY_ID); + + int expectedFlags = DISABLE2_MASK & DISABLE2_NOTIFICATION_SHADE; + String pkg = mContext.getPackageName(); + + // before disabling + assertEquals(DISABLE_NONE, + mStatusBarManagerService.getDisableFlags(mMockStatusBar, user1Id)[0]); + + // disable + mStatusBarManagerService.disable2(expectedFlags, mMockStatusBar, pkg); + + ArgumentCaptor<DisableStates> disableStatesCaptor = ArgumentCaptor.forClass( + DisableStates.class); + verify(mMockStatusBar).disableForAllDisplays(disableStatesCaptor.capture()); + DisableStates capturedDisableStates = disableStatesCaptor.getValue(); + assertTrue(capturedDisableStates.animate); + assertEquals(capturedDisableStates.displaysWithStates.size(), 2); + Pair<Integer, Integer> display0States = capturedDisableStates.displaysWithStates.get(0); + assertEquals((int) display0States.first, 0); + assertEquals((int) display0States.second, expectedFlags); + Pair<Integer, Integer> display2States = capturedDisableStates.displaysWithStates.get( + SECONDARY_DISPLAY_ID); + assertEquals((int) display2States.first, 0); + assertEquals((int) display2States.second, expectedFlags); + } + + @Test public void testSetQuickSettingsDisabled2() throws Exception { int expectedFlags = DISABLE2_MASK & DISABLE2_QUICK_SETTINGS; String pkg = mContext.getPackageName(); diff --git a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java index 857a9767d9ee..842c441e09f2 100644 --- a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java @@ -452,14 +452,14 @@ public class SystemConfigTest { + " <library \n" + " name=\"foo\"\n" + " file=\"" + mFooJar + "\"\n" - + " on-bootclasspath-before=\"A\"\n" + + " on-bootclasspath-before=\"Q\"\n" + " on-bootclasspath-since=\"W\"\n" + " />\n\n" + " </permissions>"; parseSharedLibraries(contents); assertFooIsOnlySharedLibrary(); SystemConfig.SharedLibraryEntry entry = mSysConfig.getSharedLibraries().get("foo"); - assertThat(entry.onBootclasspathBefore).isEqualTo("A"); + assertThat(entry.onBootclasspathBefore).isEqualTo("Q"); assertThat(entry.onBootclasspathSince).isEqualTo("W"); } diff --git a/services/tests/servicestests/test-apps/TopologyTestApp/Android.bp b/services/tests/servicestests/test-apps/TopologyTestApp/Android.bp new file mode 100644 index 000000000000..dcf9cc216687 --- /dev/null +++ b/services/tests/servicestests/test-apps/TopologyTestApp/Android.bp @@ -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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test_helper_app { + name: "TopologyTestApp", + + srcs: ["**/*.java"], + + dex_preopt: { + enabled: false, + }, + optimize: { + enabled: false, + }, + + platform_apis: true, + certificate: "platform", +} diff --git a/services/tests/servicestests/test-apps/TopologyTestApp/AndroidManifest.xml b/services/tests/servicestests/test-apps/TopologyTestApp/AndroidManifest.xml new file mode 100644 index 000000000000..dad2315148df --- /dev/null +++ b/services/tests/servicestests/test-apps/TopologyTestApp/AndroidManifest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.servicestests.apps.topologytestapp"> + + <uses-permission android:name="android.permission.MANAGE_DISPLAYS" /> + + <application android:label="TopologyUpdateTestApp"> + <activity android:name="com.android.servicestests.apps.topologytestapp.TopologyUpdateActivity" + android:exported="true" /> + </application> + +</manifest> diff --git a/services/tests/servicestests/test-apps/TopologyTestApp/OWNERS b/services/tests/servicestests/test-apps/TopologyTestApp/OWNERS new file mode 100644 index 000000000000..e9557f84f8fb --- /dev/null +++ b/services/tests/servicestests/test-apps/TopologyTestApp/OWNERS @@ -0,0 +1,3 @@ +# Bug component: 345010 + +include /services/core/java/com/android/server/display/OWNERS diff --git a/services/tests/servicestests/test-apps/TopologyTestApp/src/com/android/servicestests/apps/topologytestapp/TopologyUpdateActivity.java b/services/tests/servicestests/test-apps/TopologyTestApp/src/com/android/servicestests/apps/topologytestapp/TopologyUpdateActivity.java new file mode 100644 index 000000000000..b35ba3c2c60c --- /dev/null +++ b/services/tests/servicestests/test-apps/TopologyTestApp/src/com/android/servicestests/apps/topologytestapp/TopologyUpdateActivity.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 com.android.servicestests.apps.topologytestapp; + +import android.app.Activity; +import android.content.Intent; +import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayTopology; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.os.Process; +import android.os.RemoteException; +import android.util.Log; + +import java.util.function.Consumer; + +/** + * A simple activity listening to topology updates + */ +public class TopologyUpdateActivity extends Activity { + public static final int MESSAGE_LAUNCHED = 1; + public static final int MESSAGE_CALLBACK = 2; + + private static final String TAG = TopologyUpdateActivity.class.getSimpleName(); + + private static final String TEST_MESSENGER = "MESSENGER"; + + private Messenger mMessenger; + private DisplayManager mDisplayManager; + private final Consumer<DisplayTopology> mTopologyListener = this::callback; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + mMessenger = intent.getParcelableExtra(TEST_MESSENGER, Messenger.class); + mDisplayManager = getApplicationContext().getSystemService(DisplayManager.class); + mDisplayManager.registerTopologyListener(getMainExecutor(), mTopologyListener); + launched(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mDisplayManager.unregisterTopologyListener(mTopologyListener); + } + + private void launched() { + try { + Message msg = Message.obtain(); + msg.what = MESSAGE_LAUNCHED; + msg.arg1 = android.os.Process.myPid(); + msg.arg2 = Process.myUid(); + Log.d(TAG, "Launched"); + mMessenger.send(msg); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + + private void callback(DisplayTopology topology) { + try { + Message msg = Message.obtain(); + msg.what = MESSAGE_CALLBACK; + msg.obj = topology; + Log.d(TAG, "Msg " + topology); + mMessenger.send(msg); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } +} diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java index fcdf88f16550..0495e967c0e3 100644 --- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java @@ -39,8 +39,6 @@ import androidx.test.filters.MediumTest; import com.android.hardware.input.Flags; import com.android.internal.annotations.Keep; -import junit.framework.Assert; - import junitparams.JUnitParamsRunner; import junitparams.Parameters; @@ -433,112 +431,94 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { @Test public void testKeyGestureRecentApps() { - Assert.assertTrue( - sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS); mPhoneWindowManager.assertShowRecentApps(); } @Test public void testKeyGestureAppSwitch() { - Assert.assertTrue( - sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH); mPhoneWindowManager.assertToggleRecentApps(); } @Test public void testKeyGestureLaunchAssistant() { - Assert.assertTrue( - sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT); mPhoneWindowManager.assertSearchManagerLaunchAssist(); } @Test public void testKeyGestureLaunchVoiceAssistant() { - Assert.assertTrue( - sendKeyGestureEventComplete( - KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT); mPhoneWindowManager.assertSearchManagerLaunchAssist(); } @Test public void testKeyGestureGoHome() { - Assert.assertTrue( - sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_HOME)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_HOME); mPhoneWindowManager.assertGoToHomescreen(); } @Test public void testKeyGestureLaunchSystemSettings() { - Assert.assertTrue( - sendKeyGestureEventComplete( - KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS); mPhoneWindowManager.assertLaunchSystemSettings(); } @Test public void testKeyGestureLock() { - Assert.assertTrue( - sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN); mPhoneWindowManager.assertLockedAfterAppTransitionFinished(); } @Test public void testKeyGestureToggleNotificationPanel() throws RemoteException { - Assert.assertTrue( - sendKeyGestureEventComplete( - KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL); mPhoneWindowManager.assertTogglePanel(); } @Test public void testKeyGestureScreenshot() { - Assert.assertTrue( - sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT); mPhoneWindowManager.assertTakeScreenshotCalled(); } @Test public void testKeyGestureTriggerBugReport() throws RemoteException { - Assert.assertTrue( - sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT); mPhoneWindowManager.assertTakeBugreport(true); } @Test public void testKeyGestureBack() { - Assert.assertTrue(sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_BACK)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_BACK); mPhoneWindowManager.assertBackEventInjected(); } @Test public void testKeyGestureMultiWindowNavigation() { - Assert.assertTrue(sendKeyGestureEventComplete( - KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION); mPhoneWindowManager.assertMoveFocusedTaskToFullscreen(); } @Test public void testKeyGestureDesktopMode() { - Assert.assertTrue( - sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE); mPhoneWindowManager.assertMoveFocusedTaskToDesktop(); } @Test public void testKeyGestureSplitscreenNavigation() { - Assert.assertTrue(sendKeyGestureEventComplete( - KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT); mPhoneWindowManager.assertMoveFocusedTaskToStageSplit(true); - Assert.assertTrue(sendKeyGestureEventComplete( - KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT); mPhoneWindowManager.assertMoveFocusedTaskToStageSplit(false); } @Test public void testKeyGestureShortcutHelper() { - Assert.assertTrue(sendKeyGestureEventComplete( - KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER); mPhoneWindowManager.assertToggleShortcutsMenu(); } @@ -549,173 +529,139 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { for (int i = 0; i < currentBrightness.length; i++) { mPhoneWindowManager.prepareBrightnessDecrease(currentBrightness[i]); - Assert.assertTrue( - sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN); mPhoneWindowManager.verifyNewBrightness(newBrightness[i]); } } @Test public void testKeyGestureRecentAppSwitcher() { - Assert.assertTrue(sendKeyGestureEventStart( - KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER)); + sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER); mPhoneWindowManager.assertShowRecentApps(); - - Assert.assertTrue(sendKeyGestureEventComplete( - KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER); mPhoneWindowManager.assertHideRecentApps(); } @Test public void testKeyGestureLanguageSwitch() { - Assert.assertTrue( - sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH); mPhoneWindowManager.assertSwitchKeyboardLayout(1, DEFAULT_DISPLAY); - Assert.assertTrue( - sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH, - KeyEvent.META_SHIFT_ON)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH, + KeyEvent.META_SHIFT_ON); mPhoneWindowManager.assertSwitchKeyboardLayout(-1, DEFAULT_DISPLAY); } @Test public void testKeyGestureLaunchSearch() { - Assert.assertTrue( - sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH); mPhoneWindowManager.assertLaunchSearch(); } @Test public void testKeyGestureScreenshotChord() { - Assert.assertTrue( - sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD)); + sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD); mPhoneWindowManager.moveTimeForward(500); - Assert.assertTrue( - sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD)); + sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD); mPhoneWindowManager.assertTakeScreenshotCalled(); } @Test public void testKeyGestureScreenshotChordCancelled() { - Assert.assertTrue( - sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD)); - Assert.assertTrue( - sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD)); + sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD); + sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD); mPhoneWindowManager.assertTakeScreenshotNotCalled(); } @Test public void testKeyGestureRingerToggleChord() { mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE); - Assert.assertTrue( - sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD)); + sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD); mPhoneWindowManager.moveTimeForward(500); - Assert.assertTrue( - sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD)); + sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD); mPhoneWindowManager.assertVolumeMute(); } @Test public void testKeyGestureRingerToggleChordCancelled() { mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE); - Assert.assertTrue( - sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD)); - Assert.assertTrue( - sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD)); + sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD); + sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD); mPhoneWindowManager.assertVolumeNotMuted(); } @Test public void testKeyGestureGlobalAction() { mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS); - Assert.assertTrue( - sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS)); + sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS); mPhoneWindowManager.moveTimeForward(500); - Assert.assertTrue( - sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS)); + sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS); mPhoneWindowManager.assertShowGlobalActionsCalled(); } @Test public void testKeyGestureGlobalActionCancelled() { mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS); - Assert.assertTrue( - sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS)); - Assert.assertTrue( - sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS)); + sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS); + sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS); mPhoneWindowManager.assertShowGlobalActionsNotCalled(); } @Test public void testKeyGestureTvTriggerBugReport() { - Assert.assertTrue( - sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT)); + sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT); mPhoneWindowManager.moveTimeForward(1000); - Assert.assertTrue( - sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT)); + sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT); mPhoneWindowManager.assertBugReportTakenForTv(); } @Test public void testKeyGestureTvTriggerBugReportCancelled() { - Assert.assertTrue( - sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT)); - Assert.assertTrue( - sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT)); + sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT); + sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT); mPhoneWindowManager.assertBugReportNotTakenForTv(); } @Test public void testKeyGestureAccessibilityShortcut() { - Assert.assertTrue( - sendKeyGestureEventComplete( - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT); mPhoneWindowManager.assertAccessibilityKeychordCalled(); } @Test public void testKeyGestureCloseAllDialogs() { - Assert.assertTrue( - sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS); mPhoneWindowManager.assertCloseAllDialogs(); } @Test @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES) public void testKeyGestureToggleTalkback() { - Assert.assertTrue( - sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK); mPhoneWindowManager.assertTalkBack(true); - Assert.assertTrue( - sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK); mPhoneWindowManager.assertTalkBack(false); } @Test @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES) public void testKeyGestureToggleVoiceAccess() { - Assert.assertTrue( - sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS); mPhoneWindowManager.assertVoiceAccess(true); - Assert.assertTrue( - sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS); mPhoneWindowManager.assertVoiceAccess(false); } @Test public void testKeyGestureToggleDoNotDisturb() { mPhoneWindowManager.overrideZenMode(Settings.Global.ZEN_MODE_OFF); - Assert.assertTrue( - sendKeyGestureEventComplete( - KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB); mPhoneWindowManager.assertZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS); mPhoneWindowManager.overrideZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS); - Assert.assertTrue( - sendKeyGestureEventComplete( - KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB)); + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB); mPhoneWindowManager.assertZenMode(Settings.Global.ZEN_MODE_OFF); } diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java index 32a3b7f2c9cc..f3d5e39ec127 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java @@ -35,6 +35,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.policy.PhoneWindowManager.EXTRA_TRIGGER_HUB; +import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP; import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP; import static com.google.common.truth.Truth.assertThat; @@ -43,6 +44,7 @@ import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -63,6 +65,7 @@ import android.view.contentprotection.flags.Flags; import androidx.test.filters.SmallTest; import com.android.internal.util.test.LocalServiceKeeperRule; +import com.android.internal.widget.LockPatternUtils; import com.android.server.input.InputManagerInternal; import com.android.server.pm.UserManagerInternal; import com.android.server.policy.keyguard.KeyguardServiceDelegate; @@ -119,6 +122,8 @@ public class PhoneWindowManagerTests { private DisplayPolicy mDisplayPolicy; @Mock private KeyguardServiceDelegate mKeyguardServiceDelegate; + @Mock + private LockPatternUtils mLockPatternUtils; @Before public void setUp() { @@ -146,7 +151,7 @@ public class PhoneWindowManagerTests { mPhoneWindowManager.mKeyguardDelegate = mKeyguardServiceDelegate; final InputManager im = mock(InputManager.class); - doNothing().when(im).registerKeyGestureEventHandler(any()); + doNothing().when(im).registerKeyGestureEventHandler(anyList(), any()); doReturn(im).when(mContext).getSystemService(eq(Context.INPUT_SERVICE)); } @@ -253,6 +258,7 @@ public class PhoneWindowManagerTests { @Test public void powerPress_hubOrDreamOrSleep_goesToSleepFromDream() { when(mDisplayPolicy.isAwake()).thenReturn(true); + when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false); initPhoneWindowManager(); // Set power button behavior. @@ -274,6 +280,7 @@ public class PhoneWindowManagerTests { @Test public void powerPress_hubOrDreamOrSleep_hubAvailableLocks() { when(mDisplayPolicy.isAwake()).thenReturn(true); + when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false); mContext.getTestablePermissions().setPermission(android.Manifest.permission.DEVICE_POWER, PERMISSION_GRANTED); initPhoneWindowManager(); @@ -302,6 +309,7 @@ public class PhoneWindowManagerTests { @Test public void powerPress_hubOrDreamOrSleep_hubNotAvailableDreams() { when(mDisplayPolicy.isAwake()).thenReturn(true); + when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false); initPhoneWindowManager(); // Set power button behavior. @@ -322,6 +330,77 @@ public class PhoneWindowManagerTests { verify(mDreamManagerInternal).requestDream(); } + @Test + public void powerPress_dreamOrAwakeOrSleep_awakeFromDream() { + when(mDisplayPolicy.isAwake()).thenReturn(true); + initPhoneWindowManager(); + + // Set power button behavior. + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.POWER_BUTTON_SHORT_PRESS, + SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP); + mPhoneWindowManager.updateSettings(null); + + // Can not dream when device is dreaming. + when(mDreamManagerInternal.canStartDreaming(any(Boolean.class))).thenReturn(false); + // Device is dreaming. + when(mDreamManagerInternal.isDreaming()).thenReturn(true); + + // Power button pressed. + int eventTime = 0; + mPhoneWindowManager.powerPress(eventTime, 1, 0); + + // Dream is stopped. + verify(mDreamManagerInternal) + .stopDream(false /*immediate*/, "short press power" /*reason*/); + } + + @Test + public void powerPress_dreamOrAwakeOrSleep_canNotDreamGoToSleep() { + when(mDisplayPolicy.isAwake()).thenReturn(true); + initPhoneWindowManager(); + + // Set power button behavior. + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.POWER_BUTTON_SHORT_PRESS, + SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP); + mPhoneWindowManager.updateSettings(null); + + // Can not dream for other reasons. + when(mDreamManagerInternal.canStartDreaming(any(Boolean.class))).thenReturn(false); + // Device is not dreaming. + when(mDreamManagerInternal.isDreaming()).thenReturn(false); + + // Power button pressed. + int eventTime = 0; + mPhoneWindowManager.powerPress(eventTime, 1, 0); + + // Device goes to sleep. + verify(mPowerManager).goToSleep(eventTime, PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, 0); + } + + @Test + public void powerPress_dreamOrAwakeOrSleep_dreamFromActive() { + when(mDisplayPolicy.isAwake()).thenReturn(true); + initPhoneWindowManager(); + + // Set power button behavior. + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.POWER_BUTTON_SHORT_PRESS, + SHORT_PRESS_POWER_DREAM_OR_AWAKE_OR_SLEEP); + mPhoneWindowManager.updateSettings(null); + + // Can dream when active. + when(mDreamManagerInternal.canStartDreaming(any(Boolean.class))).thenReturn(true); + + // Power button pressed. + int eventTime = 0; + mPhoneWindowManager.powerPress(eventTime, 1, 0); + + // Dream is requested. + verify(mDreamManagerInternal).requestDream(); + } + private void initPhoneWindowManager() { mPhoneWindowManager.mDefaultDisplayPolicy = mDisplayPolicy; mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class); @@ -345,6 +424,11 @@ public class PhoneWindowManagerTests { return mKeyguardServiceDelegate; } + @Override + LockPatternUtils getLockPatternUtils() { + return mLockPatternUtils; + } + /** * {@code WindowWakeUpPolicy} registers a local service in its constructor, easier to just * mock it out so we don't have to unregister it after every test. diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java index c57adfd69b06..f89c6f638384 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java +++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java @@ -238,33 +238,33 @@ class ShortcutKeyTestBase { sendKeyCombination(new int[]{keyCode}, durationMillis, false, DEFAULT_DISPLAY); } - boolean sendKeyGestureEventStart(int gestureType) { - return mPhoneWindowManager.sendKeyGestureEvent( + void sendKeyGestureEventStart(int gestureType) { + mPhoneWindowManager.sendKeyGestureEvent( new KeyGestureEvent.Builder().setKeyGestureType(gestureType).setAction( KeyGestureEvent.ACTION_GESTURE_START).build()); } - boolean sendKeyGestureEventComplete(int gestureType) { - return mPhoneWindowManager.sendKeyGestureEvent( + void sendKeyGestureEventComplete(int gestureType) { + mPhoneWindowManager.sendKeyGestureEvent( new KeyGestureEvent.Builder().setKeyGestureType(gestureType).setAction( KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); } - boolean sendKeyGestureEventCancel(int gestureType) { - return mPhoneWindowManager.sendKeyGestureEvent( + void sendKeyGestureEventCancel(int gestureType) { + mPhoneWindowManager.sendKeyGestureEvent( new KeyGestureEvent.Builder().setKeyGestureType(gestureType).setAction( KeyGestureEvent.ACTION_GESTURE_COMPLETE).setFlags( KeyGestureEvent.FLAG_CANCELLED).build()); } - boolean sendKeyGestureEventComplete(int gestureType, int modifierState) { - return mPhoneWindowManager.sendKeyGestureEvent( + void sendKeyGestureEventComplete(int gestureType, int modifierState) { + mPhoneWindowManager.sendKeyGestureEvent( new KeyGestureEvent.Builder().setModifierState(modifierState).setKeyGestureType( gestureType).setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); } - boolean sendKeyGestureEventComplete(int keycode, int modifierState, int gestureType) { - return mPhoneWindowManager.sendKeyGestureEvent( + void sendKeyGestureEventComplete(int keycode, int modifierState, int gestureType) { + mPhoneWindowManager.sendKeyGestureEvent( new KeyGestureEvent.Builder().setKeycodes(new int[]{keycode}).setModifierState( modifierState).setKeyGestureType(gestureType).setAction( KeyGestureEvent.ACTION_GESTURE_COMPLETE).build()); diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index e56fd3c6272d..7b6d361c55d4 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -49,6 +49,7 @@ import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAV import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.after; @@ -353,7 +354,7 @@ class TestPhoneWindowManager { doReturn(mAppOpsManager).when(mContext).getSystemService(eq(AppOpsManager.class)); doReturn(mDisplayManager).when(mContext).getSystemService(eq(DisplayManager.class)); doReturn(mInputManager).when(mContext).getSystemService(eq(InputManager.class)); - doNothing().when(mInputManager).registerKeyGestureEventHandler(any()); + doNothing().when(mInputManager).registerKeyGestureEventHandler(anyList(), any()); doNothing().when(mInputManager).unregisterKeyGestureEventHandler(any()); doReturn(mPackageManager).when(mContext).getPackageManager(); doReturn(mSensorPrivacyManager).when(mContext).getSystemService( @@ -476,8 +477,8 @@ class TestPhoneWindowManager { mPhoneWindowManager.interceptUnhandledKey(event, mInputToken); } - boolean sendKeyGestureEvent(KeyGestureEvent event) { - return mPhoneWindowManager.handleKeyGestureEvent(event, mInputToken); + void sendKeyGestureEvent(KeyGestureEvent event) { + mPhoneWindowManager.handleKeyGestureEvent(event, mInputToken); } /** diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 7f242dea9f45..773a566f6315 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -1459,21 +1459,6 @@ public class ActivityRecordTests extends WindowTestsBase { } /** - * Verify that finish bottom activity from a task won't boost it to top. - */ - @Test - public void testFinishBottomActivityIfPossible_noZBoost() { - final ActivityRecord bottomActivity = createActivityWithTask(); - final ActivityRecord topActivity = new ActivityBuilder(mAtm) - .setTask(bottomActivity.getTask()).build(); - topActivity.setVisibleRequested(true); - // simulating bottomActivity as a trampoline activity. - bottomActivity.setState(RESUMED, "test"); - bottomActivity.finishIfPossible("test", false); - assertFalse(bottomActivity.mNeedsZBoost); - } - - /** * Verify that complete finish request for visible activity must be delayed before the next one * becomes visible. */ diff --git a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java deleted file mode 100644 index e4628c45a15b..000000000000 --- a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.intThat; - -import android.platform.test.annotations.Presubmit; -import android.view.SurfaceControl; - -import androidx.test.filters.SmallTest; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - - -/** - * Animation related tests for the {@link ActivityRecord} class. - * - * Build/Install/Run: - * atest AppWindowTokenAnimationTests - */ -@SmallTest -@Presubmit -@RunWith(WindowTestRunner.class) -public class AppWindowTokenAnimationTests extends WindowTestsBase { - - private ActivityRecord mActivity; - - @Mock - private AnimationAdapter mSpec; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - - mActivity = createActivityRecord(mDisplayContent); - } - - @Test - public void clipAfterAnim_boundsLayerIsCreated() { - mActivity.mNeedsAnimationBoundsLayer = true; - - mActivity.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */, - ANIMATION_TYPE_APP_TRANSITION); - verify(mTransaction).reparent(eq(mActivity.getSurfaceControl()), - eq(mActivity.mSurfaceAnimator.mLeash)); - verify(mTransaction).reparent(eq(mActivity.mSurfaceAnimator.mLeash), - eq(mActivity.mAnimationBoundsLayer)); - } - - @Test - public void clipAfterAnim_boundsLayerZBoosted() { - final Task task = mActivity.getTask(); - final ActivityRecord topActivity = createActivityRecord(task); - task.assignChildLayers(mTransaction); - - assertThat(topActivity.getLastLayer()).isGreaterThan(mActivity.getLastLayer()); - - mActivity.mNeedsAnimationBoundsLayer = true; - mActivity.mNeedsZBoost = true; - mActivity.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */, - ANIMATION_TYPE_APP_TRANSITION); - - verify(mTransaction).setLayer(eq(mActivity.mAnimationBoundsLayer), - intThat(layer -> layer > topActivity.getLastLayer())); - - // The layer should be restored after the animation leash is removed. - mActivity.onAnimationLeashLost(mTransaction); - assertThat(mActivity.mNeedsZBoost).isFalse(); - assertThat(topActivity.getLastLayer()).isGreaterThan(mActivity.getLastLayer()); - } - - @Test - public void clipAfterAnim_boundsLayerIsDestroyed() { - mActivity.mNeedsAnimationBoundsLayer = true; - mActivity.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */, - ANIMATION_TYPE_APP_TRANSITION); - final SurfaceControl leash = mActivity.mSurfaceAnimator.mLeash; - final SurfaceControl animationBoundsLayer = mActivity.mAnimationBoundsLayer; - final ArgumentCaptor<SurfaceAnimator.OnAnimationFinishedCallback> callbackCaptor = - ArgumentCaptor.forClass( - SurfaceAnimator.OnAnimationFinishedCallback.class); - verify(mSpec).startAnimation(any(), any(), eq(ANIMATION_TYPE_APP_TRANSITION), - callbackCaptor.capture()); - - callbackCaptor.getValue().onAnimationFinished( - ANIMATION_TYPE_APP_TRANSITION, mSpec); - verify(mTransaction).remove(eq(leash)); - verify(mTransaction).remove(eq(animationBoundsLayer)); - assertThat(mActivity.mNeedsAnimationBoundsLayer).isFalse(); - } - - @Test - public void clipAfterAnimCancelled_boundsLayerIsDestroyed() { - mActivity.mNeedsAnimationBoundsLayer = true; - mActivity.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */, - ANIMATION_TYPE_APP_TRANSITION); - final SurfaceControl leash = mActivity.mSurfaceAnimator.mLeash; - final SurfaceControl animationBoundsLayer = mActivity.mAnimationBoundsLayer; - - mActivity.mSurfaceAnimator.cancelAnimation(); - verify(mTransaction).remove(eq(leash)); - verify(mTransaction).remove(eq(animationBoundsLayer)); - assertThat(mActivity.mNeedsAnimationBoundsLayer).isFalse(); - } - - @Test - public void clipNoneAnim_boundsLayerIsNotCreated() { - mActivity.mNeedsAnimationBoundsLayer = false; - - mActivity.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */, - ANIMATION_TYPE_APP_TRANSITION); - verify(mTransaction).reparent(eq(mActivity.getSurfaceControl()), - eq(mActivity.mSurfaceAnimator.mLeash)); - assertThat(mActivity.mAnimationBoundsLayer).isNull(); - } -} diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java index 27e147d98b1f..747b09cb5660 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java @@ -74,14 +74,14 @@ public class BackgroundLaunchProcessControllerTests { BackgroundActivityStartCallback mCallback = new BackgroundActivityStartCallback() { @Override - public boolean isActivityStartAllowed(Collection<IBinder> tokens, int uid, - String packageName) { + public BackgroundActivityStartCallbackResult isActivityStartAllowed( + Collection<IBinder> tokens, int uid, String packageName) { for (IBinder token : tokens) { if (token == null || mActivityStartAllowed.contains(token)) { - return true; + return new BackgroundActivityStartCallbackResult(true, token); } } - return false; + return RESULT_FALSE; } @Override 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 ec83c50e95aa..1aa8681c9bfd 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java @@ -49,10 +49,8 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.app.ActivityOptions; @@ -192,24 +190,6 @@ public class TaskDisplayAreaTests extends WindowTestsBase { } @Test - public void testActivityWithZBoost_taskDisplayAreaDoesNotMoveUp() { - final Task rootTask = createTask(mDisplayContent); - final Task task = createTaskInRootTask(rootTask, 0 /* userId */); - final ActivityRecord activity = createNonAttachedActivityRecord(mDisplayContent); - task.addChild(activity, 0 /* addPos */); - final TaskDisplayArea taskDisplayArea = activity.getDisplayArea(); - activity.mNeedsAnimationBoundsLayer = true; - activity.mNeedsZBoost = true; - spyOn(taskDisplayArea.mSurfaceAnimator); - - mDisplayContent.assignChildLayers(mTransaction); - - assertThat(activity.needsZBoost()).isTrue(); - assertThat(taskDisplayArea.needsZBoost()).isFalse(); - verify(taskDisplayArea.mSurfaceAnimator, never()).setLayer(eq(mTransaction), anyInt()); - } - - @Test public void testRootTaskPositionChildAt() { Task pinnedTask = createTask( mDisplayContent, WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD); 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 044aacc4b988..b617f0285606 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -100,8 +100,6 @@ import androidx.test.filters.MediumTest; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; -import libcore.junit.util.compat.CoreCompatChangeRule; - import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -414,79 +412,96 @@ public class TaskTests extends WindowTestsBase { } @Test - @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_RESIZE_APP}) - public void testIsResizeable_nonResizeable_forceResize_overridesEnabled_Resizeable() { + public void testIsResizeable_nonResizeable_forceResize_overridesEnabled_resizeable() { final Task task = new TaskBuilder(mSupervisor) .setCreateActivity(true) - .setComponent( - ComponentName.createRelative(mContext, SizeCompatTests.class.getName())) .build(); task.setResizeMode(RESIZE_MODE_UNRESIZEABLE); + final ActivityRecord activity = task.getRootActivity(); + final AppCompatResizeOverrides resizeOverrides = + activity.mAppCompatController.getResizeOverrides(); + spyOn(activity); + spyOn(resizeOverrides); + doReturn(true).when(resizeOverrides).shouldOverrideForceResizeApp(); + task.intent = null; + task.setIntent(activity); // Override should take effect and task should be resizeable. assertTrue(task.getTaskInfo().isResizeable); } @Test - @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_RESIZE_APP}) - public void testIsResizeable_nonResizeable_forceResize_overridesDisabled_nonResizeable() { + public void testIsResizeable_resizeable_forceNonResize_overridesEnabled_nonResizeable() { final Task task = new TaskBuilder(mSupervisor) .setCreateActivity(true) - .setComponent( - ComponentName.createRelative(mContext, SizeCompatTests.class.getName())) .build(); - task.setResizeMode(RESIZE_MODE_UNRESIZEABLE); - - // Disallow resize overrides. - task.mAllowForceResizeOverride = false; + task.setResizeMode(RESIZE_MODE_RESIZEABLE); + final ActivityRecord activity = task.getRootActivity(); + final AppCompatResizeOverrides resizeOverrides = + activity.mAppCompatController.getResizeOverrides(); + spyOn(activity); + spyOn(resizeOverrides); + doReturn(true).when(resizeOverrides).shouldOverrideForceNonResizeApp(); + task.intent = null; + task.setIntent(activity); - // Override should not take effect and task should be un-resizeable. + // Override should take effect and task should be un-resizeable. assertFalse(task.getTaskInfo().isResizeable); } @Test - @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_NON_RESIZE_APP}) - public void testIsResizeable_resizeable_forceNonResize_overridesEnabled_nonResizeable() { + public void testIsResizeable_resizeableTask_fullscreenOverride_resizeable() { final Task task = new TaskBuilder(mSupervisor) .setCreateActivity(true) - .setComponent( - ComponentName.createRelative(mContext, SizeCompatTests.class.getName())) .build(); - task.setResizeMode(RESIZE_MODE_RESIZEABLE); + task.setResizeMode(RESIZE_MODE_UNRESIZEABLE); + final ActivityRecord activity = task.getRootActivity(); + final AppCompatAspectRatioOverrides aspectRatioOverrides = + activity.mAppCompatController.getAspectRatioOverrides(); + spyOn(aspectRatioOverrides); + doReturn(true).when(aspectRatioOverrides).hasFullscreenOverride(); + task.intent = null; + task.setIntent(activity); - // Override should take effect and task should be un-resizeable. - assertFalse(task.getTaskInfo().isResizeable); + // Override should take effect and task should be resizeable. + assertTrue(task.getTaskInfo().isResizeable); } @Test - @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_NON_RESIZE_APP}) - public void testIsResizeable_resizeable_forceNonResize_overridesDisabled_Resizeable() { + public void testIsResizeable_resizeableTask_universalResizeable_resizeable() { final Task task = new TaskBuilder(mSupervisor) .setCreateActivity(true) - .setComponent( - ComponentName.createRelative(mContext, SizeCompatTests.class.getName())) .build(); - task.setResizeMode(RESIZE_MODE_RESIZEABLE); - - // Disallow resize overrides. - task.mAllowForceResizeOverride = false; + task.setResizeMode(RESIZE_MODE_UNRESIZEABLE); + final ActivityRecord activity = task.getRootActivity(); + spyOn(activity); + doReturn(true).when(activity).isUniversalResizeable(); + task.intent = null; + task.setIntent(activity); - // Override should not take effect and task should be resizeable. + // Override should take effect and task should be resizeable. assertTrue(task.getTaskInfo().isResizeable); } @Test - @CoreCompatChangeRule.EnableCompatChanges({ActivityInfo.FORCE_NON_RESIZE_APP}) - public void testIsResizeable_systemWideForceResize_compatForceNonResize__Resizeable() { + public void testIsResizeable_systemWideForceResize_compatForceNonResize_resizeable() { final Task task = new TaskBuilder(mSupervisor) .setCreateActivity(true) - .setComponent( - ComponentName.createRelative(mContext, SizeCompatTests.class.getName())) + .setComponent(ComponentName.createRelative(mContext, TaskTests.class.getName())) .build(); task.setResizeMode(RESIZE_MODE_RESIZEABLE); // Set system-wide force resizeable override. task.mAtmService.mForceResizableActivities = true; + final ActivityRecord activity = task.getRootActivity(); + final AppCompatResizeOverrides resizeOverrides = + activity.mAppCompatController.getResizeOverrides(); + spyOn(activity); + spyOn(resizeOverrides); + doReturn(true).when(resizeOverrides).shouldOverrideForceNonResizeApp(); + task.intent = null; + task.setIntent(activity); + // System wide override should tak priority over app compat override so the task should // remain resizeable. assertTrue(task.getTaskInfo().isResizeable); diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 14915109999c..19574fd95ac7 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -11493,17 +11493,17 @@ public class CarrierConfigManager { + "target=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, type=allowed"}); PersistableBundle auto_data_switch_rat_signal_score_string_bundle = new PersistableBundle(); auto_data_switch_rat_signal_score_string_bundle.putIntArray( - "NR_SA_MMWAVE", new int[]{10000, 13227, 16000, 18488, 20017}); + "NR_SA_MMWAVE", new int[]{6300, 10227, 16000, 18488, 19017}); auto_data_switch_rat_signal_score_string_bundle.putIntArray( - "NR_NSA_MMWAVE", new int[]{8000, 10227, 12488, 15017, 15278}); + "NR_NSA_MMWAVE", new int[]{5700, 9227, 12488, 13517, 15978}); auto_data_switch_rat_signal_score_string_bundle.putIntArray( "LTE", new int[]{3731, 5965, 8618, 11179, 13384}); auto_data_switch_rat_signal_score_string_bundle.putIntArray( - "LTE_CA", new int[]{3831, 6065, 8718, 11379, 13484}); + "LTE_CA", new int[]{3831, 6065, 8718, 11379, 14484}); auto_data_switch_rat_signal_score_string_bundle.putIntArray( - "NR_SA", new int[]{5288, 6795, 6955, 7562, 9713}); + "NR_SA", new int[]{2288, 6795, 6955, 7562, 15484}); auto_data_switch_rat_signal_score_string_bundle.putIntArray( - "NR_NSA", new int[]{5463, 6827, 8029, 9007, 9428}); + "NR_NSA", new int[]{2463, 6827, 8029, 9007, 15884}); auto_data_switch_rat_signal_score_string_bundle.putIntArray( "UMTS", new int[]{100, 169, 183, 192, 300}); auto_data_switch_rat_signal_score_string_bundle.putIntArray( diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index 0dd0a42d44b4..b8aa9e8646bd 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -3238,6 +3238,15 @@ interface ITelephony { boolean setOemEnabledSatelliteProvisionStatus(in boolean reset, in boolean isProvisioned); /** + * This API is used by CTS to override the version of the config data + * + * @param reset Whether to restore the original version + * @param version The overriding version + * @return {@code true} if successful, {@code false} otherwise + */ + boolean overrideConfigDataVersion(in boolean reset, in int version); + + /** * Test method to confirm the file contents are not altered. */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" diff --git a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt index 794fd0255726..c62bd0b72584 100644 --- a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt +++ b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt @@ -18,12 +18,10 @@ package android.hardware.input import android.content.Context import android.content.ContextWrapper -import android.os.IBinder import android.platform.test.annotations.Presubmit import android.platform.test.flag.junit.SetFlagsRule import android.view.KeyEvent import androidx.test.core.app.ApplicationProvider -import com.android.server.testutils.any import com.android.test.input.MockInputManagerRule import org.junit.Before import org.junit.Rule @@ -37,6 +35,7 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.fail +import org.junit.Assert.assertThrows /** * Tests for [InputManager.KeyGestureEventHandler]. @@ -82,7 +81,7 @@ class KeyGestureEventHandlerTest { // Handle key gesture handler registration. doAnswer { - val listener = it.getArgument(0) as IKeyGestureHandler + val listener = it.getArgument(1) as IKeyGestureHandler if (registeredListener != null && registeredListener!!.asBinder() != listener.asBinder()) { // There can only be one registered key gesture handler per process. @@ -90,7 +89,7 @@ class KeyGestureEventHandlerTest { } registeredListener = listener null - }.`when`(inputManagerRule.mock).registerKeyGestureHandler(any()) + }.`when`(inputManagerRule.mock).registerKeyGestureHandler(Mockito.any(), Mockito.any()) // Handle key gesture handler being unregistered. doAnswer { @@ -101,7 +100,7 @@ class KeyGestureEventHandlerTest { } registeredListener = null null - }.`when`(inputManagerRule.mock).unregisterKeyGestureHandler(any()) + }.`when`(inputManagerRule.mock).unregisterKeyGestureHandler(Mockito.any()) } private fun handleKeyGestureEvent(event: KeyGestureEvent) { @@ -121,11 +120,12 @@ class KeyGestureEventHandlerTest { var callbackCount = 0 // Add a key gesture event listener - inputManager.registerKeyGestureEventHandler(KeyGestureHandler { event, _ -> + inputManager.registerKeyGestureEventHandler( + listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + ) { event, _ -> assertEquals(HOME_GESTURE_EVENT, event) callbackCount++ - true - }) + } // Request handling for key gesture event will notify the handler. handleKeyGestureEvent(HOME_GESTURE_EVENT) @@ -135,29 +135,41 @@ class KeyGestureEventHandlerTest { @Test fun testAddingHandlersRegistersInternalCallbackHandler() { // Set up two callbacks. - val callback1 = KeyGestureHandler { _, _ -> false } - val callback2 = KeyGestureHandler { _, _ -> false } + val callback1 = InputManager.KeyGestureEventHandler { _, _ -> } + val callback2 = InputManager.KeyGestureEventHandler { _, _ -> } assertNull(registeredListener) // Adding the handler should register the callback with InputManagerService. - inputManager.registerKeyGestureEventHandler(callback1) + inputManager.registerKeyGestureEventHandler( + listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), + callback1 + ) assertNotNull(registeredListener) // Adding another handler should not register new internal listener. val currListener = registeredListener - inputManager.registerKeyGestureEventHandler(callback2) + inputManager.registerKeyGestureEventHandler( + listOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK), + callback2 + ) assertEquals(currListener, registeredListener) } @Test fun testRemovingHandlersUnregistersInternalCallbackHandler() { // Set up two callbacks. - val callback1 = KeyGestureHandler { _, _ -> false } - val callback2 = KeyGestureHandler { _, _ -> false } + val callback1 = InputManager.KeyGestureEventHandler { _, _ -> } + val callback2 = InputManager.KeyGestureEventHandler { _, _ -> } - inputManager.registerKeyGestureEventHandler(callback1) - inputManager.registerKeyGestureEventHandler(callback2) + inputManager.registerKeyGestureEventHandler( + listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), + callback1 + ) + inputManager.registerKeyGestureEventHandler( + listOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK), + callback2 + ) // Only removing all handlers should remove the internal callback inputManager.unregisterKeyGestureEventHandler(callback1) @@ -172,47 +184,74 @@ class KeyGestureEventHandlerTest { var callbackCount1 = 0 var callbackCount2 = 0 // Handler 1 captures all home gestures - val callback1 = KeyGestureHandler { event, _ -> + val callback1 = InputManager.KeyGestureEventHandler { event, _ -> callbackCount1++ - event.keyGestureType == KeyGestureEvent.KEY_GESTURE_TYPE_HOME + assertEquals(KeyGestureEvent.KEY_GESTURE_TYPE_HOME, event.keyGestureType) } - // Handler 2 captures all gestures - val callback2 = KeyGestureHandler { _, _ -> + // Handler 2 captures all back gestures + val callback2 = InputManager.KeyGestureEventHandler { event, _ -> callbackCount2++ - true + assertEquals(KeyGestureEvent.KEY_GESTURE_TYPE_BACK, event.keyGestureType) } // Add both key gesture event handlers - inputManager.registerKeyGestureEventHandler(callback1) - inputManager.registerKeyGestureEventHandler(callback2) + inputManager.registerKeyGestureEventHandler( + listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), + callback1 + ) + inputManager.registerKeyGestureEventHandler( + listOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK), + callback2 + ) - // Request handling for key gesture event, should notify callbacks in order. So, only the - // first handler should receive a callback since it captures the event. + // Request handling for home key gesture event, should notify only callback1 handleKeyGestureEvent(HOME_GESTURE_EVENT) assertEquals(1, callbackCount1) assertEquals(0, callbackCount2) - // Second handler should receive the event since the first handler doesn't capture the event + // Request handling for back key gesture event, should notify only callback2 handleKeyGestureEvent(BACK_GESTURE_EVENT) - assertEquals(2, callbackCount1) + assertEquals(1, callbackCount1) assertEquals(1, callbackCount2) inputManager.unregisterKeyGestureEventHandler(callback1) - // Request handling for key gesture event, should still trigger callback2 but not callback1. + + // Request handling for home key gesture event, should not trigger callback2 handleKeyGestureEvent(HOME_GESTURE_EVENT) - assertEquals(2, callbackCount1) - assertEquals(2, callbackCount2) + assertEquals(1, callbackCount1) + assertEquals(1, callbackCount2) + } + + @Test + fun testUnableToRegisterSameHandlerTwice() { + val handler = InputManager.KeyGestureEventHandler { _, _ -> } + + inputManager.registerKeyGestureEventHandler( + listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), + handler + ) + + assertThrows(IllegalArgumentException::class.java) { + inputManager.registerKeyGestureEventHandler( + listOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK), handler + ) + } } - inner class KeyGestureHandler( - private var handler: (event: KeyGestureEvent, token: IBinder?) -> Boolean - ) : InputManager.KeyGestureEventHandler { + @Test + fun testUnableToRegisterSameGestureTwice() { + val handler1 = InputManager.KeyGestureEventHandler { _, _ -> } + val handler2 = InputManager.KeyGestureEventHandler { _, _ -> } + + inputManager.registerKeyGestureEventHandler( + listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), + handler1 + ) - override fun handleKeyGestureEvent( - event: KeyGestureEvent, - focusedToken: IBinder? - ): Boolean { - return handler(event, focusedToken) + assertThrows(IllegalArgumentException::class.java) { + inputManager.registerKeyGestureEventHandler( + listOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), handler2 + ) } } } diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index 4f1fb6487b19..163dda84a71c 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -63,6 +63,7 @@ import org.junit.After import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -107,7 +108,10 @@ class KeyGestureControllerTests { const val SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0 const val SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL = 1 const val SETTINGS_KEY_BEHAVIOR_NOTHING = 2 + const val SYSTEM_PID = 0 const val TEST_PID = 10 + const val RANDOM_PID1 = 11 + const val RANDOM_PID2 = 12 } @JvmField @@ -170,6 +174,7 @@ class KeyGestureControllerTests { return atomicFile } }) + startNewInputGlobalTestSession() } @After @@ -199,17 +204,22 @@ class KeyGestureControllerTests { val correctIm = context.getSystemService(InputManager::class.java)!! val virtualDevice = correctIm.getInputDevice(KeyCharacterMap.VIRTUAL_KEYBOARD)!! val kcm = virtualDevice.keyCharacterMap!! - inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager) - val inputManager = InputManager(context) - Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) - .thenReturn(inputManager) - val keyboardDevice = InputDevice.Builder().setId(DEVICE_ID).build() Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID)) Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice) ExtendedMockito.`when`(KeyCharacterMap.load(Mockito.anyInt())).thenReturn(kcm) } + private fun startNewInputGlobalTestSession() { + if (this::inputManagerGlobalSession.isInitialized) { + inputManagerGlobalSession.close() + } + inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager) + val inputManager = InputManager(context) + Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) + .thenReturn(inputManager) + } + private fun setupKeyGestureController() { keyGestureController = KeyGestureController( @@ -225,13 +235,14 @@ class KeyGestureControllerTests { return accessibilityShortcutController } }) - Mockito.`when`(iInputManager.registerKeyGestureHandler(Mockito.any())) + Mockito.`when`(iInputManager.registerKeyGestureHandler(Mockito.any(), Mockito.any())) .thenAnswer { val args = it.arguments if (args[0] != null) { keyGestureController.registerKeyGestureHandler( - args[0] as IKeyGestureHandler, - TEST_PID + args[0] as IntArray, + args[1] as IKeyGestureHandler, + SYSTEM_PID ) } } @@ -285,59 +296,6 @@ class KeyGestureControllerTests { ) } - @Test - fun testKeyGestureEvent_multipleGestureHandlers() { - setupKeyGestureController() - - // Set up two callbacks. - var callbackCount1 = 0 - var callbackCount2 = 0 - var selfCallback = 0 - val externalHandler1 = KeyGestureHandler { _, _ -> - callbackCount1++ - true - } - val externalHandler2 = KeyGestureHandler { _, _ -> - callbackCount2++ - true - } - val selfHandler = KeyGestureHandler { _, _ -> - selfCallback++ - false - } - - // Register key gesture handler: External process (last in priority) - keyGestureController.registerKeyGestureHandler(externalHandler1, currentPid + 1) - - // Register key gesture handler: External process (second in priority) - keyGestureController.registerKeyGestureHandler(externalHandler2, currentPid - 1) - - // Register key gesture handler: Self process (first in priority) - keyGestureController.registerKeyGestureHandler(selfHandler, currentPid) - - keyGestureController.handleKeyGesture(/* deviceId = */ 0, intArrayOf(KeyEvent.KEYCODE_HOME), - /* modifierState = */ 0, KeyGestureEvent.KEY_GESTURE_TYPE_HOME, - KeyGestureEvent.ACTION_GESTURE_COMPLETE, /* displayId */ 0, - /* focusedToken = */ null, /* flags = */ 0, /* appLaunchData = */null - ) - - assertEquals( - "Self handler should get callbacks first", - 1, - selfCallback - ) - assertEquals( - "Higher priority handler should get callbacks first", - 1, - callbackCount2 - ) - assertEquals( - "Lower priority handler should not get callbacks if already handled", - 0, - callbackCount1 - ) - } - class TestData( val name: String, val keys: IntArray, @@ -789,10 +747,6 @@ class KeyGestureControllerTests { ) fun testCustomKeyGesturesNotAllowedForSystemGestures(test: TestData) { setupKeyGestureController() - // Need to re-init so that bookmarks are correctly blocklisted - Mockito.`when`(iInputManager.getAppLaunchBookmarks()) - .thenReturn(keyGestureController.appLaunchBookmarks) - keyGestureController.systemRunning() val builder = InputGestureData.Builder() .setKeyGestureType(test.expectedKeyGestureType) @@ -1163,9 +1117,6 @@ class KeyGestureControllerTests { KeyEvent.KEYCODE_FULLSCREEN ) - val handler = KeyGestureHandler { _, _ -> false } - keyGestureController.registerKeyGestureHandler(handler, 0) - for (key in testKeys) { sendKeys(intArrayOf(key), assertNotSentToApps = true) } @@ -1179,6 +1130,7 @@ class KeyGestureControllerTests { testKeyGestureNotProduced( "SEARCH -> Default Search", intArrayOf(KeyEvent.KEYCODE_SEARCH), + intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH) ) } @@ -1207,6 +1159,10 @@ class KeyGestureControllerTests { testKeyGestureNotProduced( "SETTINGS -> Do Nothing", intArrayOf(KeyEvent.KEYCODE_SETTINGS), + intArrayOf( + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL + ) ) } @@ -1290,28 +1246,6 @@ class KeyGestureControllerTests { ) ), TestData( - "VOLUME_DOWN + VOLUME_UP -> Accessibility Chord", - intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP), - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD, - intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP), - 0, - intArrayOf( - KeyGestureEvent.ACTION_GESTURE_START, - KeyGestureEvent.ACTION_GESTURE_COMPLETE - ) - ), - TestData( - "BACK + DPAD_DOWN -> Accessibility Chord(for TV)", - intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN), - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD, - intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN), - 0, - intArrayOf( - KeyGestureEvent.ACTION_GESTURE_START, - KeyGestureEvent.ACTION_GESTURE_COMPLETE - ) - ), - TestData( "BACK + DPAD_CENTER -> TV Trigger Bug Report", intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER), KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT, @@ -1428,9 +1362,11 @@ class KeyGestureControllerTests { testLooper.dispatchAll() // Reinitialize the gesture controller simulating a login/logout for the user. + startNewInputGlobalTestSession() setupKeyGestureController() keyGestureController.setCurrentUserId(userId) testLooper.dispatchAll() + val savedInputGestures = keyGestureController.getCustomInputGestures(userId, null) assertEquals( "Test: $test doesn't produce correct number of saved input gestures", @@ -1469,6 +1405,7 @@ class KeyGestureControllerTests { // Delete the old data and reinitialize the controller simulating a "fresh" install. tempFile.delete() + startNewInputGlobalTestSession() setupKeyGestureController() keyGestureController.setCurrentUserId(userId) testLooper.dispatchAll() @@ -1541,9 +1478,12 @@ class KeyGestureControllerTests { val handledEvents = mutableListOf<KeyGestureEvent>() val handler = KeyGestureHandler { event, _ -> handledEvents.add(KeyGestureEvent(event)) - true } - keyGestureController.registerKeyGestureHandler(handler, 0) + keyGestureController.registerKeyGestureHandler( + intArrayOf(test.expectedKeyGestureType), + handler, + TEST_PID + ) handledEvents.clear() keyGestureController.handleTouchpadGesture(test.touchpadGestureType) @@ -1570,7 +1510,7 @@ class KeyGestureControllerTests { event.appLaunchData ) - keyGestureController.unregisterKeyGestureHandler(handler, 0) + keyGestureController.unregisterKeyGestureHandler(handler, TEST_PID) } @Test @@ -1591,9 +1531,11 @@ class KeyGestureControllerTests { testLooper.dispatchAll() // Reinitialize the gesture controller simulating a login/logout for the user. + startNewInputGlobalTestSession() setupKeyGestureController() keyGestureController.setCurrentUserId(userId) testLooper.dispatchAll() + val savedInputGestures = keyGestureController.getCustomInputGestures(userId, null) assertEquals( "Test: $test doesn't produce correct number of saved input gestures", @@ -1627,6 +1569,7 @@ class KeyGestureControllerTests { // Delete the old data and reinitialize the controller simulating a "fresh" install. tempFile.delete() + startNewInputGlobalTestSession() setupKeyGestureController() keyGestureController.setCurrentUserId(userId) testLooper.dispatchAll() @@ -1699,13 +1642,97 @@ class KeyGestureControllerTests { Mockito.verify(accessibilityShortcutController, never()).performAccessibilityShortcut() } + @Test + fun testUnableToRegisterFromSamePidTwice() { + setupKeyGestureController() + + val handler1 = KeyGestureHandler { _, _ -> } + val handler2 = KeyGestureHandler { _, _ -> } + keyGestureController.registerKeyGestureHandler( + intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), + handler1, + RANDOM_PID1 + ) + + assertThrows(IllegalStateException::class.java) { + keyGestureController.registerKeyGestureHandler( + intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_BACK), + handler2, + RANDOM_PID1 + ) + } + } + + @Test + fun testUnableToRegisterSameGestureTwice() { + setupKeyGestureController() + + val handler1 = KeyGestureHandler { _, _ -> } + val handler2 = KeyGestureHandler { _, _ -> } + keyGestureController.registerKeyGestureHandler( + intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), + handler1, + RANDOM_PID1 + ) + + assertThrows(IllegalArgumentException::class.java) { + keyGestureController.registerKeyGestureHandler( + intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_HOME), + handler2, + RANDOM_PID2 + ) + } + } + + @Test + fun testUnableToRegisterEmptyListOfGestures() { + setupKeyGestureController() + + val handler = KeyGestureHandler { _, _ -> } + + assertThrows(IllegalArgumentException::class.java) { + keyGestureController.registerKeyGestureHandler( + intArrayOf(), + handler, + RANDOM_PID1 + ) + } + } + + @Test + fun testGestureHandlerNotCalledOnceUnregistered() { + setupKeyGestureController() + + var callbackCount = 0 + val handler1 = KeyGestureHandler { _, _ -> callbackCount++ } + keyGestureController.registerKeyGestureHandler( + intArrayOf(KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS), + handler1, + TEST_PID + ) + sendKeys(intArrayOf(KeyEvent.KEYCODE_RECENT_APPS)) + assertEquals(1, callbackCount) + + keyGestureController.unregisterKeyGestureHandler( + handler1, + TEST_PID + ) + + // Callback should not be sent after unregister + sendKeys(intArrayOf(KeyEvent.KEYCODE_RECENT_APPS)) + assertEquals(1, callbackCount) + } + private fun testKeyGestureInternal(test: TestData) { val handledEvents = mutableListOf<KeyGestureEvent>() val handler = KeyGestureHandler { event, _ -> handledEvents.add(KeyGestureEvent(event)) - true } - keyGestureController.registerKeyGestureHandler(handler, 0) + keyGestureController.registerKeyGestureHandler( + intArrayOf(test.expectedKeyGestureType), + handler, + TEST_PID + ) handledEvents.clear() sendKeys(test.keys) @@ -1744,16 +1771,19 @@ class KeyGestureControllerTests { ) } - keyGestureController.unregisterKeyGestureHandler(handler, 0) + keyGestureController.unregisterKeyGestureHandler(handler, TEST_PID) } - private fun testKeyGestureNotProduced(testName: String, testKeys: IntArray) { + private fun testKeyGestureNotProduced( + testName: String, + testKeys: IntArray, + possibleGestures: IntArray + ) { var handledEvents = mutableListOf<KeyGestureEvent>() val handler = KeyGestureHandler { event, _ -> handledEvents.add(KeyGestureEvent(event)) - true } - keyGestureController.registerKeyGestureHandler(handler, 0) + keyGestureController.registerKeyGestureHandler(possibleGestures, handler, TEST_PID) handledEvents.clear() sendKeys(testKeys) @@ -1823,10 +1853,10 @@ class KeyGestureControllerTests { } inner class KeyGestureHandler( - private var handler: (event: AidlKeyGestureEvent, token: IBinder?) -> Boolean + private var handler: (event: AidlKeyGestureEvent, token: IBinder?) -> Unit ) : IKeyGestureHandler.Stub() { - override fun handleKeyGesture(event: AidlKeyGestureEvent, token: IBinder?): Boolean { - return handler(event, token) + override fun handleKeyGesture(event: AidlKeyGestureEvent, token: IBinder?) { + handler(event, token) } } } diff --git a/tests/Input/src/com/android/test/input/AnrTest.kt b/tests/Input/src/com/android/test/input/AnrTest.kt index f8cb86b7b1fe..3ad3763a5d20 100644 --- a/tests/Input/src/com/android/test/input/AnrTest.kt +++ b/tests/Input/src/com/android/test/input/AnrTest.kt @@ -16,6 +16,7 @@ package com.android.test.input import android.app.ActivityManager +import android.app.ActivityTaskManager import android.app.ApplicationExitInfo import android.app.Instrumentation import android.content.Intent @@ -28,6 +29,7 @@ import android.os.SystemClock import android.server.wm.CtsWindowInfoUtils.getWindowCenter import android.server.wm.CtsWindowInfoUtils.waitForWindowOnTop import android.testing.PollingCheck +import android.util.Log import android.view.InputEvent import android.view.MotionEvent import android.view.MotionEvent.ACTION_DOWN @@ -46,21 +48,19 @@ import com.android.cts.input.inputeventmatchers.withMotionAction import java.time.Duration import java.util.concurrent.LinkedBlockingQueue import java.util.function.Supplier -import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Assert.fail -import org.junit.Before +import org.junit.Assume.assumeTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith /** - * Click on the center of the window identified by the provided window token. - * The click is performed using "UinputTouchScreen" device. - * If the touchscreen device is closed too soon, it may cause the click to be dropped. Therefore, - * the provided runnable can ensure that the click is delivered before the device is closed, thus - * avoiding this race. + * Click on the center of the window identified by the provided window token. The click is performed + * using "UinputTouchScreen" device. If the touchscreen device is closed too soon, it may cause the + * click to be dropped. Therefore, the provided runnable can ensure that the click is delivered + * before the device is closed, thus avoiding this race. */ private fun clickOnWindow( token: IBinder, @@ -104,6 +104,10 @@ class AnrTest { private val remoteInputEvents = LinkedBlockingQueue<InputEvent>() private val verifier = BlockingQueueEventVerifier(remoteInputEvents) + // Some devices don't support ANR error dialogs, such as cars, TVs, etc. + private val anrDialogsAreSupported = + ActivityTaskManager.currentUiModeSupportsErrorDialogs(instrumentation.targetContext) + val binder = object : IAnrTestService.Stub() { override fun provideActivityInfo(token: IBinder, displayId: Int, pid: Int) { @@ -121,34 +125,37 @@ class AnrTest { @get:Rule val debugInputRule = DebugInputRule() - @Before - fun setUp() { - startUnresponsiveActivity() - PACKAGE_NAME = UnresponsiveGestureMonitorActivity::class.java.getPackage()!!.getName() - } - - @After fun tearDown() {} - @Test @DebugInputRule.DebugInput(bug = 339924248) fun testGestureMonitorAnr_Close() { + startUnresponsiveActivity() + val timestamp = System.currentTimeMillis() triggerAnr() - clickCloseAppOnAnrDialog() + if (anrDialogsAreSupported) { + clickCloseAppOnAnrDialog() + } else { + Log.i(TAG, "The device does not support ANR dialogs, skipping check for ANR window") + // We still want to wait for the app to get killed by the ActivityManager + } + waitForNewExitReasonAfter(timestamp) } @Test @DebugInputRule.DebugInput(bug = 339924248) fun testGestureMonitorAnr_Wait() { + assumeTrue(anrDialogsAreSupported) + startUnresponsiveActivity() triggerAnr() clickWaitOnAnrDialog() SystemClock.sleep(500) // Wait at least 500ms after tapping on wait // ANR dialog should reappear after a delay - find the close button on it to verify + val timestamp = System.currentTimeMillis() clickCloseAppOnAnrDialog() + waitForNewExitReasonAfter(timestamp) } private fun clickCloseAppOnAnrDialog() { // Find anr dialog and kill app - val timestamp = System.currentTimeMillis() val uiDevice: UiDevice = UiDevice.getInstance(instrumentation) val closeAppButton: UiObject2? = uiDevice.wait(Until.findObject(By.res("android:id/aerr_close")), 20000) @@ -157,14 +164,6 @@ class AnrTest { return } closeAppButton.click() - /** - * We must wait for the app to be fully closed before exiting this test. This is because - * another test may again invoke 'am start' for the same activity. If the 1st process that - * got ANRd isn't killed by the time second 'am start' runs, the killing logic will apply to - * the newly launched 'am start' instance, and the second test will fail because the - * unresponsive activity will never be launched. - */ - waitForNewExitReasonAfter(timestamp) } private fun clickWaitOnAnrDialog() { @@ -180,16 +179,27 @@ class AnrTest { } private fun getExitReasons(): List<ApplicationExitInfo> { + val packageName = UnresponsiveGestureMonitorActivity::class.java.getPackage()!!.name lateinit var infos: List<ApplicationExitInfo> instrumentation.runOnMainSync { val am = instrumentation.getContext().getSystemService(ActivityManager::class.java)!! - infos = am.getHistoricalProcessExitReasons(PACKAGE_NAME, remotePid!!, NO_MAX) + infos = am.getHistoricalProcessExitReasons(packageName, remotePid!!, NO_MAX) } return infos } + /** + * We must wait for the app to be fully closed before exiting this test. This is because another + * test may again invoke 'am start' for the same activity. If the 1st process that got ANRd + * isn't killed by the time second 'am start' runs, the killing logic will apply to the newly + * launched 'am start' instance, and the second test will fail because the unresponsive activity + * will never be launched. + * + * Also, we must ensure that we wait until it's killed, so that the next test can launch this + * activity again. + */ private fun waitForNewExitReasonAfter(timestamp: Long) { - PollingCheck.waitFor { + PollingCheck.waitFor(Duration.ofSeconds(20).toMillis() * Build.HW_TIMEOUT_MULTIPLIER) { val reasons = getExitReasons() !reasons.isEmpty() && reasons[0].timestamp >= timestamp } @@ -199,16 +209,15 @@ class AnrTest { } private fun triggerAnr() { - clickOnWindow( - remoteWindowToken!!, - remoteDisplayId!!, - instrumentation, - ) { verifier.assertReceivedMotion(withMotionAction(ACTION_DOWN)) } + clickOnWindow(remoteWindowToken!!, remoteDisplayId!!, instrumentation) { + verifier.assertReceivedMotion(withMotionAction(ACTION_DOWN)) + } SystemClock.sleep(DISPATCHING_TIMEOUT.toLong()) // default ANR timeout for gesture monitors } private fun startUnresponsiveActivity() { + remoteWindowToken = null val intent = Intent(instrumentation.targetContext, UnresponsiveGestureMonitorActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NEW_DOCUMENT @@ -218,12 +227,17 @@ class AnrTest { instrumentation.targetContext.startActivity(intent) // first, wait for the token to become valid PollingCheck.check( - "UnresponsiveGestureMonitorActivity failed to call 'provideActivityInfo'", - Duration.ofSeconds(5).toMillis()) { remoteWindowToken != null } + "UnresponsiveGestureMonitorActivity failed to call 'provideActivityInfo'", + Duration.ofSeconds(10).toMillis() * Build.HW_TIMEOUT_MULTIPLIER, + ) { + remoteWindowToken != null + } // next, wait for the window of the activity to get on top // we could combine the two checks above, but the current setup makes it easier to detect // errors - assertTrue("Remote activity window did not become visible", - waitForWindowOnTop(Duration.ofSeconds(5), Supplier { remoteWindowToken })) + assertTrue( + "Remote activity window did not become visible", + waitForWindowOnTop(Duration.ofSeconds(5), Supplier { remoteWindowToken }), + ) } } diff --git a/tools/aapt2/ResourcesInternal.proto b/tools/aapt2/ResourcesInternal.proto index f4735a2f6ce7..380c5f21103c 100644 --- a/tools/aapt2/ResourcesInternal.proto +++ b/tools/aapt2/ResourcesInternal.proto @@ -50,8 +50,11 @@ message CompiledFile { // Any symbols this file auto-generates/exports (eg. @+id/foo in an XML file). repeated Symbol exported_symbol = 5; - // The status of the flag the file is behind if any + // The status of the read only flag the file is behind if any uint32 flag_status = 6; bool flag_negated = 7; string flag_name = 8; + + // Whether the file uses read/write feature flags + bool uses_readwrite_feature_flags = 9; } diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp index a5e18d35a256..3b4f5429f254 100644 --- a/tools/aapt2/cmd/Compile.cpp +++ b/tools/aapt2/cmd/Compile.cpp @@ -407,6 +407,45 @@ static bool IsValidFile(IAaptContext* context, const std::string& input_path) { return true; } +class FindReadWriteFlagsVisitor : public xml::Visitor { + public: + FindReadWriteFlagsVisitor(const FeatureFlagValues& feature_flag_values) + : feature_flag_values_(feature_flag_values) { + } + + void Visit(xml::Element* node) override { + if (had_flags_) { + return; + } + auto* attr = node->FindAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag); + if (attr != nullptr) { + std::string_view flag_name = util::TrimWhitespace(attr->value); + if (flag_name.starts_with('!')) { + flag_name = flag_name.substr(1); + } + if (auto it = feature_flag_values_.find(flag_name); it != feature_flag_values_.end()) { + if (!it->second.read_only) { + had_flags_ = true; + return; + } + } else { + // Flag not passed to aapt2, must evaluate at runtime + had_flags_ = true; + return; + } + } + VisitChildren(node); + } + + bool HadFlags() const { + return had_flags_; + } + + private: + bool had_flags_ = false; + const FeatureFlagValues& feature_flag_values_; +}; + static bool CompileXml(IAaptContext* context, const CompileOptions& options, const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer, const std::string& output_path) { @@ -436,6 +475,10 @@ static bool CompileXml(IAaptContext* context, const CompileOptions& options, xmlres->file.type = ResourceFile::Type::kProtoXml; xmlres->file.flag = ParseFlag(path_data.flag_name); + FindReadWriteFlagsVisitor visitor(options.feature_flag_values); + xmlres->root->Accept(&visitor); + xmlres->file.uses_readwrite_feature_flags = visitor.HadFlags(); + if (xmlres->file.flag) { std::string error; auto flag_status = GetFlagStatus(xmlres->file.flag, options.feature_flag_values, &error); diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h index 9452e588953e..5576ec0b882f 100644 --- a/tools/aapt2/cmd/Convert.h +++ b/tools/aapt2/cmd/Convert.h @@ -38,14 +38,14 @@ class ConvertCommand : public Command { "--enable-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" - "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " - "the APK is O+", + "Only applies sparse encoding if minSdk of the APK is >= 32", &enable_sparse_encoding_); - AddOptionalSwitch("--force-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.\n" - "Applies sparse encoding to all resources regardless of minSdk.", - &force_sparse_encoding_); + AddOptionalSwitch( + "--force-sparse-encoding", + "Enables encoding sparse entries using a binary search tree.\n" + "This decreases APK size at the cost of resource retrieval performance.\n" + "Only applies sparse encoding if minSdk of the APK is >= 32 or is not set", + &force_sparse_encoding_); AddOptionalSwitch( "--enable-compact-entries", "This decreases APK size by using compact resource entries for simple data types.", diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 755dbb6f8e42..0e18ee250993 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -615,6 +615,8 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv file_op.xml_to_flatten->file.source = file_ref->GetSource(); file_op.xml_to_flatten->file.name = ResourceName(pkg->name, type->named_type, entry->name); + file_op.xml_to_flatten->file.uses_readwrite_feature_flags = + config_value->uses_readwrite_feature_flags; } // NOTE(adamlesinski): Explicitly construct a StringPiece here, or @@ -647,6 +649,17 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv } } + FeatureFlagsFilterOptions flags_filter_options; + // Don't fail on unrecognized flags or flags without values as these flags might be + // defined and have a value by the time they are evaluated at runtime. + flags_filter_options.fail_on_unrecognized_flags = false; + flags_filter_options.flags_must_have_value = false; + flags_filter_options.remove_disabled_elements = true; + FeatureFlagsFilter flags_filter(options_.feature_flag_values, flags_filter_options); + if (!flags_filter.Consume(context_, file_op.xml_to_flatten.get())) { + return 1; + } + std::vector<std::unique_ptr<xml::XmlResource>> versioned_docs = LinkAndVersionXmlFile(table, &file_op); if (versioned_docs.empty()) { @@ -673,6 +686,7 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv // Update the output format of this XML file. file_ref->type = XmlFileTypeForOutputFormat(options_.output_format); + bool result = table->AddResource( NewResourceBuilder(file.name) .SetValue(std::move(file_ref), file.config) @@ -685,14 +699,6 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv } } - FeatureFlagsFilterOptions flags_filter_options; - flags_filter_options.fail_on_unrecognized_flags = false; - flags_filter_options.flags_must_have_value = false; - FeatureFlagsFilter flags_filter(options_.feature_flag_values, flags_filter_options); - if (!flags_filter.Consume(context_, doc.get())) { - return 1; - } - error |= !FlattenXml(context_, *doc, dst_path, options_.keep_raw_values, false /*utf16*/, options_.output_format, archive_writer); } diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h index 977978834fcd..54a8c8625eb5 100644 --- a/tools/aapt2/cmd/Link.h +++ b/tools/aapt2/cmd/Link.h @@ -164,9 +164,12 @@ class LinkCommand : public Command { AddOptionalSwitch("--no-resource-removal", "Disables automatic removal of resources without\n" "defaults. Use this only when building runtime resource overlay packages.", &options_.no_resource_removal); - AddOptionalSwitch("--enable-sparse-encoding", - "This decreases APK size at the cost of resource retrieval performance.", - &options_.use_sparse_encoding); + AddOptionalSwitch( + "--enable-sparse-encoding", + "Enables encoding sparse entries using a binary search tree.\n" + "This decreases APK size at the cost of resource retrieval performance.\n" + "Only applies sparse encoding if minSdk of the APK is >= 32", + &options_.use_sparse_encoding); AddOptionalSwitch("--enable-compact-entries", "This decreases APK size by using compact resource entries for simple data types.", &options_.table_flattener_options.use_compact_entries); diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h index 012b0f230ca2..a8f547e3d96c 100644 --- a/tools/aapt2/cmd/Optimize.h +++ b/tools/aapt2/cmd/Optimize.h @@ -108,14 +108,14 @@ class OptimizeCommand : public Command { "--enable-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" - "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " - "the APK is O+", + "Only applies sparse encoding if minSdk of the APK is >= 32", &options_.enable_sparse_encoding); - AddOptionalSwitch("--force-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.\n" - "Applies sparse encoding to all resources regardless of minSdk.", - &options_.force_sparse_encoding); + AddOptionalSwitch( + "--force-sparse-encoding", + "Enables encoding sparse entries using a binary search tree.\n" + "This decreases APK size at the cost of resource retrieval performance.\n" + "Only applies sparse encoding if minSdk of the APK is >= 32 or is not set", + &options_.force_sparse_encoding); AddOptionalSwitch( "--enable-compact-entries", "This decreases APK size by using compact resource entries for simple data types.", diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index 50144ae816b6..d19c9f2d5d75 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -197,13 +197,16 @@ class PackageFlattener { bool sparse_encode = sparse_entries_ == SparseEntriesMode::Enabled || sparse_entries_ == SparseEntriesMode::Forced; - if (sparse_entries_ == SparseEntriesMode::Forced || - (context_->GetMinSdkVersion() == 0 && config.sdkVersion == 0)) { - // Sparse encode if forced or sdk version is not set in context and config. - } else { - // Otherwise, only sparse encode if the entries will be read on platforms S_V2+. - sparse_encode = sparse_encode && (context_->GetMinSdkVersion() >= SDK_S_V2); - } + // Only sparse encode if the entries will be read on platforms S_V2+. Sparse encoding + // is not supported on older platforms (b/197642721, b/197976367). + // + // We also allow sparse encoding for minSdk is 0 (not set) if sparse encoding is forced, + // in order to support Bundletool's usage of aapt2 where minSdk is not set in splits. + bool meets_min_sdk_requirement_for_sparse_encoding = + (context_->GetMinSdkVersion() >= SDK_S_V2) || + (context_->GetMinSdkVersion() == 0 && sparse_entries_ == SparseEntriesMode::Forced); + + sparse_encode = sparse_encode && meets_min_sdk_requirement_for_sparse_encoding; // Only sparse encode if the offsets are representable in 2 bytes. sparse_encode = sparse_encode && short_offsets; diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp index 9156b96b67ec..0e8aae14a350 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -15,6 +15,7 @@ */ #include "format/binary/TableFlattener.h" +#include <string> #include "android-base/stringprintf.h" #include "androidfw/TypeWrappers.h" @@ -326,6 +327,28 @@ static std::unique_ptr<ResourceTable> BuildTableWithSparseEntries( return table; } +static void CheckSparseEntries(IAaptContext* context, const ConfigDescription& sparse_config, + const std::string& sparse_contents) { + ResourceTable sparse_table; + BinaryResourceParser parser(context->GetDiagnostics(), &sparse_table, Source("test.arsc"), + sparse_contents.data(), sparse_contents.size()); + ASSERT_TRUE(parser.Parse()); + + auto value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_0", + sparse_config); + ASSERT_THAT(value, NotNull()); + EXPECT_EQ(0u, value->value.data); + + ASSERT_THAT(test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_1", + sparse_config), + IsNull()); + + value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_4", + sparse_config); + ASSERT_THAT(value, NotNull()); + EXPECT_EQ(4u, value->value.data); +} + TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) { std::unique_ptr<IAaptContext> context = test::ContextBuilder() .SetCompilationPackage("android") @@ -347,29 +370,56 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) { EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); - // Attempt to parse the sparse contents. + CheckSparseEntries(context.get(), sparse_config, sparse_contents); +} - ResourceTable sparse_table; - BinaryResourceParser parser(context->GetDiagnostics(), &sparse_table, Source("test.arsc"), - sparse_contents.data(), sparse_contents.size()); - ASSERT_TRUE(parser.Parse()); +TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2AndForced) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder() + .SetCompilationPackage("android") + .SetPackageId(0x01) + .SetMinSdkVersion(SDK_S_V2) + .Build(); - auto value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_0", - sparse_config); - ASSERT_THAT(value, NotNull()); - EXPECT_EQ(0u, value->value.data); + const ConfigDescription sparse_config = test::ParseConfigOrDie("en-rGB"); + auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); - ASSERT_THAT(test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_1", - sparse_config), - IsNull()); + TableFlattenerOptions options; + options.sparse_entries = SparseEntriesMode::Forced; - value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_4", - sparse_config); - ASSERT_THAT(value, NotNull()); - EXPECT_EQ(4u, value->value.data); + std::string no_sparse_contents; + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); + + std::string sparse_contents; + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); + + EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); + + CheckSparseEntries(context.get(), sparse_config, sparse_contents); } -TEST_F(TableFlattenerTest, FlattenSparseEntryWithConfigSdkVersionSV2) { +TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkBeforeSV2) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder() + .SetCompilationPackage("android") + .SetPackageId(0x01) + .SetMinSdkVersion(SDK_LOLLIPOP) + .Build(); + + const ConfigDescription sparse_config = test::ParseConfigOrDie("en-rGB"); + auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); + + TableFlattenerOptions options; + options.sparse_entries = SparseEntriesMode::Enabled; + + std::string no_sparse_contents; + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); + + std::string sparse_contents; + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); + + EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size()); +} + +TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkBeforeSV2AndConfigSdkVersionSV2) { std::unique_ptr<IAaptContext> context = test::ContextBuilder() .SetCompilationPackage("android") .SetPackageId(0x01) @@ -391,7 +441,7 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithConfigSdkVersionSV2) { EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size()); } -TEST_F(TableFlattenerTest, FlattenSparseEntryRegardlessOfMinSdkWhenForced) { +TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkBeforeSV2AndForced) { std::unique_ptr<IAaptContext> context = test::ContextBuilder() .SetCompilationPackage("android") .SetPackageId(0x01) @@ -410,7 +460,7 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryRegardlessOfMinSdkWhenForced) { std::string sparse_contents; ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); - EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); + EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size()); } TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSet) { @@ -429,28 +479,28 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSet) { std::string sparse_contents; ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); - EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); + EXPECT_EQ(no_sparse_contents.size(), sparse_contents.size()); +} - // Attempt to parse the sparse contents. +TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSetAndForced) { + std::unique_ptr<IAaptContext> context = + test::ContextBuilder().SetCompilationPackage("android").SetPackageId(0x01).Build(); - ResourceTable sparse_table; - BinaryResourceParser parser(context->GetDiagnostics(), &sparse_table, Source("test.arsc"), - sparse_contents.data(), sparse_contents.size()); - ASSERT_TRUE(parser.Parse()); + const ConfigDescription sparse_config = test::ParseConfigOrDie("en-rGB"); + auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); - auto value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_0", - sparse_config); - ASSERT_THAT(value, NotNull()); - EXPECT_EQ(0u, value->value.data); + TableFlattenerOptions options; + options.sparse_entries = SparseEntriesMode::Forced; - ASSERT_THAT(test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_1", - sparse_config), - IsNull()); + std::string no_sparse_contents; + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); - value = test::GetValueForConfig<BinaryPrimitive>(&sparse_table, "android:string/foo_4", - sparse_config); - ASSERT_THAT(value, NotNull()); - EXPECT_EQ(4u, value->value.data); + std::string sparse_contents; + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); + + EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); + + CheckSparseEntries(context.get(), sparse_config, sparse_contents); } TEST_F(TableFlattenerTest, DoNotUseSparseEntryForDenseConfig) { diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp index 91ec3485ac3b..b8936553a193 100644 --- a/tools/aapt2/format/proto/ProtoDeserialize.cpp +++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp @@ -640,6 +640,7 @@ bool DeserializeCompiledFileFromPb(const pb::internal::CompiledFile& pb_file, out_file->name = name_ref.ToResourceName(); out_file->source.path = pb_file.source_path(); out_file->type = DeserializeFileReferenceTypeFromPb(pb_file.type()); + out_file->uses_readwrite_feature_flags = pb_file.uses_readwrite_feature_flags(); out_file->flag_status = (FlagStatus)pb_file.flag_status(); if (!pb_file.flag_name().empty()) { diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp index fcc77d5a9d6d..da99c4f5917c 100644 --- a/tools/aapt2/format/proto/ProtoSerialize.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize.cpp @@ -767,6 +767,7 @@ void SerializeCompiledFileToPb(const ResourceFile& file, pb::internal::CompiledF out_file->set_flag_negated(file.flag->negated); out_file->set_flag_name(file.flag->name); } + out_file->set_uses_readwrite_feature_flags(file.uses_readwrite_feature_flags); for (const SourcedResourceName& exported : file.exported_symbols) { pb::internal::CompiledFile_Symbol* pb_symbol = out_file->add_exported_symbol(); diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp index 47a71fe36e9f..4dcb8507fa45 100644 --- a/tools/aapt2/link/FlaggedResources_test.cpp +++ b/tools/aapt2/link/FlaggedResources_test.cpp @@ -226,9 +226,11 @@ TEST_F(FlaggedResourcesTest, ReadWriteFlagInXmlGetsFlagged) { } } } + ASSERT_TRUE(found) << "No entry for layout1 at v36 with FLAG_USES_FEATURE_FLAGS bit set"; - // There should only be 1 entry that has the FLAG_USES_FEATURE_FLAGS bit of flags set to 1 - ASSERT_EQ(fields_flagged, 1); + // There should only be 2 entry that has the FLAG_USES_FEATURE_FLAGS bit of flags set to 1, the + // three versions of the layout file that has flags + ASSERT_EQ(fields_flagged, 3); } } // namespace aapt diff --git a/tools/aapt2/link/FlaggedXmlVersioner.cpp b/tools/aapt2/link/FlaggedXmlVersioner.cpp index 8a3337c446cb..626cae73bfa2 100644 --- a/tools/aapt2/link/FlaggedXmlVersioner.cpp +++ b/tools/aapt2/link/FlaggedXmlVersioner.cpp @@ -35,10 +35,6 @@ class AllDisabledFlagsVisitor : public xml::Visitor { VisitChildren(node); } - bool HadFlags() const { - return had_flags_; - } - private: bool FixupOrShouldRemove(const std::unique_ptr<xml::Node>& node) { if (auto* el = NodeCast<Element>(node.get())) { @@ -47,7 +43,6 @@ class AllDisabledFlagsVisitor : public xml::Visitor { return false; } - had_flags_ = true; // This class assumes all flags are disabled so we want to remove any elements behind flags // unless the flag specification is negated. In the negated case we remove the featureFlag // attribute because we have already determined whether we are keeping the element or not. @@ -62,56 +57,27 @@ class AllDisabledFlagsVisitor : public xml::Visitor { return false; } - - bool had_flags_ = false; -}; - -// An xml visitor that goes through the a doc and determines if any elements are behind a flag. -class FindFlagsVisitor : public xml::Visitor { - public: - void Visit(xml::Element* node) override { - if (had_flags_) { - return; - } - auto* attr = node->FindAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag); - if (attr != nullptr) { - had_flags_ = true; - return; - } - VisitChildren(node); - } - - bool HadFlags() const { - return had_flags_; - } - - bool had_flags_ = false; }; std::vector<std::unique_ptr<xml::XmlResource>> FlaggedXmlVersioner::Process(IAaptContext* context, xml::XmlResource* doc) { std::vector<std::unique_ptr<xml::XmlResource>> docs; - if ((static_cast<ApiVersion>(doc->file.config.sdkVersion) >= SDK_BAKLAVA) || - (static_cast<ApiVersion>(context->GetMinSdkVersion()) >= SDK_BAKLAVA)) { + if (!doc->file.uses_readwrite_feature_flags) { + docs.push_back(doc->Clone()); + } else if ((static_cast<ApiVersion>(doc->file.config.sdkVersion) >= SDK_BAKLAVA) || + (static_cast<ApiVersion>(context->GetMinSdkVersion()) >= SDK_BAKLAVA)) { // Support for read/write flags was added in baklava so if the doc will only get used on // baklava or later we can just return the original doc. docs.push_back(doc->Clone()); - FindFlagsVisitor visitor; - doc->root->Accept(&visitor); - docs.back()->file.uses_readwrite_feature_flags = visitor.HadFlags(); } else { auto preBaklavaVersion = doc->Clone(); AllDisabledFlagsVisitor visitor; preBaklavaVersion->root->Accept(&visitor); - preBaklavaVersion->file.uses_readwrite_feature_flags = false; docs.push_back(std::move(preBaklavaVersion)); - if (visitor.HadFlags()) { - auto baklavaVersion = doc->Clone(); - baklavaVersion->file.config.sdkVersion = SDK_BAKLAVA; - baklavaVersion->file.uses_readwrite_feature_flags = true; - docs.push_back(std::move(baklavaVersion)); - } + auto baklavaVersion = doc->Clone(); + baklavaVersion->file.config.sdkVersion = SDK_BAKLAVA; + docs.push_back(std::move(baklavaVersion)); } return docs; } diff --git a/tools/aapt2/link/FlaggedXmlVersioner_test.cpp b/tools/aapt2/link/FlaggedXmlVersioner_test.cpp index 0c1314f165cc..0dc464253385 100644 --- a/tools/aapt2/link/FlaggedXmlVersioner_test.cpp +++ b/tools/aapt2/link/FlaggedXmlVersioner_test.cpp @@ -101,6 +101,7 @@ TEST_F(FlaggedXmlVersionerTest, PreBaklavaGetsSplit) { <TextView android:featureFlag="package.flag" /><TextView /><TextView /> </LinearLayout>)"); doc->file.config.sdkVersion = SDK_GINGERBREAD; + doc->file.uses_readwrite_feature_flags = true; FlaggedXmlVersioner versioner; auto results = versioner.Process(context_.get(), doc.get()); @@ -131,6 +132,7 @@ TEST_F(FlaggedXmlVersionerTest, NoVersionGetsSplit) { <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"> <TextView android:featureFlag="package.flag" /><TextView /><TextView /> </LinearLayout>)"); + doc->file.uses_readwrite_feature_flags = true; FlaggedXmlVersioner versioner; auto results = versioner.Process(context_.get(), doc.get()); @@ -162,6 +164,7 @@ TEST_F(FlaggedXmlVersionerTest, NegatedFlagAttributeRemoved) { <TextView android:featureFlag="!package.flag" /><TextView /><TextView /> </LinearLayout>)"); doc->file.config.sdkVersion = SDK_GINGERBREAD; + doc->file.uses_readwrite_feature_flags = true; FlaggedXmlVersioner versioner; auto results = versioner.Process(context_.get(), doc.get()); @@ -192,6 +195,7 @@ TEST_F(FlaggedXmlVersionerTest, NegatedFlagAttributeRemovedNoSpecifiedVersion) { <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"> <TextView android:featureFlag="!package.flag" /><TextView /><TextView /> </LinearLayout>)"); + doc->file.uses_readwrite_feature_flags = true; FlaggedXmlVersioner versioner; auto results = versioner.Process(context_.get(), doc.get()); diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp index 1d4adc4a57d8..17f332397317 100644 --- a/tools/aapt2/link/TableMerger.cpp +++ b/tools/aapt2/link/TableMerger.cpp @@ -295,6 +295,8 @@ bool TableMerger::DoMerge(const android::Source& src, ResourceTablePackage* src_ dst_config_value = dst_entry->FindOrCreateValue(src_config_value->config, src_config_value->product); } + dst_config_value->uses_readwrite_feature_flags |= + src_config_value->uses_readwrite_feature_flags; // Continue if we're taking the new resource. CloningValueTransformer cloner(&main_table_->string_pool); @@ -378,12 +380,13 @@ bool TableMerger::MergeFile(const ResourceFile& file_desc, bool overlay, io::IFi file_ref->file = file; file_ref->SetFlagStatus(file_desc.flag_status); file_ref->SetFlag(file_desc.flag); - ResourceTablePackage* pkg = table.FindOrCreatePackage(file_desc.name.package); - pkg->FindOrCreateType(file_desc.name.type) - ->FindOrCreateEntry(file_desc.name.entry) - ->FindOrCreateValue(file_desc.config, {}) - ->value = std::move(file_ref); + ResourceConfigValue* config_value = pkg->FindOrCreateType(file_desc.name.type) + ->FindOrCreateEntry(file_desc.name.entry) + ->FindOrCreateValue(file_desc.config, {}); + + config_value->value = std::move(file_ref); + config_value->uses_readwrite_feature_flags = file_desc.uses_readwrite_feature_flags; return DoMerge(file->GetSource(), pkg, false /*mangle*/, overlay /*overlay*/, true /*allow_new*/); } diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md index 6bdbaaed9858..413f817ea8fd 100644 --- a/tools/aapt2/readme.md +++ b/tools/aapt2/readme.md @@ -5,6 +5,10 @@ 2017. This README will be updated more frequently in the future. - Added a new flag `--no-compress-fonts`. This can significantly speed up loading fonts from APK assets, at the cost of increasing the storage size of the APK. +- Changed the behavior of `--enable-sparse-encoding`. Sparse encoding is only applied if the + minSdkVersion is >= 32. +- Changed the behavior of `--force-sparse-encoding`. Sparse encoding is only applied if the + minSdkVersion is >= 32 or is not set. ## Version 2.19 - Added navigation resource type. |