diff options
671 files changed, 14104 insertions, 6289 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index f9cc1259a375..b9537b357517 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -50,6 +50,7 @@ aconfig_declarations_group { "android.hardware.devicestate.feature.flags-aconfig-java", "android.hardware.flags-aconfig-java", "android.hardware.radio.flags-aconfig-java", + "android.hardware.serial.flags-aconfig-java", "android.hardware.usb.flags-aconfig-java", "android.location.flags-aconfig-java", "android.media.codec-aconfig-java", @@ -1962,3 +1963,18 @@ java_aconfig_library { aconfig_declarations: "android.service.selinux.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], } + +// Serial +aconfig_declarations { + name: "android.hardware.serial.flags-aconfig", + exportable: true, + package: "android.hardware.serial.flags", + container: "system", + srcs: ["core/java/android/hardware/serial/flags/*.aconfig"], +} + +java_aconfig_library { + name: "android.hardware.serial.flags-aconfig-java", + aconfig_declarations: "android.hardware.serial.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} 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/apct-tests/perftests/core/src/android/os/TracePerfTest.java b/apct-tests/perftests/core/src/android/os/TracePerfTest.java index 00e1c1fdbf4b..3df708d1a5cd 100644 --- a/apct-tests/perftests/core/src/android/os/TracePerfTest.java +++ b/apct-tests/perftests/core/src/android/os/TracePerfTest.java @@ -46,6 +46,7 @@ public class TracePerfTest { private static final String FOO = "foo"; private static final Category FOO_CATEGORY = new Category(FOO); + private static final Category UNREGISTERED_CATEGORY = new Category("unregistered"); private static PerfettoTrace.Session sPerfettoSession; @BeforeClass @@ -163,6 +164,30 @@ public class TracePerfTest { } } + @Test + public void testInstantPerfettoWithProtoUnregistered() { + PerfettoTrace.begin(UNREGISTERED_CATEGORY, "message_queue_receive") + .beginProto() + .beginNested(2004 /* message_queue */) + .addField(1 /* sending_thread_name */, "foo") + .endNested() + .endProto() + .setTerminatingFlow(5) + .emit(); + + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + PerfettoTrace.begin(UNREGISTERED_CATEGORY, "message_queue_receive") + .beginProto() + .beginNested(2004 /* message_queue */) + .addField(1 /* sending_thread_name */, "foo") + .endNested() + .endProto() + .setTerminatingFlow(5) + .emit(); + } + } + private static TraceConfig getTraceConfig(String cat) { BufferConfig bufferConfig = BufferConfig.newBuilder().setSizeKb(1024).build(); TrackEventConfig trackEventConfig = TrackEventConfig 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/uinput/src/com/android/commands/uinput/EvemuParser.java b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java index da991624e685..d3e62d5351f0 100644 --- a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java +++ b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java @@ -48,12 +48,17 @@ public class EvemuParser implements EventParser { private static class CommentAwareReader { private final LineNumberReader mReader; - private String mPreviousLine; - private String mNextLine; + /** The previous line of the file, or {@code null} if we're at the start of the file. */ + private @Nullable String mPreviousLine; + /** + * The next line of the file to be returned from {@link #peekLine()}, or {@code null} if we + * haven't peeked since the last {@link #advance()} or are at the end of the file. + */ + private @Nullable String mNextLine; + private boolean mAtEndOfFile = false; - CommentAwareReader(LineNumberReader in) throws IOException { + CommentAwareReader(LineNumberReader in) { mReader = in; - mNextLine = findNextLine(); } private @Nullable String findNextLine() throws IOException { @@ -61,7 +66,7 @@ public class EvemuParser implements EventParser { while (line != null && line.length() == 0) { String unstrippedLine = mReader.readLine(); if (unstrippedLine == null) { - // End of file. + mAtEndOfFile = true; return null; } line = stripComments(unstrippedLine); @@ -85,22 +90,28 @@ public class EvemuParser implements EventParser { * {@code null} if the end of the file is reached. However, it does not advance to the * next line of the file. */ - public @Nullable String peekLine() { + public @Nullable String peekLine() throws IOException { + if (mNextLine == null && !mAtEndOfFile) { + mNextLine = findNextLine(); + } return mNextLine; } /** Moves to the next line of the file. */ - public void advance() throws IOException { + public void advance() { mPreviousLine = mNextLine; - mNextLine = findNextLine(); + mNextLine = null; } public boolean isAtEndOfFile() { - return mNextLine == null; + return mAtEndOfFile; } - /** Returns the previous line, for error messages. */ - public String getPreviousLine() { + /** + * Returns the previous line, for error messages. Will be {@code null} if we're at the start + * of the file. + */ + public @Nullable String getPreviousLine() { return mPreviousLine; } 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 d834020ca328..42c60b0ba0da 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/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java index 2559bd036039..e2af7c03b5fa 100644 --- a/core/java/android/app/ApplicationStartInfo.java +++ b/core/java/android/app/ApplicationStartInfo.java @@ -1082,6 +1082,15 @@ public final class ApplicationStartInfo implements Parcelable { final ApplicationStartInfo o = (ApplicationStartInfo) other; + boolean intentEquals = true; + if (android.content.flags.Flags.intentSaveToXmlPackage()) { + if (mStartIntent == null) { + intentEquals = o.mStartIntent == null; + } else { + intentEquals = mStartIntent.filterEquals(o.mStartIntent); + } + } + return mPid == o.mPid && mRealUid == o.mRealUid && mPackageUid == o.mPackageUid @@ -1095,14 +1104,16 @@ public final class ApplicationStartInfo implements Parcelable { && timestampsEquals(o) && mWasForceStopped == o.mWasForceStopped && mMonotonicCreationTimeMs == o.mMonotonicCreationTimeMs - && mStartComponent == o.mStartComponent; + && mStartComponent == o.mStartComponent + && intentEquals; } @Override public int hashCode() { return Objects.hash(mPid, mRealUid, mPackageUid, mDefiningUid, mReason, mStartupState, mStartType, mLaunchMode, mPackageName, mProcessName, mStartupTimestampsNs, - mMonotonicCreationTimeMs, mStartComponent); + mMonotonicCreationTimeMs, mStartComponent, + android.content.flags.Flags.intentSaveToXmlPackage() ? mStartIntent : null); } private boolean timestampsEquals(@NonNull ApplicationStartInfo other) { 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/notification.aconfig b/core/java/android/app/notification.aconfig index 6f0eafe487af..d764c58b2b1e 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -385,3 +385,13 @@ flag { description: "Shows summarized notifications in the UI" bug: "390217880" } + +flag { + name: "nm_collapsed_lines" + namespace: "systemui" + description: "Shows 2 lines for collapsed notifications by default" + bug: "390217880" + metadata { + purpose: PURPOSE_BUGFIX + } +} 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/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/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index 6e9dcf5a83a1..335448bf131e 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -79,6 +79,7 @@ import android.util.Log; import android.util.Pair; import android.util.Size; import android.view.Display; +import android.window.DesktopModeFlags; import com.android.internal.camera.flags.Flags; import com.android.internal.util.ArrayUtils; @@ -1685,7 +1686,7 @@ public final class CameraManager { */ public static int getRotationOverride(@Nullable Context context, @Nullable PackageManager packageManager, @Nullable String packageName) { - if (com.android.window.flags.Flags.enableCameraCompatForDesktopWindowing()) { + if (DesktopModeFlags.ENABLE_CAMERA_COMPAT_SIMULATE_REQUESTED_ORIENTATION.isTrue()) { return getRotationOverrideInternal(context, packageManager, packageName); } else { return shouldOverrideToPortrait(packageManager, packageName) 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/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 6c2ce3685b30..c41a5ce02e61 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -213,3 +213,12 @@ flag { is_fixed_read_only: true } +flag { + name: "fix_search_modifier_fallbacks" + namespace: "input" + description: "Fixes a bug in which fallbacks from Search based key combinations were not activating." + bug: "384113980" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/hardware/serial/flags/flags.aconfig b/core/java/android/hardware/serial/flags/flags.aconfig index d8244ba55fcc..bdb8b40af2bf 100644 --- a/core/java/android/hardware/serial/flags/flags.aconfig +++ b/core/java/android/hardware/serial/flags/flags.aconfig @@ -1,4 +1,4 @@ -package: "android.hardware.serial" +package: "android.hardware.serial.flags" container: "system" flag { 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..c3c28b20d649 100644 --- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java +++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java @@ -34,8 +34,6 @@ import android.util.Printer; import android.util.SparseArray; import android.util.proto.ProtoOutputStream; -import com.android.internal.ravenwood.RavenwoodEnvironment; - import dalvik.annotation.optimization.NeverCompile; import java.io.FileDescriptor; @@ -144,6 +142,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. @@ -1115,7 +1119,6 @@ public final class MessageQueue { msg.markInUse(); msg.arg1 = token; - incAndTraceMessageCount(msg, when); if (!enqueueMessageUnchecked(msg, when)) { Log.wtf(TAG_C, "Unexpected error while adding sync barrier!"); @@ -1131,7 +1134,6 @@ public final class MessageQueue { msg.markInUse(); msg.when = when; msg.arg1 = token; - incAndTraceMessageCount(msg, when); if (Flags.messageQueueTailTracking() && mLast != null && mLast.when <= when) { /* Message goes to tail of list */ diff --git a/core/java/android/os/PerfettoTrace.java b/core/java/android/os/PerfettoTrace.java index 932836f8a050..8b96b89c949a 100644 --- a/core/java/android/os/PerfettoTrace.java +++ b/core/java/android/os/PerfettoTrace.java @@ -232,7 +232,8 @@ public final class PerfettoTrace { * @param eventName The event name to appear in the trace. */ public static PerfettoTrackEventExtra.Builder instant(Category category, String eventName) { - return PerfettoTrackEventExtra.builder().init(PERFETTO_TE_TYPE_INSTANT, category) + return PerfettoTrackEventExtra.builder(category.isEnabled()) + .init(PERFETTO_TE_TYPE_INSTANT, category) .setEventName(eventName); } @@ -243,7 +244,8 @@ public final class PerfettoTrace { * @param eventName The event name to appear in the trace. */ public static PerfettoTrackEventExtra.Builder begin(Category category, String eventName) { - return PerfettoTrackEventExtra.builder().init(PERFETTO_TE_TYPE_SLICE_BEGIN, category) + return PerfettoTrackEventExtra.builder(category.isEnabled()) + .init(PERFETTO_TE_TYPE_SLICE_BEGIN, category) .setEventName(eventName); } @@ -253,7 +255,8 @@ public final class PerfettoTrace { * @param category The perfetto category. */ public static PerfettoTrackEventExtra.Builder end(Category category) { - return PerfettoTrackEventExtra.builder().init(PERFETTO_TE_TYPE_SLICE_END, category); + return PerfettoTrackEventExtra.builder(category.isEnabled()) + .init(PERFETTO_TE_TYPE_SLICE_END, category); } /** @@ -263,7 +266,8 @@ public final class PerfettoTrace { * @param value The value of the counter. */ public static PerfettoTrackEventExtra.Builder counter(Category category, long value) { - return PerfettoTrackEventExtra.builder().init(PERFETTO_TE_TYPE_COUNTER, category) + return PerfettoTrackEventExtra.builder(category.isEnabled()) + .init(PERFETTO_TE_TYPE_COUNTER, category) .setCounter(value); } @@ -286,7 +290,8 @@ public final class PerfettoTrace { * @param value The value of the counter. */ public static PerfettoTrackEventExtra.Builder counter(Category category, double value) { - return PerfettoTrackEventExtra.builder().init(PERFETTO_TE_TYPE_COUNTER, category) + return PerfettoTrackEventExtra.builder(category.isEnabled()) + .init(PERFETTO_TE_TYPE_COUNTER, category) .setCounter(value); } diff --git a/core/java/android/os/PerfettoTrackEventExtra.java b/core/java/android/os/PerfettoTrackEventExtra.java index 07b44a87ef88..5fc7cf7be246 100644 --- a/core/java/android/os/PerfettoTrackEventExtra.java +++ b/core/java/android/os/PerfettoTrackEventExtra.java @@ -37,6 +37,7 @@ import java.util.function.Supplier; public final class PerfettoTrackEventExtra { private static final boolean DEBUG = false; private static final int DEFAULT_EXTRA_CACHE_SIZE = 5; + private static final Builder NO_OP_BUILDER = new Builder(/* extra= */ null, /* isCategoryEnabled= */ false); private static final ThreadLocal<PerfettoTrackEventExtra> sTrackEventExtra = new ThreadLocal<PerfettoTrackEventExtra>() { @Override @@ -153,8 +154,8 @@ public final class PerfettoTrackEventExtra { private Builder mParent; private FieldContainer mCurrentContainer; - private boolean mIsCategoryEnabled; + private final boolean mIsCategoryEnabled; private final CounterInt64 mCounterInt64; private final CounterDouble mCounterDouble; private final Proto mProto; @@ -175,24 +176,29 @@ public final class PerfettoTrackEventExtra { private final Pool<Builder> mBuilderCache; private Builder() { - mExtra = sTrackEventExtra.get(); - mNamedTrackCache = mExtra.mNamedTrackCache; - mCounterTrackCache = mExtra.mCounterTrackCache; - mArgInt64Cache = mExtra.mArgInt64Cache; - mArgDoubleCache = mExtra.mArgDoubleCache; - mArgBoolCache = mExtra.mArgBoolCache; - mArgStringCache = mExtra.mArgStringCache; - mFieldInt64Cache = mExtra.mFieldInt64Cache; - mFieldDoubleCache = mExtra.mFieldDoubleCache; - mFieldStringCache = mExtra.mFieldStringCache; - mFieldNestedCache = mExtra.mFieldNestedCache; - mBuilderCache = mExtra.mBuilderCache; - - mCounterInt64 = mExtra.getCounterInt64(); - mCounterDouble = mExtra.getCounterDouble(); - mProto = mExtra.getProto(); - mFlow = mExtra.getFlow(); - mTerminatingFlow = mExtra.getTerminatingFlow(); + this(sTrackEventExtra.get(), true); + } + + public Builder(PerfettoTrackEventExtra extra, boolean isCategoryEnabled) { + mIsCategoryEnabled = isCategoryEnabled; + mExtra = extra; + mNamedTrackCache = mExtra == null ? null : mExtra.mNamedTrackCache; + mCounterTrackCache = mExtra == null ? null : mExtra.mCounterTrackCache; + mArgInt64Cache = mExtra == null ? null : mExtra.mArgInt64Cache; + mArgDoubleCache = mExtra == null ? null : mExtra.mArgDoubleCache; + mArgBoolCache = mExtra == null ? null : mExtra.mArgBoolCache; + mArgStringCache = mExtra == null ? null : mExtra.mArgStringCache; + mFieldInt64Cache = mExtra == null ? null : mExtra.mFieldInt64Cache; + mFieldDoubleCache = mExtra == null ? null : mExtra.mFieldDoubleCache; + mFieldStringCache = mExtra == null ? null : mExtra.mFieldStringCache; + mFieldNestedCache = mExtra == null ? null : mExtra.mFieldNestedCache; + mBuilderCache = mExtra == null ? null : mExtra.mBuilderCache; + + mCounterInt64 = mExtra == null ? null : mExtra.getCounterInt64(); + mCounterDouble = mExtra == null ? null : mExtra.getCounterDouble(); + mProto = mExtra == null ? null : mExtra.getProto(); + mFlow = mExtra == null ? null : mExtra.getFlow(); + mTerminatingFlow = mExtra == null ? null : mExtra.getTerminatingFlow(); } /** @@ -214,6 +220,10 @@ public final class PerfettoTrackEventExtra { * Initialize the builder for a new trace event. */ public Builder init(int traceType, PerfettoTrace.Category category) { + if (!mIsCategoryEnabled) { + return this; + } + mTraceType = traceType; mCategory = category; mEventName = ""; @@ -225,7 +235,7 @@ public final class PerfettoTrackEventExtra { mExtra.reset(); // Reset after on init in case the thread created builders without calling emit - return initInternal(this, null, category.isEnabled()); + return initInternal(this, null); } /** @@ -529,7 +539,7 @@ public final class PerfettoTrackEventExtra { } mProto.clearFields(); mExtra.addPerfettoPointer(mProto); - return mBuilderCache.get(sBuilderSupplier).initInternal(this, mProto, true); + return mBuilderCache.get(sBuilderSupplier).initInternal(this, mProto); } /** @@ -560,7 +570,7 @@ public final class PerfettoTrackEventExtra { FieldNested field = mFieldNestedCache.get(sFieldNestedSupplier); field.setId(id); mExtra.addPerfettoPointer(mCurrentContainer, field); - return mBuilderCache.get(sBuilderSupplier).initInternal(this, field, true); + return mBuilderCache.get(sBuilderSupplier).initInternal(this, field); } /** @@ -577,11 +587,9 @@ public final class PerfettoTrackEventExtra { } - private Builder initInternal(Builder parent, FieldContainer field, - boolean isCategoryEnabled) { + private Builder initInternal(Builder parent, FieldContainer field) { mParent = parent; mCurrentContainer = field; - mIsCategoryEnabled = isCategoryEnabled; mIsBuilt = false; return this; @@ -613,9 +621,12 @@ public final class PerfettoTrackEventExtra { /** * Start a {@link Builder} to build a {@link PerfettoTrackEventExtra}. */ - public static Builder builder() { - return sTrackEventExtra.get().mBuilderCache.get(sBuilderSupplier).initInternal(null, null, - false); + public static Builder builder(boolean isCategoryEnabled) { + if (isCategoryEnabled) { + return sTrackEventExtra.get().mBuilderCache.get(sBuilderSupplier) + .initInternal(null, null); + } + return NO_OP_BUILDER; } private final RingBuffer<NamedTrack> mNamedTrackCache = 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/OrientationEventListener.java b/core/java/android/view/OrientationEventListener.java index 2feb44ffc3ae..892b80bd0213 100644 --- a/core/java/android/view/OrientationEventListener.java +++ b/core/java/android/view/OrientationEventListener.java @@ -26,8 +26,7 @@ import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.util.Log; - -import com.android.window.flags.Flags; +import android.window.DesktopModeFlags; /** * Helper class for receiving notifications from the SensorManager when @@ -77,9 +76,10 @@ public abstract class OrientationEventListener { mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); if (mSensor != null) { // Create listener only if sensors do exist. - mSensorEventListener = Flags.enableCameraCompatForDesktopWindowing() - ? new CompatSensorEventListenerImpl(new SensorEventListenerImpl()) - : new SensorEventListenerImpl(); + mSensorEventListener = + DesktopModeFlags.ENABLE_CAMERA_COMPAT_SIMULATE_REQUESTED_ORIENTATION.isTrue() + ? new CompatSensorEventListenerImpl(new SensorEventListenerImpl()) + : new SensorEventListenerImpl(); } } diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index 97cf8fc748e8..8944c3fa0b19 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -120,7 +120,7 @@ public class WindowManagerImpl implements WindowManager { this(context, null /* parentWindow */, null /* clientToken */); } - private WindowManagerImpl(Context context, Window parentWindow, + public WindowManagerImpl(Context context, Window parentWindow, @Nullable IBinder windowContextToken) { mContext = context; mParentWindow = parentWindow; 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..d2893a46d3f3 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -44,10 +44,12 @@ 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), + ENABLE_CAMERA_COMPAT_SIMULATE_REQUESTED_ORIENTATION( + Flags::enableCameraCompatForDesktopWindowing, true), ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION(Flags::enableCaptionCompatInsetForceConsumption, true), ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS( @@ -103,7 +105,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 +117,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/com/android/internal/jank/DisplayResolutionTracker.java b/core/java/com/android/internal/jank/DisplayResolutionTracker.java index 5d66b3c10197..74153157df4d 100644 --- a/core/java/com/android/internal/jank/DisplayResolutionTracker.java +++ b/core/java/com/android/internal/jank/DisplayResolutionTracker.java @@ -142,14 +142,23 @@ public class DisplayResolutionTracker { public interface DisplayInterface { /** Reurns an implementation wrapping {@link DisplayManagerGlobal}. */ static DisplayInterface getDefault(@Nullable Handler handler) { + long displayEventsToBeSubscribed; + if (com.android.server.display.feature.flags.Flags + .displayListenerPerformanceImprovements() + && com.android.server.display.feature.flags.Flags + .delayImplicitRrRegistrationUntilRrAccessed()) { + displayEventsToBeSubscribed = DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED + | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED; + } else { + displayEventsToBeSubscribed = DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED + | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED + | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE; + } DisplayManagerGlobal manager = DisplayManagerGlobal.getInstance(); return new DisplayInterface() { @Override public void registerDisplayListener(DisplayManager.DisplayListener listener) { - manager.registerDisplayListener(listener, handler, - DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED - | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED - | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE, + manager.registerDisplayListener(listener, handler, displayEventsToBeSubscribed, ActivityThread.currentPackageName()); } diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index 6f7e5ad51b89..7cecc39dacde 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -66,6 +66,7 @@ import com.android.internal.protolog.IProtoLogConfigurationService.RegisterClien import com.android.internal.protolog.common.ILogger; import com.android.internal.protolog.common.IProtoLog; import com.android.internal.protolog.common.IProtoLogGroup; +import com.android.internal.protolog.common.InvalidFormatStringException; import com.android.internal.protolog.common.LogDataType; import com.android.internal.protolog.common.LogLevel; @@ -207,7 +208,12 @@ public abstract class PerfettoProtoLogImpl extends IProtoLogClient.Stub implemen @Override public void log(LogLevel logLevel, IProtoLogGroup group, String messageString, Object... args) { - log(logLevel, group, new Message(messageString), args); + try { + log(logLevel, group, new Message(messageString), args); + } catch (InvalidFormatStringException e) { + Slog.e(LOG_TAG, "Invalid protolog string format", e); + log(logLevel, group, new Message("INVALID MESSAGE"), new Object[0]); + } } /** @@ -831,7 +837,7 @@ public abstract class PerfettoProtoLogImpl extends IProtoLogClient.Stub implemen this.mMessageString = null; } - private Message(@NonNull String messageString) { + private Message(@NonNull String messageString) throws InvalidFormatStringException { this.mMessageHash = null; final List<Integer> argTypes = LogDataType.parseFormatString(messageString); this.mMessageMask = LogDataType.logDataTypesToBitMask(argTypes); diff --git a/core/java/com/android/internal/protolog/common/LogDataType.java b/core/java/com/android/internal/protolog/common/LogDataType.java index c05824a58a77..10b07d9387de 100644 --- a/core/java/com/android/internal/protolog/common/LogDataType.java +++ b/core/java/com/android/internal/protolog/common/LogDataType.java @@ -86,8 +86,8 @@ public class LogDataType { case '%': break; default: - throw new InvalidFormatStringException("Invalid format string field" - + " %${messageString[i + 1]}"); + throw new InvalidFormatStringException("Invalid Protolog message format in " + + "\"" + messageString + "\" at index " + i + "."); } i += 2; } else { 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/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java index 3da19220248b..822ef34ad925 100644 --- a/core/java/com/android/internal/widget/ConversationLayout.java +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -401,9 +401,19 @@ public class ConversationLayout extends FrameLayout @RemotableViewMethod(asyncImpl = "setIsCollapsedAsync") public void setIsCollapsed(boolean isCollapsed) { mIsCollapsed = isCollapsed; - mMessagingLinearLayout.setMaxDisplayedLines(isCollapsed - ? TextUtils.isEmpty(mSummarizedContent) ? 1 : MAX_SUMMARIZATION_LINES - : Integer.MAX_VALUE); + int maxLines = Integer.MAX_VALUE; + if (isCollapsed) { + if (!TextUtils.isEmpty(mSummarizedContent)) { + maxLines = MAX_SUMMARIZATION_LINES; + } else { + if (android.app.Flags.nmCollapsedLines()) { + maxLines = 2; + } else { + maxLines = 1; + } + } + } + mMessagingLinearLayout.setMaxDisplayedLines(maxLines); updateExpandButton(); updateContentEndPaddings(); } @@ -1177,7 +1187,9 @@ public class ConversationLayout extends FrameLayout nameOverride = mNameReplacement; } newGroup.setShowingAvatar(!mIsOneToOne && !mIsCollapsed); - newGroup.setSingleLine(mIsCollapsed && TextUtils.isEmpty(mSummarizedContent)); + newGroup.setSingleLine(mIsCollapsed + ? !android.app.Flags.nmCollapsedLines() && TextUtils.isEmpty(mSummarizedContent) + : false); newGroup.setIsCollapsed(mIsCollapsed); newGroup.setSender(sender, nameOverride); newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner); diff --git a/core/java/com/android/internal/widget/MessagingLayout.java b/core/java/com/android/internal/widget/MessagingLayout.java index 4cc4b38f95a5..f9c8228e455d 100644 --- a/core/java/com/android/internal/widget/MessagingLayout.java +++ b/core/java/com/android/internal/widget/MessagingLayout.java @@ -157,6 +157,10 @@ public class MessagingLayout extends FrameLayout @RemotableViewMethod(asyncImpl = "setIsCollapsedAsync") public void setIsCollapsed(boolean isCollapsed) { mIsCollapsed = isCollapsed; + if (mIsCollapsed) { + mMessagingLinearLayout.setMaxDisplayedLines( + android.app.Flags.nmCollapsedLines() ? 2 : 1); + } } /** @@ -549,7 +553,9 @@ public class MessagingLayout extends FrameLayout if (sender != mUser && mNameReplacement != null) { nameOverride = mNameReplacement; } - newGroup.setSingleLine(mIsCollapsed && TextUtils.isEmpty(mSummarizedContent)); + newGroup.setSingleLine(mIsCollapsed + ? !android.app.Flags.nmCollapsedLines() && TextUtils.isEmpty(mSummarizedContent) + : false); newGroup.setShowingAvatar(!mIsCollapsed); newGroup.setIsCollapsed(mIsCollapsed); newGroup.setSender(sender, nameOverride); diff --git a/core/java/com/android/server/servicewatcher/OWNERS b/core/java/com/android/server/servicewatcher/OWNERS index ced619f05f1d..dbc598e1547f 100644 --- a/core/java/com/android/server/servicewatcher/OWNERS +++ b/core/java/com/android/server/servicewatcher/OWNERS @@ -1,5 +1,5 @@ # Bug component: 25692 -sooniln@google.com +dnchrist@google.com wyattriley@google.com 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/values/config.xml b/core/res/res/values/config.xml index e47adc90fc7a..7bb799a24ef1 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> @@ -3207,6 +3209,10 @@ Note that HSUM devices without this enabled will not automatically have a main user. --> <bool name="config_isMainUserPermanentAdmin">true</bool> + <!-- Whether all secondary users (and the system user) are eligible to have profiles. + If false, only the MainUser is eligible to have profiles. --> + <bool name="config_supportProfilesOnNonMainUser">false</bool> + <!-- Whether switch to headless system user is allowed. If allowed, headless system user can run in the foreground even though it is not a full user. --> <bool name="config_canSwitchToHeadlessSystemUser">false</bool> 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/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index a37ca2847638..2dc5687a1253 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -363,6 +363,7 @@ <java-symbol type="bool" name="config_speed_up_audio_on_mt_calls" /> <java-symbol type="bool" name="config_useFixedVolume" /> <java-symbol type="bool" name="config_isMainUserPermanentAdmin"/> + <java-symbol type="bool" name="config_supportProfilesOnNonMainUser"/> <java-symbol type="bool" name="config_canSwitchToHeadlessSystemUser"/> <java-symbol type="bool" name="config_enableMultiUserUI"/> <java-symbol type="bool" name="config_enableMultipleAdmins"/> 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/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/drawable/desktop_windowing_transition_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml index dd1a1b1dca13..75ec2ab9f6f9 100644 --- a/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml +++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml @@ -18,15 +18,15 @@ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> <item android:id="@+id/indicator_solid"> <shape android:shape="rectangle"> - <solid android:color="@androidprv:color/materialColorPrimaryContainer" /> + <solid android:color="@androidprv:color/materialColorPrimaryFixed" /> <corners android:radius="28dp" /> </shape> </item> <item android:id="@+id/indicator_stroke"> <shape android:shape="rectangle"> <corners android:radius="28dp" /> - <stroke android:width="1dp" - android:color="@androidprv:color/materialColorPrimaryContainer"/> + <stroke android:width="2dp" + android:color="@androidprv:color/materialColorPrimaryFixed"/> </shape> </item> </layer-list> 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/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml index 8d18f959951b..5732fc936b47 100644 --- a/libs/WindowManager/Shell/res/values/colors.xml +++ b/libs/WindowManager/Shell/res/values/colors.xml @@ -68,8 +68,6 @@ <color name="desktop_mode_caption_button_on_hover_light">#11000000</color> <color name="desktop_mode_caption_button_on_hover_dark">#11FFFFFF</color> <color name="desktop_mode_caption_button">#00000000</color> - <color name="tiling_divider_background_light">#C9C7B6</color> - <color name="tiling_divider_background_dark">#4A4739</color> <color name="tiling_handle_background_light">#000000</color> <color name="tiling_handle_background_dark">#FFFFFF</color> </resources> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index e68680219349..ca18c97f9127 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -304,6 +304,8 @@ <dimen name="bubble_transform_area_width">140dp</dimen> <!-- Width of the box at the corner of the screen where drag leads to app moving to bubble --> <dimen name="bubble_transform_area_height">140dp</dimen> + <!-- How much elevation a bubble ui needs when dragged, must be above drop target & dismiss. --> + <dimen name="dragged_bubble_elevation">3dp</dimen> <!-- Bottom and end margin for compat buttons. --> <dimen name="compat_button_margin">24dp</dimen> @@ -534,10 +536,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> @@ -566,6 +568,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/shared/res/values/dimen.xml b/libs/WindowManager/Shell/shared/res/values/dimen.xml index 3b504cf713f1..74b6023bde36 100644 --- a/libs/WindowManager/Shell/shared/res/values/dimen.xml +++ b/libs/WindowManager/Shell/shared/res/values/dimen.xml @@ -39,9 +39,9 @@ <dimen name="drag_zone_v_split_from_expanded_view_height_fold_short">100dp</dimen> <!-- Bubble drop target dimensions --> - <dimen name="drop_target_elevation">1dp</dimen> + <dimen name="drop_target_elevation">2dp</dimen> <dimen name="drop_target_radius">28dp</dimen> - <dimen name="drop_target_stroke">1dp</dimen> + <dimen name="drop_target_stroke">2dp</dimen> <dimen name="drop_target_full_screen_padding">20dp</dimen> <dimen name="drop_target_desktop_window_padding_small">100dp</dimen> <dimen name="drop_target_desktop_window_padding_large">130dp</dimen> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt index 73277310ffe4..df101fe44b75 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetView.kt @@ -30,15 +30,15 @@ import com.android.wm.shell.shared.R class DropTargetView(context: Context) : View(context) { private val rectPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = context.getColor(com.android.internal.R.color.materialColorPrimaryContainer) + color = context.getColor(com.android.internal.R.color.materialColorPrimaryFixed) style = Paint.Style.FILL alpha = (0.35f * 255).toInt() } private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = context.getColor(com.android.internal.R.color.materialColorPrimaryContainer) + color = context.getColor(com.android.internal.R.color.materialColorPrimaryFixed) style = Paint.Style.STROKE - strokeWidth = 1.dpToPx() + strokeWidth = 2.dpToPx() } private val cornerRadius = 28.dpToPx() 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/bar/BubbleBarExpandedViewDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt index 35435569d8b1..44d859dfb9ba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt @@ -17,9 +17,11 @@ package com.android.wm.shell.bubbles.bar import android.annotation.SuppressLint +import android.content.Context import android.view.MotionEvent import android.view.View import androidx.annotation.VisibleForTesting +import com.android.wm.shell.R import com.android.wm.shell.bubbles.BubblePositioner import com.android.wm.shell.shared.bubbles.BubbleBarLocation import com.android.wm.shell.shared.bubbles.DismissView @@ -32,6 +34,7 @@ import com.android.wm.shell.shared.magnetictarget.MagnetizedObject /** Controller for handling drag interactions with [BubbleBarExpandedView] */ @SuppressLint("ClickableViewAccessibility") class BubbleBarExpandedViewDragController( + private val context: Context, private val expandedView: BubbleBarExpandedView, private val dismissView: DismissView, private val animationHelper: BubbleBarAnimationHelper, @@ -54,6 +57,8 @@ class BubbleBarExpandedViewDragController( MagnetizedObject.magnetizeView(expandedView) private val magnetizedDismissTarget: MagnetizedObject.MagneticTarget + private val draggedBubbleElevation: Float + init { magnetizedExpandedView.magnetListener = MagnetListener() magnetizedExpandedView.animateStuckToTarget = @@ -70,6 +75,8 @@ class BubbleBarExpandedViewDragController( MagnetizedObject.MagneticTarget(dismissView.circle, dismissView.circle.width) magnetizedExpandedView.addTarget(magnetizedDismissTarget) + draggedBubbleElevation = context.resources.getDimension( + R.dimen.dragged_bubble_elevation) val dragMotionEventHandler = HandleDragListener() expandedView.handleView.setOnTouchListener { view, event -> @@ -103,6 +110,7 @@ class BubbleBarExpandedViewDragController( override fun onDown(v: View, ev: MotionEvent): Boolean { // While animating, don't allow new touch events if (expandedView.isAnimating) return false + expandedView.z = draggedBubbleElevation if (dropTargetManager != null && dragZoneFactory != null) { val draggedObject = DraggedObject.ExpandedView( if (bubblePositioner.isBubbleBarOnLeft) { @@ -154,11 +162,13 @@ class BubbleBarExpandedViewDragController( velX: Float, velY: Float, ) { + v.translationZ = 0f finishDrag() } override fun onCancel(v: View, ev: MotionEvent, viewInitialX: Float, viewInitialY: Float) { isStuckToDismiss = false + v.translationZ = 0f finishDrag() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index 2cc9387bd1e9..dd86725f7944 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -349,6 +349,7 @@ public class BubbleBarLayerView extends FrameLayout } }; mDragController = new BubbleBarExpandedViewDragController( + mContext, mExpandedView, mDismissView, mAnimationHelper, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java index 97184c859d4d..46f6e40ec5e8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -287,7 +287,13 @@ public class DisplayController { ? mContext : mContext.createDisplayContext(display); final Context context = perDisplayContext.createConfigurationContext(newConfig); - dr.setDisplayLayout(context, new DisplayLayout(context, display)); + final DisplayLayout displayLayout = new DisplayLayout(context, display); + if (mDisplayTopology != null) { + displayLayout.setGlobalBoundsDp( + mDisplayTopology.getAbsoluteBounds().get( + displayId, displayLayout.globalBoundsDp())); + } + dr.setDisplayLayout(context, displayLayout); for (int i = 0; i < mDisplayChangedListeners.size(); ++i) { mDisplayChangedListeners.get(i).onDisplayConfigurationChanged( displayId, newConfig); 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/DividerRoundedCorner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java index cf0ecae7c815..a1e7ff04347f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java @@ -106,31 +106,37 @@ public class DividerRoundedCorner extends View { * of non split screen. * * @param isSplitScreen Whether the divider is used by split screen or tiling. - * @param isDarkMode Whether the mode is ui dark mode. + * @param color Rounded corner color. */ - public void setup(boolean isSplitScreen, boolean isDarkMode) { + public void setup(boolean isSplitScreen, int color) { mIsSplitScreen = isSplitScreen; if (!isSplitScreen) { - mDividerBarBackground.setColor(getTilingHandleColor(isDarkMode)); + mDividerBarBackground.setColor(color); } } /** - * Notifies the divider of ui mode change. + * Notifies the divider of ui mode change and provides a new color. * - * @param isDarkMode Whether the mode is ui dark mode. + * @param color The new divider rounded corner color. */ - public void onUiModeChange(boolean isDarkMode) { + public void onUiModeChange(int color) { if (!mIsSplitScreen) { - mDividerBarBackground.setColor(getTilingHandleColor(isDarkMode)); + mDividerBarBackground.setColor(color); invalidate(); } } - private int getTilingHandleColor(boolean isDarkMode) { - return isDarkMode ? getResources().getColor( - R.color.tiling_divider_background_dark, null /* theme */) : getResources().getColor( - R.color.tiling_divider_background_light, null /* theme */); + /** + * Notifies rounded corner view of color change. + * + * @param color The new divider rounded corner color. + */ + public void onCornerColorChange(int color) { + if (!mIsSplitScreen) { + mDividerBarBackground.setColor(color); + invalidate(); + } } /** 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..5fbbb0bf1e78 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; @@ -89,6 +90,7 @@ import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; import com.android.wm.shell.desktopmode.DesktopDisplayEventHandler; import com.android.wm.shell.desktopmode.DesktopDisplayModeController; +import com.android.wm.shell.desktopmode.DesktopImeHandler; import com.android.wm.shell.desktopmode.DesktopImmersiveController; import com.android.wm.shell.desktopmode.DesktopMinimizationTransitionHandler; import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler; @@ -1044,7 +1046,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 +1065,7 @@ public abstract class WMShellModule { activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy, desktopTilingDecorViewModel, - multiDisplayDragMoveIndicatorController)); + multiDisplayDragMoveIndicatorController, compatUI.orElse(null))); } @WMSingleton @@ -1213,7 +1216,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 +1226,8 @@ public abstract class WMShellModule { shellTaskOrganizer, taskStackListener, toggleResizeDesktopTaskTransitionHandler, - desktopUserRepositories)); + desktopUserRepositories, + displayController)); } return Optional.empty(); } @@ -1341,7 +1346,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 +1362,9 @@ public abstract class WMShellModule { context, shellInit, mainScope, + shellController, displayController, + rootTaskDisplayAreaOrganizer, desktopRepositoryInitializer, desktopUserRepositories.get(), desktopTasksController.get(), @@ -1518,6 +1527,18 @@ public abstract class WMShellModule { mainHandler)); } + @WMSingleton + @Provides + static Optional<DesktopImeHandler> provideDesktopImeHandler( + DisplayImeController displayImeController, + Context context, + ShellInit shellInit) { + if (!DesktopModeStatus.canEnterDesktopMode(context)) { + return Optional.empty(); + } + return Optional.of(new DesktopImeHandler(displayImeController, shellInit)); + } + // // App zoom out // 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/DesktopImeHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImeHandler.kt new file mode 100644 index 000000000000..93ba71a17937 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImeHandler.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.view.SurfaceControl +import com.android.window.flags.Flags +import com.android.wm.shell.common.DisplayImeController +import com.android.wm.shell.common.DisplayImeController.ImePositionProcessor.IME_ANIMATION_DEFAULT +import com.android.wm.shell.sysui.ShellInit + +/** Handles the interactions between IME and desktop tasks */ +class DesktopImeHandler( + private val displayImeController: DisplayImeController, + shellInit: ShellInit, +) : DisplayImeController.ImePositionProcessor { + + init { + shellInit.addInitCallback(::onInit, this) + } + + private fun onInit() { + if (Flags.enableDesktopImeBugfix()) { + displayImeController.addPositionProcessor(this) + } + } + + override fun onImeStartPositioning( + displayId: Int, + hiddenTop: Int, + shownTop: Int, + showing: Boolean, + isFloating: Boolean, + t: SurfaceControl.Transaction?, + ): Int { + return IME_ANIMATION_DEFAULT + } +} 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/DesktopModeUiEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt index b9cb32d8a14f..e0300d688379 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt @@ -167,7 +167,35 @@ class DesktopModeUiEventLogger( @UiEvent(doc = "Exit desktop mode education tooltip on the app header menu is clicked") EXIT_DESKTOP_MODE_EDUCATION_TOOLTIP_CLICKED(2104), @UiEvent(doc = "Exit desktop mode education tooltip is dismissed by the user") - EXIT_DESKTOP_MODE_EDUCATION_TOOLTIP_DISMISSED(2105); + EXIT_DESKTOP_MODE_EDUCATION_TOOLTIP_DISMISSED(2105), + @UiEvent(doc = "A11y service opened app handle menu by selecting handle from fullscreen") + A11Y_APP_HANDLE_MENU_OPENED(2156), + @UiEvent(doc = "A11y service opened app handle menu through Switch Access actions menu ") + A11Y_SYSTEM_ACTION_APP_HANDLE_MENU(2157), + @UiEvent(doc = "A11y service selected desktop mode from app handle menu") + A11Y_APP_HANDLE_MENU_DESKTOP_VIEW(2158), + @UiEvent(doc = "A11y service selected fullscreen mode from app handle menu") + A11Y_APP_HANDLE_MENU_FULLSCREEN(2159), + @UiEvent(doc = "A11y service selected split screen mode from app handle menu") + A11Y_APP_HANDLE_MENU_SPLIT_SCREEN(2160), + @UiEvent(doc = "A11y service selected maximize/restore button from app header") + A11Y_APP_WINDOW_MAXIMIZE_RESTORE_BUTTON(2161), + @UiEvent(doc = "A11y service selected minimize button from app header") + A11Y_APP_WINDOW_MINIMIZE_BUTTON(2162), + @UiEvent(doc = "A11y service selected close button from app header") + A11Y_APP_WINDOW_CLOSE_BUTTON(2163), + @UiEvent(doc = "A11y service selected maximize button from app header maximize menu") + A11Y_MAXIMIZE_MENU_MAXIMIZE(2164), + @UiEvent(doc = "A11y service selected resize left button from app header maximize menu") + A11Y_MAXIMIZE_MENU_RESIZE_LEFT(2165), + @UiEvent(doc = "A11y service selected resize right button from app header maximize menu") + A11Y_MAXIMIZE_MENU_RESIZE_RIGHT(2166), + @UiEvent(doc = "A11y service triggered a11y action to maximize/restore app window") + A11Y_ACTION_MAXIMIZE_RESTORE(2167), + @UiEvent(doc = "A11y service triggered a11y action to resize app window left") + A11Y_ACTION_RESIZE_LEFT(2168), + @UiEvent(doc = "A11y service triggered a11y action to resize app window right") + A11Y_ACTION_RESIZE_RIGHT(2169); override fun getId(): Int = mId } 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/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index e77acfb805a6..25fdbb348356 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -1112,6 +1112,7 @@ class DesktopRepository( internal fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("${prefix}DesktopRepository") + pw.println("${innerPrefix}userId=$userId") dumpDesktopTaskData(pw, innerPrefix) pw.println("${innerPrefix}activeTasksListeners=${activeTasksListeners.size}") pw.println("${innerPrefix}visibleTasksListeners=${visibleTasksListeners.size}") @@ -1298,6 +1299,7 @@ class DesktopRepository( deskByDisplayId[displayId]?.let { sequenceOf(it) } ?: emptySequence() override fun remove(deskId: Int) { + setDeskInactive(deskId) deskByDisplayId[deskId]?.clear() } @@ -1397,6 +1399,7 @@ class DesktopRepository( desktopDisplays[displayId]?.orderedDesks?.asSequence() ?: emptySequence() override fun remove(deskId: Int) { + setDeskInactive(deskId) desktopDisplays.forEach { _, display -> display.orderedDesks.removeIf { it.deskId == deskId } } 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/DesktopUserRepositories.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt index c10752d36bf9..72758bd538d6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt @@ -21,6 +21,7 @@ import android.content.Context import android.content.pm.UserInfo import android.os.UserManager import android.util.SparseArray +import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_HSUM import androidx.core.util.forEach import com.android.internal.protolog.ProtoLog @@ -68,7 +69,10 @@ class DesktopUserRepositories( if (DesktopModeStatus.canEnterDesktopMode(context)) { shellInit.addInitCallback(::onInit, this) } - if (ENABLE_DESKTOP_WINDOWING_HSUM.isTrue()) { + if ( + ENABLE_DESKTOP_WINDOWING_HSUM.isTrue() && + !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue + ) { userIdToProfileIdsMap[userId] = userManager.getProfiles(userId).map { it.id } } } @@ -76,6 +80,13 @@ class DesktopUserRepositories( private fun onInit() { repositoryInitializer.initialize(this) shellController.addUserChangeListener(this) + if ( + ENABLE_DESKTOP_WINDOWING_HSUM.isTrue() && + DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue + ) { + userId = ActivityManager.getCurrentUser() + userIdToProfileIdsMap[userId] = userManager.getProfiles(userId).map { it.id } + } } /** Returns [DesktopRepository] for the parent user id. */ @@ -97,10 +108,10 @@ class DesktopUserRepositories( /** Dumps [DesktopRepository] for each user. */ fun dump(pw: PrintWriter, prefix: String) { - desktopRepoByUserId.forEach { key, value -> - pw.println("${prefix}userId=$key") - value.dump(pw, prefix) - } + val innerPrefix = "$prefix " + pw.println("${prefix}DesktopUserRepositories:") + pw.println("${innerPrefix}currentUserId=$userId") + desktopRepoByUserId.forEach { key, value -> value.dump(pw, innerPrefix) } } override fun onUserChanged(newUserId: Int, userContext: Context) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/common/DefaultHomePackageSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/common/DefaultHomePackageSupplier.kt index 8ce624e103ef..bf687f2e4f3a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/common/DefaultHomePackageSupplier.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/common/DefaultHomePackageSupplier.kt @@ -20,6 +20,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.PackageManager import android.os.Handler import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.sysui.ShellInit @@ -28,6 +29,7 @@ import java.util.function.Supplier /** * This supplies the package name of default home in an efficient way. The query to package manager * only executes on initialization and when the preferred activity (e.g. default home) is changed. + * Note that this returns null package name if the default home is setup wizard. */ class DefaultHomePackageSupplier( private val context: Context, @@ -36,6 +38,7 @@ class DefaultHomePackageSupplier( ) : BroadcastReceiver(), Supplier<String?> { private var defaultHomePackage: String? = null + private var isSetupWizard: Boolean = false init { shellInit.addInitCallback({ onInit() }, this) @@ -52,6 +55,14 @@ class DefaultHomePackageSupplier( private fun updateDefaultHomePackage(): String? { defaultHomePackage = context.packageManager.getHomeActivities(ArrayList())?.packageName + isSetupWizard = + defaultHomePackage != null && + context.packageManager.resolveActivity( + Intent() + .setPackage(defaultHomePackage) + .addCategory(Intent.CATEGORY_SETUP_WIZARD), + PackageManager.MATCH_SYSTEM_ONLY, + ) != null return defaultHomePackage } @@ -60,6 +71,7 @@ class DefaultHomePackageSupplier( } override fun get(): String? { + if (isSetupWizard) return null return defaultHomePackage ?: updateDefaultHomePackage() } } 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/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/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 800faca830f4..69e1f36dec0b 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; @@ -1795,6 +1802,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mMultiInstanceHelper, mWindowDecorCaptionHandleRepository, mDesktopModeEventLogger, + mDesktopModeUiEventLogger, mDesktopModeCompatPolicy); mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); @@ -1864,6 +1872,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 ae103895d56b..50bc7b5e865b 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 @@ -93,6 +93,7 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.desktopmode.CaptionState; import com.android.wm.shell.desktopmode.DesktopModeEventLogger; +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger; import com.android.wm.shell.desktopmode.DesktopModeUtils; import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; @@ -163,6 +164,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; @@ -210,6 +212,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final MultiInstanceHelper mMultiInstanceHelper; private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository; private final DesktopUserRepositories mDesktopUserRepositories; + private final DesktopModeUiEventLogger mDesktopModeUiEventLogger; private boolean mIsRecentsTransitionRunning = false; private boolean mIsDragging = false; private Runnable mLoadAppInfoRunnable; @@ -242,6 +245,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin MultiInstanceHelper multiInstanceHelper, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, DesktopModeEventLogger desktopModeEventLogger, + DesktopModeUiEventLogger desktopModeUiEventLogger, DesktopModeCompatPolicy desktopModeCompatPolicy) { this (context, userContext, displayController, taskResourceLoader, splitScreenController, desktopUserRepositories, taskOrganizer, taskInfo, taskSurface, handler, @@ -256,7 +260,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin DefaultMaximizeMenuFactory.INSTANCE, DefaultHandleMenuFactory.INSTANCE, multiInstanceHelper, windowDecorCaptionHandleRepository, desktopModeEventLogger, - desktopModeCompatPolicy); + desktopModeUiEventLogger, desktopModeCompatPolicy); } DesktopModeWindowDecoration( @@ -293,6 +297,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin MultiInstanceHelper multiInstanceHelper, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, DesktopModeEventLogger desktopModeEventLogger, + DesktopModeUiEventLogger desktopModeUiEventLogger, DesktopModeCompatPolicy desktopModeCompatPolicy) { super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface, surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, @@ -320,6 +325,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mTaskResourceLoader = taskResourceLoader; mTaskResourceLoader.onWindowDecorCreated(taskInfo); mDesktopModeCompatPolicy = desktopModeCompatPolicy; + mDesktopModeUiEventLogger = desktopModeUiEventLogger; } /** @@ -408,6 +414,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; @@ -889,10 +900,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mOnCaptionTouchListener, mOnCaptionButtonClickListener, mWindowManagerWrapper, - mHandler + mHandler, + mDesktopModeUiEventLogger ); - } else if (mRelayoutParams.mLayoutResId - == R.layout.desktop_mode_app_header) { + } else if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_header) { return mAppHeaderViewHolderFactory.create( mResult.mRootView, mOnCaptionTouchListener, @@ -902,7 +913,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mOnLeftSnapClickListener, mOnRightSnapClickListener, mOnMaximizeOrRestoreClickListener, - mOnMaximizeHoverListener); + mOnMaximizeHoverListener, + mDesktopModeUiEventLogger); } throw new IllegalArgumentException("Unexpected layout resource id"); } @@ -1366,7 +1378,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mMaximizeMenu = mMaximizeMenuFactory.create(mSyncQueue, mRootTaskDisplayAreaOrganizer, mDisplayController, mTaskInfo, mContext, (width, height) -> calculateMaximizeMenuPosition(width, height), - mSurfaceControlTransactionSupplier); + mSurfaceControlTransactionSupplier, mDesktopModeUiEventLogger); mMaximizeMenu.show( /* isTaskInImmersiveMode= */ @@ -1461,6 +1473,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(); @@ -1477,8 +1491,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin shouldShowManageWindowsButton, shouldShowChangeAspectRatioButton, isDesktopModeSupportedOnDisplay(mContext, mDisplay), + shouldShowRestartButton, isBrowserApp, isBrowserApp ? getAppLink() : getBrowserLink(), + mDesktopModeUiEventLogger, mResult.mCaptionWidth, mResult.mCaptionHeight, mResult.mCaptionX, @@ -1513,6 +1529,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } return Unit.INSTANCE; }, + /* onRestartClickListener= */ mOnRestartClickListener, /* onCloseMenuClickListener= */ () -> { closeHandleMenu(); return Unit.INSTANCE; @@ -1963,6 +1980,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin MultiInstanceHelper multiInstanceHelper, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, DesktopModeEventLogger desktopModeEventLogger, + DesktopModeUiEventLogger desktopModeUiEventLogger, DesktopModeCompatPolicy desktopModeCompatPolicy) { return new DesktopModeWindowDecoration( context, @@ -1990,6 +2008,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin multiInstanceHelper, windowDecorCaptionHandleRepository, desktopModeEventLogger, + desktopModeUiEventLogger, desktopModeCompatPolicy); } } 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 284fbc310cbe..25cf06b4247c 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 @@ -28,6 +28,7 @@ import android.graphics.Bitmap import android.graphics.Point import android.graphics.PointF import android.graphics.Rect +import android.os.Bundle import android.view.LayoutInflater import android.view.MotionEvent import android.view.MotionEvent.ACTION_OUTSIDE @@ -35,6 +36,7 @@ import android.view.SurfaceControl import android.view.View import android.view.WindowInsets.Type.systemBars import android.view.WindowManager +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import android.widget.ImageButton import android.widget.ImageView import android.widget.Space @@ -49,6 +51,10 @@ import androidx.core.view.isGone import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.bubbles.ContextUtils.isRtl +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_APP_HANDLE_MENU_DESKTOP_VIEW +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_APP_HANDLE_MENU_FULLSCREEN +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_APP_HANDLE_MENU_SPLIT_SCREEN import com.android.wm.shell.shared.annotations.ShellBackgroundThread import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper @@ -94,8 +100,10 @@ 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 desktopModeUiEventLogger: DesktopModeUiEventLogger, private val captionWidth: Int, private val captionHeight: Int, captionX: Int, @@ -138,7 +146,8 @@ class HandleMenu( private val shouldShowMoreActionsPill: Boolean get() = SHOULD_SHOW_SCREENSHOT_BUTTON || shouldShowNewWindowButton || - shouldShowManageWindowsButton || shouldShowChangeAspectRatioButton + shouldShowManageWindowsButton || shouldShowChangeAspectRatioButton || + shouldShowRestartButton private var loadAppInfoJob: Job? = null @@ -156,6 +165,7 @@ class HandleMenu( onChangeAspectRatioClickListener: () -> Unit, openInAppOrBrowserClickListener: (Intent) -> Unit, onOpenByDefaultClickListener: () -> Unit, + onRestartClickListener: () -> Unit, onCloseMenuClickListener: () -> Unit, onOutsideTouchListener: () -> Unit, forceShowSystemBars: Boolean = false, @@ -175,6 +185,7 @@ class HandleMenu( onChangeAspectRatioClickListener = onChangeAspectRatioClickListener, openInAppOrBrowserClickListener = openInAppOrBrowserClickListener, onOpenByDefaultClickListener = onOpenByDefaultClickListener, + onRestartClickListener = onRestartClickListener, onCloseMenuClickListener = onCloseMenuClickListener, onOutsideTouchListener = onOutsideTouchListener, forceShowSystemBars = forceShowSystemBars, @@ -197,12 +208,14 @@ class HandleMenu( onChangeAspectRatioClickListener: () -> Unit, openInAppOrBrowserClickListener: (Intent) -> Unit, onOpenByDefaultClickListener: () -> Unit, + onRestartClickListener: () -> Unit, onCloseMenuClickListener: () -> Unit, onOutsideTouchListener: () -> Unit, forceShowSystemBars: Boolean = false, ) { val handleMenuView = HandleMenuView( context = context, + desktopModeUiEventLogger = desktopModeUiEventLogger, menuWidth = menuWidth, captionHeight = captionHeight, shouldShowWindowingPill = shouldShowWindowingPill, @@ -211,6 +224,7 @@ class HandleMenu( shouldShowManageWindowsButton = shouldShowManageWindowsButton, shouldShowChangeAspectRatioButton = shouldShowChangeAspectRatioButton, shouldShowDesktopModeButton = shouldShowDesktopModeButton, + shouldShowRestartButton = shouldShowRestartButton, isBrowserApp = isBrowserApp ).apply { bind(taskInfo, shouldShowMoreActionsPill) @@ -224,6 +238,7 @@ class HandleMenu( this.onOpenInAppOrBrowserClickListener = { openInAppOrBrowserClickListener.invoke(openInAppOrBrowserIntent!!) } + this.onRestartClickListener = onRestartClickListener this.onOpenByDefaultClickListener = onOpenByDefaultClickListener this.onCloseMenuClickListener = onCloseMenuClickListener this.onOutsideTouchListener = onOutsideTouchListener @@ -430,6 +445,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 } @@ -464,6 +483,7 @@ class HandleMenu( @SuppressLint("ClickableViewAccessibility") class HandleMenuView( private val context: Context, + private val desktopModeUiEventLogger: DesktopModeUiEventLogger, menuWidth: Int, captionHeight: Int, private val shouldShowWindowingPill: Boolean, @@ -472,6 +492,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) @@ -500,6 +521,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) @@ -543,6 +570,8 @@ 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>( @@ -568,6 +597,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 @@ -584,6 +614,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) { @@ -593,6 +624,45 @@ class HandleMenu( return@setOnTouchListener true } + desktopBtn.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + if (action == AccessibilityAction.ACTION_CLICK.id) { + desktopModeUiEventLogger.log(taskInfo, A11Y_APP_HANDLE_MENU_DESKTOP_VIEW) + } + return super.performAccessibilityAction(host, action, args) + } + } + + fullscreenBtn.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + if (action == AccessibilityAction.ACTION_CLICK.id) { + desktopModeUiEventLogger.log(taskInfo, A11Y_APP_HANDLE_MENU_FULLSCREEN) + } + return super.performAccessibilityAction(host, action, args) + } + } + + splitscreenBtn.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + if (action == AccessibilityAction.ACTION_CLICK.id) { + desktopModeUiEventLogger.log(taskInfo, A11Y_APP_HANDLE_MENU_SPLIT_SCREEN) + } + return super.performAccessibilityAction(host, action, args) + } + } + with(context) { // Update a11y announcement out to say "double tap to enter Fullscreen" ViewCompat.replaceAccessibilityAction( @@ -756,20 +826,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 ) } @@ -793,7 +859,7 @@ class HandleMenu( background = createBackgroundDrawable( color = style.textColor, cornerRadius = iconButtonRippleRadius, - drawableInsets = endInsets + drawableInsets = iconButtonDrawableInsetEnd ) } } @@ -806,6 +872,7 @@ class HandleMenu( newWindowBtn to shouldShowNewWindowButton, manageWindowBtn to shouldShowManageWindowsButton, changeAspectRatioBtn to shouldShowChangeAspectRatioButton, + restartBtn to shouldShowRestartButton, ).forEach { (button, shouldShow) -> button.apply { isGone = !shouldShow @@ -843,6 +910,10 @@ class HandleMenu( openByDefaultBtn.apply { isGone = isBrowserApp imageTintList = ColorStateList.valueOf(style.textColor) + background = createBackgroundDrawable( + color = style.textColor, + cornerRadius = iconButtonRippleRadius, + drawableInsets = iconButtonDrawableInsetEnd) } } @@ -866,6 +937,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 } } @@ -884,8 +962,10 @@ interface HandleMenuFactory { shouldShowManageWindowsButton: Boolean, shouldShowChangeAspectRatioButton: Boolean, shouldShowDesktopModeButton: Boolean, + shouldShowRestartButton: Boolean, isBrowserApp: Boolean, openInAppOrBrowserIntent: Intent?, + desktopModeUiEventLogger: DesktopModeUiEventLogger, captionWidth: Int, captionHeight: Int, captionX: Int, @@ -908,8 +988,10 @@ object DefaultHandleMenuFactory : HandleMenuFactory { shouldShowManageWindowsButton: Boolean, shouldShowChangeAspectRatioButton: Boolean, shouldShowDesktopModeButton: Boolean, + shouldShowRestartButton: Boolean, isBrowserApp: Boolean, openInAppOrBrowserIntent: Intent?, + desktopModeUiEventLogger: DesktopModeUiEventLogger, captionWidth: Int, captionHeight: Int, captionX: Int, @@ -928,8 +1010,10 @@ object DefaultHandleMenuFactory : HandleMenuFactory { shouldShowManageWindowsButton, shouldShowChangeAspectRatioButton, shouldShowDesktopModeButton, + shouldShowRestartButton, isBrowserApp, openInAppOrBrowserIntent, + desktopModeUiEventLogger, captionWidth, captionHeight, captionX, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index 5d1a7a0cc3a2..a10826fca3e2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -60,14 +60,16 @@ import android.window.TaskConstants import androidx.compose.material3.ColorScheme import androidx.compose.ui.graphics.toArgb import androidx.core.animation.addListener -import androidx.core.view.ViewCompat -import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat import androidx.core.view.isGone import androidx.core.view.isVisible import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_MAXIMIZE_MENU_MAXIMIZE +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_MAXIMIZE_MENU_RESIZE_LEFT +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_MAXIMIZE_MENU_RESIZE_RIGHT import com.android.wm.shell.desktopmode.isTaskMaximized import com.android.wm.shell.shared.animation.Interpolators.EMPHASIZED_DECELERATE import com.android.wm.shell.shared.animation.Interpolators.FAST_OUT_LINEAR_IN @@ -85,13 +87,14 @@ import java.util.function.Supplier * to the right or left half of the screen. */ class MaximizeMenu( - private val syncQueue: SyncTransactionQueue, - private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer, - private val displayController: DisplayController, - private val taskInfo: RunningTaskInfo, - private val decorWindowContext: Context, - private val positionSupplier: (Int, Int) -> Point, - private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() } + private val syncQueue: SyncTransactionQueue, + private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer, + private val displayController: DisplayController, + private val taskInfo: RunningTaskInfo, + private val decorWindowContext: Context, + private val positionSupplier: (Int, Int) -> Point, + private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() }, + private val desktopModeUiEventLogger: DesktopModeUiEventLogger ) { private var maximizeMenu: AdditionalViewHostViewContainer? = null private var maximizeMenuView: MaximizeMenuView? = null @@ -132,7 +135,7 @@ class MaximizeMenu( onLeftSnapClickListener = onLeftSnapClickListener, onRightSnapClickListener = onRightSnapClickListener, onHoverListener = onHoverListener, - onOutsideTouchListener = onOutsideTouchListener + onOutsideTouchListener = onOutsideTouchListener, ) maximizeMenuView?.let { view -> view.animateOpenMenu(onEnd = { @@ -167,7 +170,7 @@ class MaximizeMenu( onLeftSnapClickListener: () -> Unit, onRightSnapClickListener: () -> Unit, onHoverListener: (Boolean) -> Unit, - onOutsideTouchListener: () -> Unit + onOutsideTouchListener: () -> Unit, ) { val t = transactionSupplier.get() val builder = SurfaceControl.Builder() @@ -186,6 +189,7 @@ class MaximizeMenu( "MaximizeMenu") maximizeMenuView = MaximizeMenuView( context = decorWindowContext, + desktopModeUiEventLogger = desktopModeUiEventLogger, sizeToggleDirection = getSizeToggleDirection(), immersiveConfig = if (showImmersiveOption) { MaximizeMenuView.ImmersiveConfig.Visible( @@ -265,6 +269,7 @@ class MaximizeMenu( */ class MaximizeMenuView( context: Context, + private val desktopModeUiEventLogger: DesktopModeUiEventLogger, private val sizeToggleDirection: SizeToggleDirection, immersiveConfig: ImmersiveConfig, showSnapOptions: Boolean, @@ -425,7 +430,10 @@ class MaximizeMenu( ) { super.onInitializeAccessibilityNodeInfo(host, info) - info.addAction(AccessibilityAction.ACTION_CLICK) + info.addAction(AccessibilityAction( + AccessibilityAction.ACTION_CLICK.id, + context.getString(R.string.maximize_menu_talkback_action_maximize_restore_text) + )) host.isClickable = true } @@ -435,6 +443,7 @@ class MaximizeMenu( args: Bundle? ): Boolean { if (action == AccessibilityAction.ACTION_CLICK.id) { + desktopModeUiEventLogger.log(taskInfo, A11Y_MAXIMIZE_MENU_MAXIMIZE) onMaximizeClickListener?.invoke() } return super.performAccessibilityAction(host, action, args) @@ -447,7 +456,10 @@ class MaximizeMenu( info: AccessibilityNodeInfo ) { super.onInitializeAccessibilityNodeInfo(host, info) - info.addAction(AccessibilityAction.ACTION_CLICK) + info.addAction(AccessibilityAction( + AccessibilityAction.ACTION_CLICK.id, + context.getString(R.string.maximize_menu_talkback_action_snap_left_text) + )) host.isClickable = true } @@ -457,6 +469,7 @@ class MaximizeMenu( args: Bundle? ): Boolean { if (action == AccessibilityAction.ACTION_CLICK.id) { + desktopModeUiEventLogger.log(taskInfo, A11Y_MAXIMIZE_MENU_RESIZE_LEFT) onLeftSnapClickListener?.invoke() } return super.performAccessibilityAction(host, action, args) @@ -469,7 +482,10 @@ class MaximizeMenu( info: AccessibilityNodeInfo ) { super.onInitializeAccessibilityNodeInfo(host, info) - info.addAction(AccessibilityAction.ACTION_CLICK) + info.addAction(AccessibilityAction( + AccessibilityAction.ACTION_CLICK.id, + context.getString(R.string.maximize_menu_talkback_action_snap_right_text) + )) host.isClickable = true } @@ -479,35 +495,13 @@ class MaximizeMenu( args: Bundle? ): Boolean { if (action == AccessibilityAction.ACTION_CLICK.id) { + desktopModeUiEventLogger.log(taskInfo, A11Y_MAXIMIZE_MENU_RESIZE_RIGHT) onRightSnapClickListener?.invoke() } return super.performAccessibilityAction(host, action, args) } } - with(context.resources) { - ViewCompat.replaceAccessibilityAction( - snapLeftButton, - AccessibilityActionCompat.ACTION_CLICK, - getString(R.string.maximize_menu_talkback_action_snap_left_text), - null - ) - - ViewCompat.replaceAccessibilityAction( - snapRightButton, - AccessibilityActionCompat.ACTION_CLICK, - getString(R.string.maximize_menu_talkback_action_snap_right_text), - null - ) - - ViewCompat.replaceAccessibilityAction( - sizeToggleButton, - AccessibilityActionCompat.ACTION_CLICK, - getString(R.string.maximize_menu_talkback_action_maximize_restore_text), - null - ) - } - // Maximize/restore button. val sizeToggleBtnTextId = if (sizeToggleDirection == SizeToggleDirection.RESTORE) R.string.desktop_mode_maximize_menu_restore_button_text @@ -792,15 +786,15 @@ class MaximizeMenu( } /** Measure width of the root view of this menu. */ - fun measureWidth() : Int { - rootView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); - return rootView.getMeasuredWidth() + fun measureWidth(): Int { + rootView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + return rootView.measuredWidth } /** Measure height of the root view of this menu. */ - fun measureHeight() : Int { - rootView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); - return rootView.getMeasuredHeight() + fun measureHeight(): Int { + rootView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + return rootView.measuredHeight } private fun deactivateSnapOptions() { @@ -1048,7 +1042,8 @@ interface MaximizeMenuFactory { taskInfo: RunningTaskInfo, decorWindowContext: Context, positionSupplier: (Int, Int) -> Point, - transactionSupplier: Supplier<Transaction> + transactionSupplier: Supplier<Transaction>, + desktopModeUiEventLogger: DesktopModeUiEventLogger, ): MaximizeMenu } @@ -1061,7 +1056,8 @@ object DefaultMaximizeMenuFactory : MaximizeMenuFactory { taskInfo: RunningTaskInfo, decorWindowContext: Context, positionSupplier: (Int, Int) -> Point, - transactionSupplier: Supplier<Transaction> + transactionSupplier: Supplier<Transaction>, + desktopModeUiEventLogger: DesktopModeUiEventLogger, ): MaximizeMenu { return MaximizeMenu( syncQueue, @@ -1070,7 +1066,8 @@ object DefaultMaximizeMenuFactory : MaximizeMenuFactory { taskInfo, decorWindowContext, positionSupplier, - transactionSupplier + transactionSupplier, + desktopModeUiEventLogger ) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt index c5057aa3cc18..f09f22fb1951 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt @@ -92,4 +92,7 @@ internal class DecorThemeUtil(private val context: Context) { Theme.LIGHT -> lightColors Theme.DARK -> darkColors } + + fun getColorScheme(isDarkMode: Boolean): ColorScheme = + if (isDarkMode) darkColors else lightColors } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt index 57f8046065b4..ab6b72947586 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt @@ -198,6 +198,11 @@ class DesktopTilingDividerWindowManager( tilingDividerView?.onUiModeChange(isDarkMode) } + /** Notifies the divider view of task info change and possible color change. */ + fun onTaskInfoChange() { + tilingDividerView?.onTaskInfoChange() + } + /** Hides the divider bar. */ fun hideDividerBar() { if (!dividerShown) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt index 854d9e1deef1..3e92d7d994a6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt @@ -375,6 +375,7 @@ class DesktopTilingWindowDecoration( fun onTaskInfoChange(taskInfo: RunningTaskInfo) { val isCurrentTaskInDarkMode = isTaskInDarkMode(taskInfo) + desktopTilingDividerWindowManager?.onTaskInfoChange() if (isCurrentTaskInDarkMode == isDarkMode || !isTilingManagerInitialised) return isDarkMode = isCurrentTaskInDarkMode desktopTilingDividerWindowManager?.onUiModeChange(isDarkMode) @@ -432,15 +433,38 @@ class DesktopTilingWindowDecoration( startTransaction: Transaction, finishTransaction: Transaction, ) { + var leftTaskBroughtToFront = false + var rightTaskBroughtToFront = false + for (change in info.changes) { change.taskInfo?.let { if (it.isFullscreen || isMinimized(change.mode, info.type)) { removeTaskIfTiled(it.taskId, /* taskVanished= */ false, it.isFullscreen) } else if (isEnteringPip(change, info.type)) { removeTaskIfTiled(it.taskId, /* taskVanished= */ true, it.isFullscreen) + } else if (isTransitionToFront(change.mode, info.type)) { + handleTaskBroughtToFront(it.taskId) + leftTaskBroughtToFront = + leftTaskBroughtToFront || + it.taskId == leftTaskResizingHelper?.taskInfo?.taskId + rightTaskBroughtToFront = + rightTaskBroughtToFront || + it.taskId == rightTaskResizingHelper?.taskInfo?.taskId } } } + + if (leftTaskBroughtToFront && rightTaskBroughtToFront) { + desktopTilingDividerWindowManager?.showDividerBar() + } + } + + private fun handleTaskBroughtToFront(taskId: Int) { + if (taskId == leftTaskResizingHelper?.taskInfo?.taskId) { + leftTaskResizingHelper?.onAppBecomingVisible() + } else if (taskId == rightTaskResizingHelper?.taskInfo?.taskId) { + rightTaskResizingHelper?.onAppBecomingVisible() + } } private fun isMinimized(changeMode: Int, infoType: Int): Boolean { @@ -471,6 +495,9 @@ class DesktopTilingWindowDecoration( return false } + private fun isTransitionToFront(changeMode: Int, transitionType: Int): Boolean = + changeMode == TRANSIT_TO_FRONT && transitionType == TRANSIT_TO_FRONT + class AppResizingHelper( val taskInfo: RunningTaskInfo, val desktopModeWindowDecoration: DesktopModeWindowDecoration, @@ -484,6 +511,7 @@ class DesktopTilingWindowDecoration( ) { var isInitialised = false var newBounds = Rect(bounds) + var visibilityCallback: (() -> Unit)? = null private lateinit var resizeVeil: ResizeVeil private val displayContext = displayController.getDisplayContext(taskInfo.displayId) private val userContext = @@ -521,6 +549,11 @@ class DesktopTilingWindowDecoration( fun updateVeil(t: Transaction) = resizeVeil.updateTransactionWithResizeVeil(t, newBounds) + fun onAppBecomingVisible() { + visibilityCallback?.invoke() + visibilityCallback = null + } + fun hideVeil() = resizeVeil.hideVeil() private fun createIconFactory(context: Context, dimensions: Int): BaseIconFactory { @@ -593,11 +626,16 @@ class DesktopTilingWindowDecoration( removeTask(leftTaskResizingHelper, taskVanished, shouldDelayUpdate) leftTaskResizingHelper = null val taskId = rightTaskResizingHelper?.taskInfo?.taskId - if (taskId != null && taskRepository.isVisibleTask(taskId)) { + val callback: (() -> Unit)? = { rightTaskResizingHelper ?.desktopModeWindowDecoration ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate) } + if (taskId != null && taskRepository.isVisibleTask(taskId)) { + callback?.invoke() + } else if (rightTaskResizingHelper != null) { + rightTaskResizingHelper?.visibilityCallback = callback + } tearDownTiling() return } @@ -607,11 +645,17 @@ class DesktopTilingWindowDecoration( removeTask(rightTaskResizingHelper, taskVanished, shouldDelayUpdate) rightTaskResizingHelper = null val taskId = leftTaskResizingHelper?.taskInfo?.taskId - if (taskId != null && taskRepository.isVisibleTask(taskId)) { + val callback: (() -> Unit)? = { leftTaskResizingHelper ?.desktopModeWindowDecoration ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate) } + if (taskId != null && taskRepository.isVisibleTask(taskId)) { + callback?.invoke() + } else if (leftTaskResizingHelper != null) { + leftTaskResizingHelper?.visibilityCallback = callback + } + tearDownTiling() } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt index 54dcd2d082dc..e9bd6f8ef85c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt @@ -29,6 +29,7 @@ import android.view.RoundedCorner import android.view.View import android.view.ViewConfiguration import android.widget.FrameLayout +import androidx.compose.ui.graphics.toArgb import com.android.internal.annotations.VisibleForTesting import com.android.internal.config.sysui.SystemUiDeviceConfigFlags import com.android.wm.shell.R @@ -36,6 +37,7 @@ import com.android.wm.shell.common.split.DividerHandleView import com.android.wm.shell.common.split.DividerRoundedCorner import com.android.wm.shell.shared.animation.Interpolators import com.android.wm.shell.windowdecor.DragDetector +import com.android.wm.shell.windowdecor.common.DecorThemeUtil /** Divider for tiling split screen, currently mostly a copy of [DividerView]. */ class TilingDividerView : FrameLayout, View.OnTouchListener, DragDetector.MotionEventHandler { @@ -56,6 +58,9 @@ class TilingDividerView : FrameLayout, View.OnTouchListener, DragDetector.Motion @VisibleForTesting var handleY: IntRange = 0..0 private var canResize = false private var resized = false + private var isDarkMode = false + private var decorThemeUtil = DecorThemeUtil(context) + /** * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with * insets. @@ -90,10 +95,12 @@ class TilingDividerView : FrameLayout, View.OnTouchListener, DragDetector.Motion ) { callback = dividerMoveCallback this.dividerBounds.set(dividerBounds) + this.isDarkMode = isDarkMode + paint.color = decorThemeUtil.getColorScheme(isDarkMode).outlineVariant.toArgb() handle.setIsLeftRightSplit(true) handle.setup(/* isSplitScreen= */ false, isDarkMode) corners.setIsLeftRightSplit(true) - corners.setup(/* isSplitScreen= */ false, isDarkMode) + corners.setup(/* isSplitScreen= */ false, paint.color) handleRegionHeight = handleRegionSize.height handleRegionWidth = handleRegionSize.width cornersRadius = @@ -108,17 +115,22 @@ class TilingDividerView : FrameLayout, View.OnTouchListener, DragDetector.Motion } fun onUiModeChange(isDarkMode: Boolean) { + this.isDarkMode = isDarkMode handle.onUiModeChange(isDarkMode) - corners.onUiModeChange(isDarkMode) - paint.color = - if (isDarkMode) { - resources.getColor(R.color.tiling_divider_background_dark, null /* theme */) - } else { - resources.getColor(R.color.tiling_divider_background_light, null /* theme */) - } + paint.color = decorThemeUtil.getColorScheme(isDarkMode).outlineVariant.toArgb() + corners.onUiModeChange(paint.color) invalidate() } + fun onTaskInfoChange() { + decorThemeUtil = DecorThemeUtil(context) + if (paint.color != decorThemeUtil.getColorScheme(isDarkMode).outlineVariant.toArgb()) { + paint.color = decorThemeUtil.getColorScheme(isDarkMode).outlineVariant.toArgb() + corners.onCornerColorChange(paint.color) + invalidate() + } + } + override fun onFinishInflate() { super.onFinishInflate() dividerBar = requireViewById(R.id.divider_bar) @@ -128,15 +140,10 @@ class TilingDividerView : FrameLayout, View.OnTouchListener, DragDetector.Motion resources.getDimensionPixelSize(R.dimen.docked_stack_divider_lift_elevation) setOnTouchListener(this) setWillNotDraw(false) - paint.color = - if ( - context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == - Configuration.UI_MODE_NIGHT_YES - ) { - resources.getColor(R.color.tiling_divider_background_dark, /* theme= */null) - } else { - resources.getColor(R.color.tiling_divider_background_light, /* theme= */ null) - } + val isDarkMode = + context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == + Configuration.UI_MODE_NIGHT_YES + paint.color = decorThemeUtil.getColorScheme(isDarkMode).outlineVariant.toArgb() paint.isAntiAlias = true paint.style = Paint.Style.FILL } 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 9d16be59ba34..f09c500dcdd0 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 @@ -39,6 +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.desktopmode.DesktopModeUiEventLogger +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_APP_HANDLE_MENU_OPENED import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper import com.android.wm.shell.windowdecor.AppHandleAnimator import com.android.wm.shell.windowdecor.WindowManagerWrapper @@ -53,7 +55,8 @@ class AppHandleViewHolder( onCaptionTouchListener: View.OnTouchListener, onCaptionButtonClickListener: OnClickListener, private val windowManagerWrapper: WindowManagerWrapper, - private val handler: Handler + private val handler: Handler, + private val desktopModeUiEventLogger: DesktopModeUiEventLogger, ) : WindowDecorationViewHolder<AppHandleViewHolder.HandleData>(rootView) { data class HandleData( @@ -201,6 +204,7 @@ class AppHandleViewHolder( // Passthrough the a11y click action so the caption handle, so that app handle menu // is opened on a11y click, similar to a real click if (action == AccessibilityAction.ACTION_CLICK.id) { + desktopModeUiEventLogger.log(taskInfo, A11Y_APP_HANDLE_MENU_OPENED) captionHandle.performClick() } return super.performAccessibilityAction(host, action, args) @@ -216,10 +220,13 @@ class AppHandleViewHolder( } } - // Update a11y action text so that Talkback announces "Press double tap to open app handle - // menu" while focused on status bar input layer + // Update a11y action text so that Talkback announces "Press double tap to open menu" + // while focused on status bar input layer ViewCompat.replaceAccessibilityAction( - view, AccessibilityActionCompat.ACTION_CLICK, "Open app handle menu", null + view, + AccessibilityActionCompat.ACTION_CLICK, + context.getString(R.string.app_handle_chip_accessibility_announce), + null ) } @@ -297,12 +304,14 @@ class AppHandleViewHolder( onCaptionButtonClickListener: OnClickListener, windowManagerWrapper: WindowManagerWrapper, handler: Handler, + desktopModeUiEventLogger: DesktopModeUiEventLogger, ): AppHandleViewHolder = AppHandleViewHolder( rootView, onCaptionTouchListener, onCaptionButtonClickListener, windowManagerWrapper, handler, + desktopModeUiEventLogger, ) } } 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 0e2698d0b6fa..d2e3a07f4651 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 @@ -49,6 +49,13 @@ import com.android.internal.R.color.materialColorSurfaceContainerHigh import com.android.internal.R.color.materialColorSurfaceContainerLow import com.android.internal.R.color.materialColorSurfaceDim import com.android.wm.shell.R +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_ACTION_MAXIMIZE_RESTORE +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_ACTION_RESIZE_LEFT +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_ACTION_RESIZE_RIGHT +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_APP_WINDOW_CLOSE_BUTTON +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_APP_WINDOW_MAXIMIZE_RESTORE_BUTTON +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_APP_WINDOW_MINIMIZE_BUTTON import com.android.wm.shell.windowdecor.MaximizeButtonView import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.common.DrawableInsets @@ -75,6 +82,7 @@ class AppHeaderViewHolder( mOnRightSnapClickListener: () -> Unit, mOnMaximizeOrRestoreClickListener: () -> Unit, onMaximizeHoverAnimationFinishedListener: () -> Unit, + private val desktopModeUiEventLogger: DesktopModeUiEventLogger, ) : WindowDecorationViewHolder<AppHeaderViewHolder.HeaderData>(rootView) { data class HeaderData( @@ -151,6 +159,8 @@ class AppHeaderViewHolder( private lateinit var a11yTextMaximize: String private lateinit var a11yTextRestore: String + private lateinit var currentTaskInfo: RunningTaskInfo + init { captionView.setOnTouchListener(onCaptionTouchListener) captionHandle.setOnTouchListener(onCaptionTouchListener) @@ -197,9 +207,18 @@ class AppHeaderViewHolder( args: Bundle? ): Boolean { when (action) { - R.id.action_snap_left -> mOnLeftSnapClickListener.invoke() - R.id.action_snap_right -> mOnRightSnapClickListener.invoke() - R.id.action_maximize_restore -> mOnMaximizeOrRestoreClickListener.invoke() + R.id.action_snap_left -> { + desktopModeUiEventLogger.log(currentTaskInfo, A11Y_ACTION_RESIZE_LEFT) + mOnLeftSnapClickListener.invoke() + } + R.id.action_snap_right -> { + desktopModeUiEventLogger.log(currentTaskInfo, A11Y_ACTION_RESIZE_RIGHT) + mOnRightSnapClickListener.invoke() + } + R.id.action_maximize_restore -> { + desktopModeUiEventLogger.log(currentTaskInfo, A11Y_ACTION_MAXIMIZE_RESTORE) + mOnMaximizeOrRestoreClickListener.invoke() + } } return super.performAccessibilityAction(host, action, args) @@ -224,10 +243,56 @@ class AppHeaderViewHolder( args: Bundle? ): Boolean { when (action) { - AccessibilityAction.ACTION_CLICK.id -> host.performClick() - R.id.action_snap_left -> mOnLeftSnapClickListener.invoke() - R.id.action_snap_right -> mOnRightSnapClickListener.invoke() - R.id.action_maximize_restore -> mOnMaximizeOrRestoreClickListener.invoke() + AccessibilityAction.ACTION_CLICK.id -> { + desktopModeUiEventLogger.log( + currentTaskInfo, A11Y_APP_WINDOW_MAXIMIZE_RESTORE_BUTTON + ) + host.performClick() + } + R.id.action_snap_left -> { + desktopModeUiEventLogger.log(currentTaskInfo, A11Y_ACTION_RESIZE_LEFT) + mOnLeftSnapClickListener.invoke() + } + R.id.action_snap_right -> { + desktopModeUiEventLogger.log(currentTaskInfo, A11Y_ACTION_RESIZE_RIGHT) + mOnRightSnapClickListener.invoke() + } + R.id.action_maximize_restore -> { + desktopModeUiEventLogger.log(currentTaskInfo, A11Y_ACTION_MAXIMIZE_RESTORE) + mOnMaximizeOrRestoreClickListener.invoke() + } + } + + return super.performAccessibilityAction(host, action, args) + } + } + + closeWindowButton.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + when (action) { + AccessibilityAction.ACTION_CLICK.id -> desktopModeUiEventLogger.log( + currentTaskInfo, A11Y_APP_WINDOW_CLOSE_BUTTON + ) + } + + return super.performAccessibilityAction(host, action, args) + } + } + + minimizeWindowButton.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + when (action) { + AccessibilityAction.ACTION_CLICK.id -> desktopModeUiEventLogger.log( + currentTaskInfo, A11Y_APP_WINDOW_MINIMIZE_BUTTON + ) } return super.performAccessibilityAction(host, action, args) @@ -310,7 +375,8 @@ class AppHeaderViewHolder( enableMaximizeLongClick: Boolean, isCaptionVisible: Boolean, ) { - if (DesktopModeFlags.ENABLE_THEMED_APP_HEADERS.isTrue()) { + currentTaskInfo = taskInfo + if (DesktopModeFlags.ENABLE_THEMED_APP_HEADERS.isTrue) { bindDataWithThemedHeaders( taskInfo, isTaskMaximized, @@ -361,7 +427,7 @@ class AppHeaderViewHolder( minimizeWindowButton.background = getDrawable(1) } maximizeButtonView.setAnimationTints(isDarkMode()) - minimizeWindowButton.isGone = !DesktopModeFlags.ENABLE_MINIMIZE_BUTTON.isTrue() + minimizeWindowButton.isGone = !DesktopModeFlags.ENABLE_MINIMIZE_BUTTON.isTrue } private fun bindDataWithThemedHeaders( @@ -417,7 +483,7 @@ class AppHeaderViewHolder( drawableInsets = minimizeDrawableInsets ) } - minimizeWindowButton.isGone = !DesktopModeFlags.ENABLE_MINIMIZE_BUTTON.isTrue() + minimizeWindowButton.isGone = !DesktopModeFlags.ENABLE_MINIMIZE_BUTTON.isTrue // Maximize button. maximizeButtonView.apply { setAnimationTints( @@ -761,6 +827,7 @@ class AppHeaderViewHolder( mOnRightSnapClickListener: () -> Unit, mOnMaximizeOrRestoreClickListener: () -> Unit, onMaximizeHoverAnimationFinishedListener: () -> Unit, + desktopModeUiEventLogger: DesktopModeUiEventLogger ): AppHeaderViewHolder = AppHeaderViewHolder( rootView, onCaptionTouchListener, @@ -771,6 +838,7 @@ class AppHeaderViewHolder( mOnRightSnapClickListener, mOnMaximizeOrRestoreClickListener, onMaximizeHoverAnimationFinishedListener, + desktopModeUiEventLogger, ) } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java index 3d5e9495e29d..1e459d55ed77 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java @@ -18,6 +18,7 @@ package com.android.wm.shell.common; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -231,4 +232,24 @@ public class DisplayControllerTests extends ShellTestCase { assertEquals(DISPLAY_ABS_BOUNDS_1, mController.getDisplayLayout(DISPLAY_ID_1).globalBoundsDp()); } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG) + public void onDisplayConfigurationChanged_reInitDisplayLayout() + throws RemoteException { + ExtendedMockito.doReturn(true) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + mController.onInit(); + mController.addDisplayWindowListener(mListener); + + mCapturedTopologyListener.accept(mMockTopology); + + DisplayLayout displayLayoutBefore = mController.getDisplayLayout(DISPLAY_ID_0); + mDisplayContainerListener.onDisplayConfigurationChanged(DISPLAY_ID_0, new Configuration()); + DisplayLayout displayLayoutAfter = mController.getDisplayLayout(DISPLAY_ID_0); + + assertNotSame(displayLayoutBefore, displayLayoutAfter); + assertEquals(DISPLAY_ABS_BOUNDS_0, + mController.getDisplayLayout(DISPLAY_ID_0).globalBoundsDp()); + } } 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/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index 3fb008377d6e..6ede990df15e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -1280,6 +1280,18 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { @Test @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeDesk_multipleDesks_active_marksInactiveInDisplay() { + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 2) + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 3) + repo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 3) + + repo.removeDesk(deskId = 3) + + assertThat(repo.getActiveDeskId(displayId = DEFAULT_DISPLAY)).isNull() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun removeDesk_multipleDesks_inactive_removes() { repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 2) repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 3) @@ -1292,6 +1304,18 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeDesk_multipleDesks_inactive_keepsOtherDeskActiveInDisplay() { + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 2) + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 3) + repo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 3) + + repo.removeDesk(deskId = 2) + + assertThat(repo.getActiveDeskId(displayId = DEFAULT_DISPLAY)).isEqualTo(3) + } + + @Test @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) fun removeDesk_removesFromPersistence() = runTest(StandardTestDispatcher()) { 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/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..2126d1d9b986 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) @@ -334,7 +337,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { mockDesktopModeWindowDecorFactory.create( any(), any(), any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), - any(), any(), any(), any()) + any(), any(), any(), any(), any()) ).thenReturn(decoration) decoration.mTaskInfo = task whenever(decoration.user).thenReturn(mockUserHandle) 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..0908f56e1cfb 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 @@ -106,6 +106,7 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.desktopmode.CaptionState; import com.android.wm.shell.desktopmode.DesktopModeEventLogger; +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger; import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; @@ -246,6 +247,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Mock private DesktopModeEventLogger mDesktopModeEventLogger; @Mock + private DesktopModeUiEventLogger mDesktopModeUiEventLogger; + @Mock private DesktopRepository mDesktopRepository; @Mock private WindowDecorTaskResourceLoader mMockTaskResourceLoader; @@ -303,15 +306,15 @@ 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(), any(), anyInt(), anyInt(), anyInt(), anyInt())) .thenReturn(mMockHandleMenu); when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any(), anyInt())) .thenReturn(false); when(mMockAppHeaderViewHolderFactory - .create(any(), any(), any(), any(), any(), any(), any(), any(), any())) + .create(any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) .thenReturn(mMockAppHeaderViewHolder); when(mMockAppHandleViewHolderFactory - .create(any(), any(), any(), any(), any())) + .create(any(), any(), any(), any(), any(), any())) .thenReturn(mMockAppHandleViewHolder); when(mMockDesktopUserRepositories.getCurrent()).thenReturn(mDesktopRepository); when(mMockDesktopUserRepositories.getProfile(anyInt())).thenReturn(mDesktopRepository); @@ -1450,6 +1453,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), anyBoolean() ); // Run runnable to set captured link to used @@ -1487,6 +1491,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), anyBoolean() ); openInBrowserCaptor.getValue().invoke(new Intent(Intent.ACTION_MAIN, TEST_URI1)); @@ -1519,6 +1524,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), anyBoolean() ); @@ -1584,6 +1590,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), closeClickListener.capture(), any(), anyBoolean() @@ -1617,6 +1624,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), /* forceShowSystemBars= */ eq(true) ); } @@ -1790,9 +1798,9 @@ 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()); + any(), anyInt(), anyInt(), anyInt(), anyInt()); } private void createMaximizeMenu(DesktopModeWindowDecoration decoration) { @@ -1889,7 +1897,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { new WindowManagerWrapper(mMockWindowManager), mMockSurfaceControlViewHostFactory, mMockWindowDecorViewHostSupplier, maximizeMenuFactory, mMockHandleMenuFactory, mMockMultiInstanceHelper, mMockCaptionHandleRepository, mDesktopModeEventLogger, - mDesktopModeCompatPolicy); + mDesktopModeUiEventLogger, mDesktopModeCompatPolicy); windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener); windowDecor.setExclusionRegionListener(mMockExclusionRegionListener); @@ -1916,6 +1924,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", @@ -2014,7 +2023,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @NonNull Context decorWindowContext, @NonNull Function2<? super Integer,? super Integer,? extends Point> positionSupplier, - @NonNull Supplier<SurfaceControl.Transaction> transactionSupplier) { + @NonNull Supplier<SurfaceControl.Transaction> transactionSupplier, + @NonNull DesktopModeUiEventLogger desktopModeUiEventLogger) { return mMaximizeMenu; } } 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..b0b95c97ae18 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 @@ -43,6 +43,7 @@ import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED @@ -95,6 +96,8 @@ class HandleMenuTest : ShellTestCase() { private lateinit var mockTaskResourceLoader: WindowDecorTaskResourceLoader @Mock private lateinit var mockAppIcon: Bitmap + @Mock + private lateinit var mockDesktopModeUiEventLogger: DesktopModeUiEventLogger private lateinit var handleMenu: HandleMenu @@ -287,8 +290,10 @@ class HandleMenuTest : ShellTestCase() { shouldShowManageWindowsButton = false, shouldShowChangeAspectRatioButton = false, shouldShowDesktopModeButton = true, + shouldShowRestartButton = true, isBrowserApp = false, null /* openInAppOrBrowserIntent */, + mockDesktopModeUiEventLogger, captionWidth = HANDLE_WIDTH, captionHeight = 50, captionX = captionX, @@ -304,6 +309,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/tiling/DesktopTilingWindowDecorationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt index e5f8d7d34a47..0adca5e864bf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt @@ -587,6 +587,31 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { } @Test + fun tilingDivider_shouldBeShown_whenTiledTasksBecomeVisible() { + val task1 = createVisibleTask() + val task2 = createVisibleTask() + val additionalTaskHelper: DesktopTilingWindowDecoration.AppResizingHelper = mock() + whenever(tiledTaskHelper.taskInfo).thenReturn(task1) + whenever(tiledTaskHelper.desktopModeWindowDecoration).thenReturn(desktopWindowDecoration) + whenever(additionalTaskHelper.taskInfo).thenReturn(task2) + whenever(additionalTaskHelper.desktopModeWindowDecoration) + .thenReturn(desktopWindowDecoration) + + tilingDecoration.leftTaskResizingHelper = tiledTaskHelper + tilingDecoration.rightTaskResizingHelper = additionalTaskHelper + tilingDecoration.desktopTilingDividerWindowManager = desktopTilingDividerWindowManager + val changeInfo = createTransitFrontTransition(task1, task2) + tilingDecoration.onTransitionReady( + transition = mock(), + info = changeInfo, + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(desktopTilingDividerWindowManager, times(1)).showDividerBar() + } + + @Test fun taskNotTiled_shouldNotBeRemoved_whenNotTiled() { val task1 = createVisibleTask() val task2 = createVisibleTask() @@ -762,6 +787,30 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { ) } + private fun createTransitFrontTransition( + task1: RunningTaskInfo?, + task2: RunningTaskInfo?, + type: Int = TRANSIT_TO_FRONT, + ) = + TransitionInfo(type, /* flags= */ 0).apply { + addChange( + Change(mock(), mock()).apply { + mode = TRANSIT_TO_FRONT + parent = null + taskInfo = task1 + flags = flags + } + ) + addChange( + Change(mock(), mock()).apply { + mode = TRANSIT_TO_FRONT + parent = null + taskInfo = task2 + flags = flags + } + ) + } + companion object { private val NON_STABLE_BOUNDS_MOCK = Rect(50, 55, 100, 100) private val STABLE_BOUNDS_MOCK = Rect(0, 0, 100, 100) 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 2c3009cb8dc4..e23019a59307 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 @@ -26,6 +26,7 @@ import androidx.test.filters.SmallTest import com.android.internal.policy.SystemBarUtils import com.android.wm.shell.R import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger import com.android.wm.shell.windowdecor.WindowManagerWrapper import org.junit.Before import org.junit.runner.RunWith @@ -57,6 +58,7 @@ class AppHandleViewHolderTest : ShellTestCase() { private val mockWindowManagerWrapper = mock<WindowManagerWrapper>() private val mockHandler = mock<Handler>() private val mockTaskInfo = mock<RunningTaskInfo>() + private val mockDesktopModeUiEventLogger = mock<DesktopModeUiEventLogger>() @Before fun setup() { @@ -92,7 +94,8 @@ class AppHandleViewHolderTest : ShellTestCase() { mockOnTouchListener, mockOnClickListener, mockWindowManagerWrapper, - mockHandler + mockHandler, + mockDesktopModeUiEventLogger, ) } } 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/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/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp index 104ece6582f5..b6a2ad7064a9 100644 --- a/libs/hwui/jni/Bitmap.cpp +++ b/libs/hwui/jni/Bitmap.cpp @@ -28,7 +28,9 @@ #include "SkRefCnt.h" #include "SkStream.h" #include "SkTypes.h" +#ifdef __linux__ // Only Linux support parcel #include "android/binder_parcel.h" +#endif #include "android_nio_utils.h" #ifdef __ANDROID__ @@ -851,6 +853,7 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) { #endif } +#ifdef __linux__ // Only Linux support parcel // Returns whether this bitmap should be written to the parcel as mutable. static bool shouldParcelAsMutable(SkBitmap& bitmap, AParcel* parcel) { // If the bitmap is immutable, then parcel as immutable. @@ -867,6 +870,7 @@ static bool shouldParcelAsMutable(SkBitmap& bitmap, AParcel* parcel) { // writing it to the parcel. return !shouldUseAshmem(parcel, bitmap.computeByteSize()); } +#endif static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, jlong bitmapHandle, jint density, jobject parcel) { diff --git a/libs/hwui/jni/GIFMovie.cpp b/libs/hwui/jni/GIFMovie.cpp index 476b6fda5007..6c82aa1ca27d 100644 --- a/libs/hwui/jni/GIFMovie.cpp +++ b/libs/hwui/jni/GIFMovie.cpp @@ -63,7 +63,7 @@ GIFMovie::GIFMovie(SkStream* stream) } fCurrIndex = -1; fLastDrawIndex = -1; - fPaintingColor = SK_AlphaTRANSPARENT; + fPaintingColor = SkPackARGB32(0, 0, 0, 0); } GIFMovie::~GIFMovie() @@ -127,7 +127,7 @@ static void copyLine(uint32_t* dst, const unsigned char* src, const ColorMapObje for (; width > 0; width--, src++, dst++) { if (*src != transparent && *src < cmap->ColorCount) { const GifColorType& col = cmap->Colors[*src]; - *dst = SkColorSetRGB(col.Red, col.Green, col.Blue); + *dst = SkPackARGB32(0xFF, col.Red, col.Green, col.Blue); } } } @@ -395,10 +395,10 @@ bool GIFMovie::onGetBitmap(SkBitmap* bm) lastIndex = fGIF->ImageCount - 1; } - SkColor bgColor = SK_ColorTRANSPARENT; + SkColor bgColor = SkPackARGB32(0, 0, 0, 0); if (gif->SColorMap != nullptr && gif->SBackGroundColor < gif->SColorMap->ColorCount) { const GifColorType& col = gif->SColorMap->Colors[gif->SBackGroundColor]; - bgColor = SkColorSetRGB(col.Red, col.Green, col.Blue); + bgColor = SkColorSetARGB(0xFF, col.Red, col.Green, col.Blue); } // draw each frames - not intelligent way @@ -411,7 +411,7 @@ bool GIFMovie::onGetBitmap(SkBitmap* bm) if (!trans && gif->SColorMap != nullptr) { fPaintingColor = bgColor; } else { - fPaintingColor = SK_ColorTRANSPARENT; + fPaintingColor = SkColorSetARGB(0, 0, 0, 0); } bm->eraseColor(fPaintingColor); 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..e5a6260cfd98 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -44,7 +44,7 @@ namespace uirenderer { namespace renderthread { // Not all of these are strictly required, but are all enabled if present. -static std::array<std::string_view, 23> sEnableExtensions{ +static std::array<std::string_view, 25> sEnableExtensions{ VK_KHR_BIND_MEMORY_2_EXTENSION_NAME, VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME, VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME, @@ -68,6 +68,8 @@ static std::array<std::string_view, 23> sEnableExtensions{ VK_EXT_GLOBAL_PRIORITY_QUERY_EXTENSION_NAME, VK_KHR_GLOBAL_PRIORITY_EXTENSION_NAME, VK_EXT_DEVICE_FAULT_EXTENSION_NAME, + VK_EXT_FRAME_BOUNDARY_EXTENSION_NAME, + VK_ANDROID_FRAME_BOUNDARY_EXTENSION_NAME, }; static bool shouldEnableExtension(const std::string_view& extension) { @@ -238,6 +240,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()); @@ -745,7 +748,14 @@ VulkanManager::VkDrawResult VulkanManager::finishFrame(SkSurface* surface) { ALOGE_IF(!context, "Surface is not backed by gpu"); GrSemaphoresSubmitted submitted = context->flush( surface, SkSurfaces::BackendSurfaceAccess::kPresent, flushInfo); - context->submit(); + + static uint64_t currentFrameID = 0; + GrSubmitInfo submitInfo; + if (!mFrameBoundaryANDROID) { + submitInfo.fMarkBoundary = GrMarkFrameBoundary::kYes; + submitInfo.fFrameID = currentFrameID++; + } + context->submit(submitInfo); VkDrawResult drawResult{ .submissionTime = systemTime(), }; diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h index a593ec6f8351..744211e39f79 100644 --- a/libs/hwui/renderthread/VulkanManager.h +++ b/libs/hwui/renderthread/VulkanManager.h @@ -30,14 +30,10 @@ // VK_ANDROID_frame_boundary is a bespoke extension defined by AGI // (https://github.com/google/agi) to enable profiling of apps rendering via // HWUI. This extension is not defined in Khronos, hence the need to declare it -// manually here. There's a superseding extension (VK_EXT_frame_boundary) being -// discussed in Khronos, but in the meantime we use the bespoke -// VK_ANDROID_frame_boundary. This is a device extension that is implemented by +// manually here. There's an extension (VK_EXT_frame_boundary) which we will use +// instead if available. This is a device extension that is implemented by // AGI's Vulkan capture layer, such that it is only supported by devices when // AGI is doing a capture of the app. -// -// TODO(b/182165045): use the Khronos blessed VK_EXT_frame_boudary once it has -// landed in the spec. typedef void(VKAPI_PTR* PFN_vkFrameBoundaryANDROID)(VkDevice device, VkSemaphore semaphore, VkImage image); #define VK_ANDROID_FRAME_BOUNDARY_EXTENSION_NAME "VK_ANDROID_frame_boundary" diff --git a/location/api/system-current.txt b/location/api/system-current.txt index eb19ba84ee62..47984745fafc 100644 --- a/location/api/system-current.txt +++ b/location/api/system-current.txt @@ -96,8 +96,8 @@ package android.location { public static final class BeidouSatelliteEphemeris.BeidouSatelliteEphemerisTime implements android.os.Parcelable { method public int describeContents(); + method @IntRange(from=0, to=31) public int getAode(); method @IntRange(from=0) public int getBeidouWeekNumber(); - method @IntRange(from=0, to=31) public int getIode(); method @IntRange(from=0, to=604792) public int getToeSeconds(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.location.BeidouSatelliteEphemeris.BeidouSatelliteEphemerisTime> CREATOR; @@ -106,8 +106,8 @@ package android.location { public static final class BeidouSatelliteEphemeris.BeidouSatelliteEphemerisTime.Builder { ctor public BeidouSatelliteEphemeris.BeidouSatelliteEphemerisTime.Builder(); method @NonNull public android.location.BeidouSatelliteEphemeris.BeidouSatelliteEphemerisTime build(); + method @NonNull public android.location.BeidouSatelliteEphemeris.BeidouSatelliteEphemerisTime.Builder setAode(int); method @NonNull public android.location.BeidouSatelliteEphemeris.BeidouSatelliteEphemerisTime.Builder setBeidouWeekNumber(@IntRange(from=0) int); - method @NonNull public android.location.BeidouSatelliteEphemeris.BeidouSatelliteEphemerisTime.Builder setIode(int); method @NonNull public android.location.BeidouSatelliteEphemeris.BeidouSatelliteEphemerisTime.Builder setToeSeconds(@IntRange(from=0, to=604792) int); } @@ -177,7 +177,7 @@ package android.location { method public int describeContents(); method @Nullable public android.location.GnssAlmanac getAlmanac(); method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation(); - method @Nullable public android.location.KlobucharIonosphericModel getIonosphericModel(); + method @Nullable public android.location.GalileoIonosphericModel getIonosphericModel(); method @Nullable public android.location.LeapSecondsModel getLeapSecondsModel(); method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels(); method @NonNull public java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections> getSatelliteCorrections(); @@ -193,7 +193,7 @@ package android.location { method @NonNull public android.location.GalileoAssistance build(); method @NonNull public android.location.GalileoAssistance.Builder setAlmanac(@Nullable android.location.GnssAlmanac); method @NonNull public android.location.GalileoAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation); - method @NonNull public android.location.GalileoAssistance.Builder setIonosphericModel(@Nullable android.location.KlobucharIonosphericModel); + method @NonNull public android.location.GalileoAssistance.Builder setIonosphericModel(@Nullable android.location.GalileoIonosphericModel); method @NonNull public android.location.GalileoAssistance.Builder setLeapSecondsModel(@Nullable android.location.LeapSecondsModel); method @NonNull public android.location.GalileoAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>); method @NonNull public android.location.GalileoAssistance.Builder setSatelliteCorrections(@NonNull java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections>); diff --git a/location/java/android/location/BeidouSatelliteEphemeris.java b/location/java/android/location/BeidouSatelliteEphemeris.java index 3382c20964d9..dc5e8b89d7d7 100644 --- a/location/java/android/location/BeidouSatelliteEphemeris.java +++ b/location/java/android/location/BeidouSatelliteEphemeris.java @@ -527,7 +527,7 @@ public final class BeidouSatelliteEphemeris implements Parcelable { * * <p>This is defined in BDS-SIS-ICD-B1I-3.0 section 5.2.4.11 Table 5-8. */ - private final int mIode; + private final int mAode; /** Beidou week number without rollover */ private final int mBeidouWeekNumber; @@ -540,18 +540,18 @@ public final class BeidouSatelliteEphemeris implements Parcelable { private final int mToeSeconds; private BeidouSatelliteEphemerisTime(Builder builder) { - Preconditions.checkArgumentInRange(builder.mIode, 0, 31, "Iode"); + Preconditions.checkArgumentInRange(builder.mAode, 0, 31, "Aode"); Preconditions.checkArgument(builder.mBeidouWeekNumber >= 0); Preconditions.checkArgumentInRange(builder.mToeSeconds, 0, 604792, "ToeSeconds"); - mIode = builder.mIode; + mAode = builder.mAode; mBeidouWeekNumber = builder.mBeidouWeekNumber; mToeSeconds = builder.mToeSeconds; } /** Returns the AODE Age of Data, Ephemeris. */ @IntRange(from = 0, to = 31) - public int getIode() { - return mIode; + public int getAode() { + return mAode; } /** Returns the Beidou week number without rollover . */ @@ -573,7 +573,7 @@ public final class BeidouSatelliteEphemeris implements Parcelable { public BeidouSatelliteEphemerisTime createFromParcel(Parcel in) { final BeidouSatelliteEphemerisTime.Builder beidouSatelliteEphemerisTime = new Builder() - .setIode(in.readInt()) + .setAode(in.readInt()) .setBeidouWeekNumber(in.readInt()) .setToeSeconds(in.readInt()); return beidouSatelliteEphemerisTime.build(); @@ -592,7 +592,7 @@ public final class BeidouSatelliteEphemeris implements Parcelable { @Override public void writeToParcel(@NonNull Parcel parcel, int flags) { - parcel.writeInt(mIode); + parcel.writeInt(mAode); parcel.writeInt(mBeidouWeekNumber); parcel.writeInt(mToeSeconds); } @@ -600,7 +600,7 @@ public final class BeidouSatelliteEphemeris implements Parcelable { @Override public String toString() { StringBuilder builder = new StringBuilder("BeidouSatelliteEphemerisTime["); - builder.append("iode = ").append(mIode); + builder.append("aode = ").append(mAode); builder.append(", beidouWeekNumber = ").append(mBeidouWeekNumber); builder.append(", toeSeconds = ").append(mToeSeconds); builder.append("]"); @@ -609,14 +609,14 @@ public final class BeidouSatelliteEphemeris implements Parcelable { /** Builder for {@link BeidouSatelliteEphemerisTime} */ public static final class Builder { - private int mIode; + private int mAode; private int mBeidouWeekNumber; private int mToeSeconds; /** Sets the AODE Age of Data, Ephemeris. */ @NonNull - public Builder setIode(int iode) { - mIode = iode; + public Builder setAode(int iode) { + mAode = iode; return this; } diff --git a/location/java/android/location/GalileoAssistance.java b/location/java/android/location/GalileoAssistance.java index 8a09e6634d09..7f81ccdf346f 100644 --- a/location/java/android/location/GalileoAssistance.java +++ b/location/java/android/location/GalileoAssistance.java @@ -41,8 +41,8 @@ public final class GalileoAssistance implements Parcelable { /** The Galileo almanac. */ @Nullable private final GnssAlmanac mAlmanac; - /** The Klobuchar ionospheric model. */ - @Nullable private final KlobucharIonosphericModel mIonosphericModel; + /** The Galileo ionospheric model. */ + @Nullable private final GalileoIonosphericModel mIonosphericModel; /** The UTC model. */ @Nullable private final UtcModel mUtcModel; @@ -102,9 +102,9 @@ public final class GalileoAssistance implements Parcelable { return mAlmanac; } - /** Returns the Klobuchar ionospheric model. */ + /** Returns the Galileo ionospheric model. */ @Nullable - public KlobucharIonosphericModel getIonosphericModel() { + public GalileoIonosphericModel getIonosphericModel() { return mIonosphericModel; } @@ -192,7 +192,7 @@ public final class GalileoAssistance implements Parcelable { return new GalileoAssistance.Builder() .setAlmanac(in.readTypedObject(GnssAlmanac.CREATOR)) .setIonosphericModel( - in.readTypedObject(KlobucharIonosphericModel.CREATOR)) + in.readTypedObject(GalileoIonosphericModel.CREATOR)) .setUtcModel(in.readTypedObject(UtcModel.CREATOR)) .setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR)) .setAuxiliaryInformation( @@ -216,7 +216,7 @@ public final class GalileoAssistance implements Parcelable { /** Builder for {@link GalileoAssistance}. */ public static final class Builder { private GnssAlmanac mAlmanac; - private KlobucharIonosphericModel mIonosphericModel; + private GalileoIonosphericModel mIonosphericModel; private UtcModel mUtcModel; private LeapSecondsModel mLeapSecondsModel; private AuxiliaryInformation mAuxiliaryInformation; @@ -232,9 +232,9 @@ public final class GalileoAssistance implements Parcelable { return this; } - /** Sets the Klobuchar ionospheric model. */ + /** Sets the Galileo ionospheric model. */ @NonNull - public Builder setIonosphericModel(@Nullable KlobucharIonosphericModel ionosphericModel) { + public Builder setIonosphericModel(@Nullable GalileoIonosphericModel ionosphericModel) { mIonosphericModel = ionosphericModel; return this; } 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/projection/TEST_MAPPING b/media/java/android/media/projection/TEST_MAPPING index b33097c50002..ea62287b7411 100644 --- a/media/java/android/media/projection/TEST_MAPPING +++ b/media/java/android/media/projection/TEST_MAPPING @@ -1,7 +1,7 @@ { - "presubmit": [ + "imports": [ { - "name": "MediaProjectionTests" + "path": "frameworks/base/services/core/java/com/android/server/media/projection" } ] -} +}
\ No newline at end of file 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/TEST_MAPPING b/media/tests/projection/TEST_MAPPING new file mode 100644 index 000000000000..b33097c50002 --- /dev/null +++ b/media/tests/projection/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "presubmit": [ + { + "name": "MediaProjectionTests" + } + ] +} 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/DeviceStateRotationLock/src/com.android.settingslib.devicestate/AndroidSecureSettings.java b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/AndroidSecureSettings.java index 8aee576c3d04..a06d8829e331 100644 --- a/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/AndroidSecureSettings.java +++ b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/AndroidSecureSettings.java @@ -24,11 +24,11 @@ import android.provider.Settings; * Implementation of {@link SecureSettings} that uses Android's {@link Settings.Secure} * implementation. */ -class AndroidSecureSettings implements SecureSettings { +public class AndroidSecureSettings implements SecureSettings { private final ContentResolver mContentResolver; - AndroidSecureSettings(ContentResolver contentResolver) { + public AndroidSecureSettings(ContentResolver contentResolver) { mContentResolver = contentResolver; } diff --git a/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateAutoRotateSettingManager.kt b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateAutoRotateSettingManager.kt index fdde3d3f5669..1da17756fae6 100644 --- a/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateAutoRotateSettingManager.kt +++ b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateAutoRotateSettingManager.kt @@ -17,6 +17,7 @@ package com.android.settingslib.devicestate import android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK +import android.util.Dumpable /** * Interface for managing [DEVICE_STATE_ROTATION_LOCK] setting. @@ -25,7 +26,7 @@ import android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK * specific device states, retrieve the setting value, and check if rotation is locked for specific * or all device states. */ -interface DeviceStateAutoRotateSettingManager { +interface DeviceStateAutoRotateSettingManager : Dumpable { // TODO: b/397928958 - Rename all terms from rotationLock to autoRotate in all apis. /** Listener for changes in device-state based auto rotate setting. */ @@ -65,5 +66,3 @@ data class SettableDeviceState( /** Returns whether there is an auto-rotation setting for this device state. */ val isSettable: Boolean ) - - diff --git a/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateAutoRotateSettingManagerImpl.kt b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateAutoRotateSettingManagerImpl.kt index 0b6c6e238956..a9f9eda07118 100644 --- a/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateAutoRotateSettingManagerImpl.kt +++ b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateAutoRotateSettingManagerImpl.kt @@ -22,11 +22,13 @@ import android.os.UserHandle import android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK import android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_IGNORED import android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_LOCKED +import android.util.IndentingPrintWriter import android.util.Log import android.util.SparseIntArray import com.android.internal.R import com.android.settingslib.devicestate.DeviceStateAutoRotateSettingManager.DeviceStateAutoRotateSettingListener import com.android.window.flags.Flags +import java.io.PrintWriter import java.util.concurrent.Executor /** @@ -104,6 +106,15 @@ class DeviceStateAutoRotateSettingManagerImpl( throw UnsupportedOperationException("API updateSetting is not implemented yet") } + override fun dump(writer: PrintWriter, args: Array<out String>?) { + val indentingWriter = IndentingPrintWriter(writer) + indentingWriter.println("DeviceStateAutoRotateSettingManagerImpl") + indentingWriter.increaseIndent() + indentingWriter.println("fallbackPostureMap: $fallbackPostureMap") + indentingWriter.println("settableDeviceState: $settableDeviceState") + indentingWriter.decreaseIndent() + } + private fun notifyListeners() = settingListeners.forEach { listener -> listener.onSettingsChanged() } diff --git a/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateAutoRotateSettingManagerProvider.kt b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateAutoRotateSettingManagerProvider.kt new file mode 100644 index 000000000000..2db8e6f97498 --- /dev/null +++ b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateAutoRotateSettingManagerProvider.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.settingslib.devicestate + +import android.content.Context +import android.os.Handler +import com.android.window.flags.Flags +import java.util.concurrent.Executor + +/** + * Provides appropriate instance of [DeviceStateAutoRotateSettingManager], based on the value of + * [Flags.FLAG_ENABLE_DEVICE_STATE_AUTO_ROTATE_SETTING_REFACTOR]. + */ +object DeviceStateAutoRotateSettingManagerProvider { + /** + * Provides an instance of [DeviceStateAutoRotateSettingManager], based on the value of + * [Flags.FLAG_ENABLE_DEVICE_STATE_AUTO_ROTATE_SETTING_REFACTOR]. It is supposed to be used + * by apps that supports dagger. + */ + @JvmStatic + fun createInstance( + context: Context, + backgroundExecutor: Executor, + secureSettings: SecureSettings, + mainHandler: Handler, + posturesHelper: PosturesHelper, + ): DeviceStateAutoRotateSettingManager = + if (Flags.enableDeviceStateAutoRotateSettingRefactor()) { + DeviceStateAutoRotateSettingManagerImpl( + context, + backgroundExecutor, + secureSettings, + mainHandler, + posturesHelper, + ) + } else { + DeviceStateRotationLockSettingsManager(context, secureSettings) + } +} diff --git a/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateRotationLockSettingsManager.java b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateRotationLockSettingsManager.java index deeba574f2ad..6d180b63cd08 100644 --- a/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateRotationLockSettingsManager.java +++ b/packages/SettingsLib/DeviceStateRotationLock/src/com.android.settingslib.devicestate/DeviceStateRotationLockSettingsManager.java @@ -20,10 +20,8 @@ import static android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_IGNORE import static android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_LOCKED; import static android.provider.Settings.Secure.DEVICE_STATE_ROTATION_LOCK_UNLOCKED; -import static com.android.settingslib.devicestate.DeviceStateAutoRotateSettingManager.DeviceStateAutoRotateSettingListener; - +import android.annotation.NonNull; import android.annotation.Nullable; -import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.database.ContentObserver; @@ -41,6 +39,7 @@ import android.util.SparseIntArray; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -51,7 +50,8 @@ import java.util.Set; * Manages device-state based rotation lock settings. Handles reading, writing, and listening for * changes. */ -public final class DeviceStateRotationLockSettingsManager { +public final class DeviceStateRotationLockSettingsManager implements + DeviceStateAutoRotateSettingManager { private static final String TAG = "DSRotLockSettingsMngr"; private static final String SEPARATOR_REGEX = ":"; @@ -68,8 +68,7 @@ public final class DeviceStateRotationLockSettingsManager { private SparseIntArray mPostureRotationLockFallbackSettings; private List<SettableDeviceState> mSettableDeviceStates; - @VisibleForTesting - DeviceStateRotationLockSettingsManager(Context context, SecureSettings secureSettings) { + public DeviceStateRotationLockSettingsManager(Context context, SecureSettings secureSettings) { mSecureSettings = secureSettings; mPosturesHelper = new PosturesHelper(context, getDeviceStateManager(context)); @@ -89,30 +88,6 @@ public final class DeviceStateRotationLockSettingsManager { return null; } - /** Returns a singleton instance of this class */ - public static synchronized DeviceStateRotationLockSettingsManager getInstance(Context context) { - if (sSingleton == null) { - Context applicationContext = context.getApplicationContext(); - ContentResolver contentResolver = applicationContext.getContentResolver(); - SecureSettings secureSettings = new AndroidSecureSettings(contentResolver); - sSingleton = - new DeviceStateRotationLockSettingsManager(applicationContext, secureSettings); - } - return sSingleton; - } - - /** Resets the singleton instance of this class. Only used for testing. */ - @VisibleForTesting - public static synchronized void resetInstance() { - sSingleton = null; - } - - /** Returns true if device-state based rotation lock settings are enabled. */ - public static boolean isDeviceStateRotationLockEnabled(Context context) { - return context.getResources() - .getStringArray(R.array.config_perDeviceStateRotationLockDefaults).length > 0; - } - private void listenForSettingsChange() { mSecureSettings .registerContentObserver( @@ -131,7 +106,8 @@ public final class DeviceStateRotationLockSettingsManager { * Registers a {@link DeviceStateAutoRotateSettingListener} to be notified when the settings * change. Can be called multiple times with different listeners. */ - public void registerListener(DeviceStateAutoRotateSettingListener runnable) { + @Override + public void registerListener(@NonNull DeviceStateAutoRotateSettingListener runnable) { mListeners.add(runnable); } @@ -139,14 +115,16 @@ public final class DeviceStateRotationLockSettingsManager { * Unregisters a {@link DeviceStateAutoRotateSettingListener}. No-op if the given instance * was never registered. */ + @Override public void unregisterListener( - DeviceStateAutoRotateSettingListener deviceStateAutoRotateSettingListener) { + @NonNull DeviceStateAutoRotateSettingListener deviceStateAutoRotateSettingListener) { if (!mListeners.remove(deviceStateAutoRotateSettingListener)) { Log.w(TAG, "Attempting to unregister a listener hadn't been registered"); } } /** Updates the rotation lock setting for a specified device state. */ + @Override public void updateSetting(int deviceState, boolean rotationLocked) { int posture = mPosturesHelper.deviceStateToPosture(deviceState); if (mPostureRotationLockFallbackSettings.indexOfKey(posture) >= 0) { @@ -173,6 +151,7 @@ public final class DeviceStateRotationLockSettingsManager { * DEVICE_STATE_ROTATION_LOCK_IGNORED}. */ @Settings.Secure.DeviceStateRotationLockSetting + @Override public int getRotationLockSetting(int deviceState) { int devicePosture = mPosturesHelper.deviceStateToPosture(deviceState); int rotationLockSetting = mPostureRotationLockSettings.get( @@ -196,6 +175,7 @@ public final class DeviceStateRotationLockSettingsManager { /** Returns true if the rotation is locked for the current device state */ + @Override public boolean isRotationLocked(int deviceState) { return getRotationLockSetting(deviceState) == DEVICE_STATE_ROTATION_LOCK_LOCKED; } @@ -204,6 +184,7 @@ public final class DeviceStateRotationLockSettingsManager { * Returns true if there is no device state for which the current setting is {@link * DEVICE_STATE_ROTATION_LOCK_UNLOCKED}. */ + @Override public boolean isRotationLockedForAllStates() { for (int i = 0; i < mPostureRotationLockSettings.size(); i++) { if (mPostureRotationLockSettings.valueAt(i) @@ -215,6 +196,8 @@ public final class DeviceStateRotationLockSettingsManager { } /** Returns a list of device states and their respective auto-rotation setting availability. */ + @Override + @NonNull public List<SettableDeviceState> getSettableDeviceStates() { // Returning a copy to make sure that nothing outside can mutate our internal list. return new ArrayList<>(mSettableDeviceStates); @@ -356,17 +339,21 @@ public final class DeviceStateRotationLockSettingsManager { } } - /** Dumps internal state. */ - public void dump(IndentingPrintWriter pw) { - pw.println("DeviceStateRotationLockSettingsManager"); - pw.increaseIndent(); - pw.println("mPostureRotationLockDefaults: " + @Override + public void dump(@NonNull PrintWriter writer, String[] args) { + IndentingPrintWriter indentingWriter = new IndentingPrintWriter(writer); + indentingWriter.println("DeviceStateRotationLockSettingsManager"); + indentingWriter.increaseIndent(); + indentingWriter.println("mPostureRotationLockDefaults: " + Arrays.toString(mPostureRotationLockDefaults)); - pw.println("mPostureDefaultRotationLockSettings: " + mPostureDefaultRotationLockSettings); - pw.println("mDeviceStateRotationLockSettings: " + mPostureRotationLockSettings); - pw.println("mPostureRotationLockFallbackSettings: " + mPostureRotationLockFallbackSettings); - pw.println("mSettableDeviceStates: " + mSettableDeviceStates); - pw.decreaseIndent(); + indentingWriter.println( + "mPostureDefaultRotationLockSettings: " + mPostureDefaultRotationLockSettings); + indentingWriter.println( + "mDeviceStateRotationLockSettings: " + mPostureRotationLockSettings); + indentingWriter.println( + "mPostureRotationLockFallbackSettings: " + mPostureRotationLockFallbackSettings); + indentingWriter.println("mSettableDeviceStates: " + mSettableDeviceStates); + indentingWriter.decreaseIndent(); } /** 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/integ/src/com/android/settingslib/devicestate/DeviceStateAutoRotateSettingManagerImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateAutoRotateSettingManagerImplTest.kt index 78dba57028ba..a9329ba3c76c 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateAutoRotateSettingManagerImplTest.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateAutoRotateSettingManagerImplTest.kt @@ -181,8 +181,10 @@ class DeviceStateAutoRotateSettingManagerImplTest { @Test fun getAutoRotateSetting_forInvalidPosture_returnsSettingForFallbackPosture() { - persistSettings(DEVICE_STATE_ROTATION_KEY_UNFOLDED, DEVICE_STATE_ROTATION_LOCK_UNLOCKED) - persistSettings(DEVICE_STATE_ROTATION_KEY_FOLDED, DEVICE_STATE_ROTATION_LOCK_LOCKED) + persistSettings( + "$DEVICE_STATE_ROTATION_KEY_FOLDED:$DEVICE_STATE_ROTATION_LOCK_LOCKED:" + + "$DEVICE_STATE_ROTATION_KEY_UNFOLDED:$DEVICE_STATE_ROTATION_LOCK_UNLOCKED" + ) val autoRotateSetting = settingManager.getRotationLockSetting(DEVICE_STATE_HALF_FOLDED) @@ -276,7 +278,6 @@ class DeviceStateAutoRotateSettingManagerImplTest { SettableDeviceState(DEVICE_STATE_ROTATION_KEY_FOLDED, isSettable = true), SettableDeviceState(DEVICE_STATE_ROTATION_KEY_HALF_FOLDED, isSettable = false), SettableDeviceState(DEVICE_STATE_ROTATION_KEY_REAR_DISPLAY, isSettable = false), - SettableDeviceState(DEVICE_STATE_ROTATION_LOCK_IGNORED, isSettable = false), ) } diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateAutoRotateSettingManagerProviderTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateAutoRotateSettingManagerProviderTest.kt new file mode 100644 index 000000000000..c3ec4edfdee5 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateAutoRotateSettingManagerProviderTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.devicestate + +import android.content.Context +import android.os.Handler +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags +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.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import java.util.concurrent.Executor +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DeviceStateAutoRotateSettingManagerProviderTest { + + @get:Rule + val setFlagsRule: SetFlagsRule = SetFlagsRule() + + @get:Rule val rule = MockitoJUnit.rule() + + private val context: Context = ApplicationProvider.getApplicationContext() + + @Mock + private lateinit var mockExecutor: Executor + + @Mock + private lateinit var mockSecureSettings: SecureSettings + + @Mock + private lateinit var mockMainHandler: Handler + + @Mock + private lateinit var mockPosturesHelper: PosturesHelper + + @Before + fun setup() { + whenever(mockSecureSettings.getStringForUser(any(), anyInt())).thenReturn("") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DEVICE_STATE_AUTO_ROTATE_SETTING_REFACTOR) + fun createInstance_refactorFlagEnabled_returnsRefactoredManager() { + val manager = + DeviceStateAutoRotateSettingManagerProvider.createInstance( + context, mockExecutor, mockSecureSettings, mockMainHandler, mockPosturesHelper + ) + + assertThat(manager).isInstanceOf(DeviceStateAutoRotateSettingManagerImpl::class.java) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DEVICE_STATE_AUTO_ROTATE_SETTING_REFACTOR) + fun createInstance_refactorFlagDisabled_returnsLegacyManager() { + val manager = + DeviceStateAutoRotateSettingManagerProvider.createInstance( + context, mockExecutor, mockSecureSettings, mockMainHandler, mockPosturesHelper + ) + + assertThat(manager).isInstanceOf(DeviceStateRotationLockSettingsManager::class.java) + } +} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java index baebaf7dfef0..b23ea5f1786f 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/devicestate/DeviceStateRotationLockSettingsManagerTest.java @@ -151,8 +151,8 @@ public class DeviceStateRotationLockSettingsManagerTest { new String[]{"2:1", "1:0:1", "0:2"}); List<SettableDeviceState> settableDeviceStates = - DeviceStateRotationLockSettingsManager.getInstance( - mMockContext).getSettableDeviceStates(); + new DeviceStateRotationLockSettingsManager(mMockContext, + mFakeSecureSettings).getSettableDeviceStates(); assertThat(settableDeviceStates).containsExactly( new SettableDeviceState(/* deviceState= */ 2, /* isSettable= */ true), 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..527a1f16a84f 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java @@ -16,6 +16,8 @@ package com.android.providers.settings; +import static com.android.settingslib.devicestate.DeviceStateAutoRotateSettingUtils.isDeviceStateRotationLockEnabled; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; @@ -49,11 +51,11 @@ import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.LocalePicker; import com.android.server.backup.Flags; -import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager; 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; @@ -347,7 +349,7 @@ public class SettingsHelper { private boolean shouldSkipAutoRotateRestore() { // When device state based auto rotation settings are available, let's skip the restoring // of the standard auto rotation settings to avoid conflicting setting values. - return DeviceStateRotationLockSettingsManager.isDeviceStateRotationLockEnabled(mContext); + return isDeviceStateRotationLockEnabled(mContext); } public String onBackupValue(String name, String value) { @@ -650,6 +652,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 +689,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/Android.bp b/packages/SystemUI/Android.bp index 7f4544379cd3..2b17ae48132c 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -220,6 +220,7 @@ filegroup { "tests/src/**/systemui/statusbar/phone/StatusBarBoundsProviderTest.kt", "tests/src/**/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationControllerTest.kt", "tests/src/**/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelImplTest.kt", + "tests/src/**/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryKairosAdapterTest.kt", "tests/src/**/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt", "tests/src/**/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt", "tests/src/**/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt", @@ -356,7 +357,9 @@ filegroup { "tests/src/**/systemui/qs/tiles/AlarmTileTest.kt", "tests/src/**/systemui/qs/tiles/BluetoothTileTest.kt", "tests/src/**/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt", + "tests/src/**/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryKairosAdapterTest.kt", "tests/src/**/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt", + "tests/src/**/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionKairosAdapterTelephonySmokeTests.kt", "tests/src/**/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt", "tests/src/**/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt", "tests/src/**/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfigTest.kt", diff --git a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp index fb1f715bc68f..172f687ef6fd 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp +++ b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp @@ -40,6 +40,7 @@ android_app { "com_android_systemui_flags_lib", "SettingsLibDisplayUtils", "SettingsLibSettingsTheme", + "SystemUI-shared-utils", "com_android_a11y_menu_flags_lib", "//frameworks/libs/systemui:view_capture", ], diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java index a60778658c59..db2fbd96408c 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java @@ -22,8 +22,6 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE; import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; -import static com.android.app.viewcapture.ViewCaptureFactory.getViewCaptureAwareWindowManagerInstance; - import static java.lang.Math.max; import android.animation.Animator; @@ -55,11 +53,11 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.UiContext; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService; import com.android.systemui.accessibility.accessibilitymenu.R; import com.android.systemui.accessibility.accessibilitymenu.activity.A11yMenuSettingsActivity.A11yMenuPreferenceFragment; import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut; +import com.android.systemui.utils.windowmanager.WindowManagerUtils; import java.util.ArrayList; import java.util.List; @@ -145,9 +143,7 @@ public class A11yMenuOverlayLayout { final Display display = mDisplayManager.getDisplay(DEFAULT_DISPLAY); final Context uiContext = mService.createWindowContext( display, TYPE_ACCESSIBILITY_OVERLAY, /* options= */null); - final ViewCaptureAwareWindowManager windowManager = - getViewCaptureAwareWindowManagerInstance(uiContext, - com.android.systemui.Flags.enableViewCaptureTracing()); + final WindowManager windowManager = WindowManagerUtils.getWindowManager(uiContext); mLayout = new A11yMenuFrameLayout(uiContext); updateLayoutPosition(uiContext); inflateLayoutAndSetOnTouchListener(mLayout, uiContext); @@ -162,8 +158,7 @@ public class A11yMenuOverlayLayout { public void clearLayout() { if (mLayout != null) { - ViewCaptureAwareWindowManager windowManager = getViewCaptureAwareWindowManagerInstance( - mLayout.getContext(), com.android.systemui.Flags.enableViewCaptureTracing()); + WindowManager windowManager = WindowManagerUtils.getWindowManager(mLayout.getContext()); if (windowManager != null) { windowManager.removeView(mLayout); } @@ -178,7 +173,7 @@ public class A11yMenuOverlayLayout { return; } updateLayoutPosition(mLayout.getContext()); - WindowManager windowManager = mLayout.getContext().getSystemService(WindowManager.class); + WindowManager windowManager = WindowManagerUtils.getWindowManager(mLayout.getContext()); if (windowManager != null) { windowManager.updateViewLayout(mLayout, mLayoutParameter); } diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java index b899c45b0f7e..842834b5fc27 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java @@ -36,6 +36,7 @@ import com.android.systemui.accessibility.accessibilitymenu.R; import com.android.systemui.accessibility.accessibilitymenu.activity.A11yMenuSettingsActivity.A11yMenuPreferenceFragment; import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut; import com.android.systemui.accessibility.accessibilitymenu.view.A11yMenuFooter.A11yMenuFooterCallBack; +import com.android.systemui.utils.windowmanager.WindowManagerUtils; import java.util.ArrayList; import java.util.List; @@ -292,8 +293,8 @@ public class A11yMenuViewPager { // Keeps footer window height unchanged no matter the density is changed. mA11yMenuFooter.adjustFooterToDensityScale(densityScale); // Adjust the view pager height for system bar and display cutout insets. - WindowManager windowManager = mA11yMenuLayout.getContext() - .getSystemService(WindowManager.class); + WindowManager windowManager = WindowManagerUtils + .getWindowManager(mA11yMenuLayout.getContext()); WindowMetrics windowMetric = windowManager.getCurrentWindowMetrics(); Insets windowInsets = windowMetric.getWindowInsets().getInsetsIgnoringVisibility( WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); 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 30bce3576e4f..9da07170430f 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,10 +1873,11 @@ flag { } flag { - name: "shade_header_fonts" + name: "shade_header_font_update" namespace: "systemui" description: "Updates the fonts of the shade header" - bug: "393609724" + bug: "393609960" + is_fixed_read_only: true } flag { @@ -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,26 @@ 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 + } +} + +flag { + name: "extended_apps_shortcut_category" + namespace: "systemui" + description: "Allow users to add shortcuts to open apps that are not present in the apps category in shortcut helper by default" + bug: "394290928" +} + +flag { + name: "use_aad_prox_sensor" + namespace: "systemui" + description: "Use AAD proximity sensor if flag is enabled and sensor is present" + bug: "402534470" +} 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..440a81fc2152 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) } @@ -1086,9 +1088,8 @@ constructor( if (!success) finishedCallback?.onAnimationFinished() } } else { - // This should never happen, as either the controller or factory should always be - // defined. This final call is for safety in case something goes wrong. - Log.wtf(TAG, "initAndRun with neither a controller nor factory") + // This happens when onDisposed() has already been called due to the animation being + // cancelled. Only issue the callback. finishedCallback?.onAnimationFinished() } } 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/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index 068218a0053a..b30e12f073ad 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -55,6 +55,7 @@ import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.mechanics.behavior.VerticalExpandContainerSpec import com.android.mechanics.behavior.verticalExpandContainerBackground +import com.android.systemui.Flags import com.android.systemui.res.R import com.android.systemui.shade.ui.composable.OverlayShade.rememberShadeExpansionMotion @@ -189,9 +190,17 @@ object OverlayShade { } object Colors { - val ScrimBackground = Color(0f, 0f, 0f, alpha = 0.2f) + val ScrimBackground = Color(0f, 0f, 0f, alpha = 0.3f) val PanelBackground: Color - @Composable @ReadOnlyComposable get() = MaterialTheme.colorScheme.surfaceContainer + @Composable + @ReadOnlyComposable + get() { + return if (Flags.notificationShadeBlur()) { + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f) + } else { + MaterialTheme.colorScheme.surfaceContainer + } + } } object Dimensions { 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 addb08fd2319..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 @@ -293,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) @@ -308,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), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index f9492dad85df..a65afad9cb7a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -127,6 +127,8 @@ fun VolumeSlider( SliderDefaults.colors( activeTickColor = MaterialTheme.colorScheme.surfaceContainerHigh, inactiveTrackColor = MaterialTheme.colorScheme.surfaceContainerHigh, + disabledActiveTickColor = MaterialTheme.colorScheme.surfaceContainerHigh, + disabledInactiveTrackColor = MaterialTheme.colorScheme.surfaceContainerHigh, ) Slider( value = state.value, 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/MagnificationModeSwitchTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java index aa95abb3528f..0b0088926aae 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java @@ -73,14 +73,10 @@ import android.widget.ImageView; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCapture; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.SysuiTestCase; import com.android.systemui.res.R; -import kotlin.Lazy; - import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -108,8 +104,6 @@ public class MagnificationModeSwitchTest extends SysuiTestCase { private SfVsyncFrameCallbackProvider mSfVsyncFrameProvider; @Mock private MagnificationModeSwitch.ClickListener mClickListener; - @Mock - private Lazy<ViewCapture> mLazyViewCapture; private TestableWindowManager mWindowManager; private ViewPropertyAnimator mViewPropertyAnimator; private MagnificationModeSwitch mMagnificationModeSwitch; @@ -123,7 +117,6 @@ public class MagnificationModeSwitchTest extends SysuiTestCase { mContext = Mockito.spy(getContext()); final WindowManager wm = mContext.getSystemService(WindowManager.class); mWindowManager = spy(new TestableWindowManager(wm)); - mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager); mContext.addMockSystemService(Context.ACCESSIBILITY_SERVICE, mAccessibilityManager); mSpyImageView = Mockito.spy(new ImageView(mContext)); mViewPropertyAnimator = Mockito.spy(mSpyImageView.animate()); @@ -139,10 +132,8 @@ public class MagnificationModeSwitchTest extends SysuiTestCase { return null; }).when(mSfVsyncFrameProvider).postFrameCallback( any(Choreographer.FrameCallback.class)); - ViewCaptureAwareWindowManager vwm = new ViewCaptureAwareWindowManager(mWindowManager, - mLazyViewCapture, false); - mMagnificationModeSwitch = new MagnificationModeSwitch(mContext, mSpyImageView, - mSfVsyncFrameProvider, mClickListener, vwm); + mMagnificationModeSwitch = new MagnificationModeSwitch(mContext, mWindowManager, + mSpyImageView, mSfVsyncFrameProvider, mClickListener); assertNotNull(mTouchListener); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java index 3cd3fefb8ef0..02ec5aac120c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java @@ -23,11 +23,11 @@ import static org.mockito.Mockito.verify; import android.content.pm.ActivityInfo; import android.testing.TestableLooper; +import android.view.WindowManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.SysuiTestCase; import com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize; @@ -58,15 +58,15 @@ public class MagnificationSettingsControllerTest extends SysuiTestCase { @Mock private SecureSettings mSecureSettings; @Mock - private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; + private WindowManager mWindowManager; @Before public void setUp() { MockitoAnnotations.initMocks(this); mMagnificationSettingsController = new MagnificationSettingsController( mContext, mSfVsyncFrameProvider, - mMagnificationSettingControllerCallback, mSecureSettings, - mWindowMagnificationSettings, mViewCaptureAwareWindowManager); + mMagnificationSettingControllerCallback, mSecureSettings, mWindowManager, + mWindowMagnificationSettings); } @After diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java index 12c866f0adb2..463bfe5ae73f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java @@ -27,14 +27,15 @@ import static org.mockito.Mockito.verify; import android.content.Context; import android.graphics.Point; +import android.os.Looper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.WindowManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.SysuiTestCase; import org.junit.Before; @@ -48,7 +49,7 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class MirrorWindowControlTest extends SysuiTestCase { - @Mock ViewCaptureAwareWindowManager mWindowManager; + @Mock WindowManager mWindowManager; View mView; int mViewWidth; int mViewHeight; @@ -69,8 +70,12 @@ public class MirrorWindowControlTest extends SysuiTestCase { return null; }).when(mWindowManager).addView(any(View.class), any(LayoutParams.class)); + if (Looper.myLooper() == null) { + Looper.prepare(); + } + mStubMirrorWindowControl = new StubMirrorWindowControl(getContext(), mView, mViewWidth, - mViewHeight); + mViewHeight, mWindowManager); } @Test @@ -122,8 +127,9 @@ public class MirrorWindowControlTest extends SysuiTestCase { boolean mInvokeOnCreateView = false; - StubMirrorWindowControl(Context context, View view, int width, int height) { - super(context); + StubMirrorWindowControl(Context context, View view, int width, int height, + WindowManager windowManager) { + super(context, windowManager); mView = view; mWidth = width; mHeight = height; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/ModeSwitchesControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/ModeSwitchesControllerTest.java index e1e515eb31f5..b1c837a99f3d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/ModeSwitchesControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/ModeSwitchesControllerTest.java @@ -24,11 +24,11 @@ import android.provider.Settings; import android.testing.TestableLooper; import android.view.Display; import android.view.View; +import android.view.WindowManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.SysuiTestCase; import org.junit.After; @@ -51,8 +51,6 @@ public class ModeSwitchesControllerTest extends SysuiTestCase { private View mSpyView; @Mock private MagnificationModeSwitch.ClickListener mListener; - @Mock - private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; @Before @@ -61,8 +59,9 @@ public class ModeSwitchesControllerTest extends SysuiTestCase { mSupplier = new FakeSwitchSupplier(mContext.getSystemService(DisplayManager.class)); mModeSwitchesController = new ModeSwitchesController(mSupplier); mModeSwitchesController.setClickListenerDelegate(mListener); - mModeSwitch = Mockito.spy(new MagnificationModeSwitch(mContext, mModeSwitchesController, - mViewCaptureAwareWindowManager)); + WindowManager wm = mContext.getSystemService(WindowManager.class); + mModeSwitch = Mockito.spy(new MagnificationModeSwitch(mContext, wm, + mModeSwitchesController)); mSpyView = Mockito.spy(new View(mContext)); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java index 6edf94939010..95ebd8190e9c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java @@ -72,7 +72,7 @@ public class DragToInteractAnimationControllerTest extends SysuiTestCase { stubWindowManager); final MenuView stubMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance, mockSecureSettings)); - mInteractView = spy(new DragToInteractView(mContext)); + mInteractView = spy(new DragToInteractView(mContext, stubWindowManager)); mDismissView = spy(new DismissView(mContext)); if (Flags.floatingMenuDragToEdit()) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java index fff6def52803..572d140b850d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java @@ -101,7 +101,7 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { mStubMenuView.setTranslationY(0); mMenuAnimationController = spy(new MenuAnimationController( mStubMenuView, stubMenuViewAppearance)); - mInteractView = spy(new DragToInteractView(mContext)); + mInteractView = spy(new DragToInteractView(mContext, windowManager)); mDismissView = spy(new DismissView(mContext)); if (Flags.floatingMenuDragToEdit()) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java index 4f043109a534..b5fa52570d6a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java @@ -39,15 +39,11 @@ import android.view.accessibility.AccessibilityManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCapture; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.settingslib.bluetooth.HearingAidDeviceManager; import com.android.systemui.SysuiTestCase; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.util.settings.SecureSettings; -import kotlin.Lazy; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -78,16 +74,11 @@ public class MenuViewLayerControllerTest extends SysuiTestCase { @Mock private WindowMetrics mWindowMetrics; - @Mock - private Lazy<ViewCapture> mLazyViewCapture; - private MenuViewLayerController mMenuViewLayerController; @Before public void setUp() throws Exception { final WindowManager wm = mContext.getSystemService(WindowManager.class); - final ViewCaptureAwareWindowManager viewCaptureAwareWm = new ViewCaptureAwareWindowManager( - mWindowManager, mLazyViewCapture, /* isViewCaptureEnabled= */ false); doAnswer(invocation -> wm.getMaximumWindowMetrics()).when( mWindowManager).getMaximumWindowMetrics(); mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager); @@ -95,8 +86,8 @@ public class MenuViewLayerControllerTest extends SysuiTestCase { when(mWindowMetrics.getBounds()).thenReturn(new Rect(0, 0, 1080, 2340)); when(mWindowMetrics.getWindowInsets()).thenReturn(stubDisplayInsets()); mMenuViewLayerController = new MenuViewLayerController(mContext, mWindowManager, - viewCaptureAwareWm, mAccessibilityManager, mSecureSettings, - mock(NavigationModeController.class), mHearingAidDeviceManager); + mAccessibilityManager, mSecureSettings, mock(NavigationModeController.class), + mHearingAidDeviceManager); } @Test 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/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index 446891a7873e..c4fafc192260 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -41,7 +41,6 @@ import android.widget.ScrollView import androidx.constraintlayout.widget.ConstraintLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.app.viewcapture.ViewCapture import com.android.internal.jank.InteractionJankMonitor import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN @@ -116,7 +115,6 @@ open class AuthContainerViewTest : SysuiTestCase() { @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var activityTaskManager: ActivityTaskManager @Mock private lateinit var accessibilityManager: AccessibilityManager - @Mock private lateinit var lazyViewCapture: Lazy<ViewCapture> private lateinit var displayRepository: FakeDisplayRepository private lateinit var displayStateInteractor: DisplayStateInteractor @@ -689,7 +687,6 @@ open class AuthContainerViewTest : SysuiTestCase() { { credentialViewModel }, fakeExecutor, vibrator, - lazyViewCapture, msdlPlayer, ) { override fun postOnAnimation(runnable: Runnable) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java index a1a2aa70d869..f4185ee48510 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -83,7 +83,6 @@ import android.view.WindowManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCapture; import com.android.internal.R; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.widget.LockPatternUtils; @@ -100,11 +99,10 @@ import com.android.systemui.util.concurrency.Execution; import com.android.systemui.util.concurrency.FakeExecution; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import com.google.android.msdl.domain.MSDLPlayer; -import dagger.Lazy; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -175,8 +173,6 @@ public class AuthControllerTest extends SysuiTestCase { private PromptViewModel mPromptViewModel; @Mock private UdfpsUtils mUdfpsUtils; - @Mock - private Lazy<ViewCapture> mLazyViewCapture; @Captor private ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mFpAuthenticatorsRegisteredCaptor; @@ -198,6 +194,8 @@ public class AuthControllerTest extends SysuiTestCase { private KeyguardManager mKeyguardManager; @Mock private MSDLPlayer mMSDLPlayer; + @Mock + private WindowManagerProvider mWindowManagerProvider; private TestableContext mContextSpy; private Execution mExecution; @@ -1198,7 +1196,8 @@ public class AuthControllerTest extends SysuiTestCase { when(mDisplayManager.getDisplay(displayId)).thenReturn(mockDisplay); } doReturn(mockDisplayContext).when(mContextSpy).createDisplayContext(mockDisplay); - when(mockDisplayContext.getSystemService(WindowManager.class)).thenReturn(mockDisplayWM); + when(mWindowManagerProvider.getWindowManager(mockDisplayContext)) + .thenReturn(mockDisplayWM); return mockDisplayWM; } @@ -1214,7 +1213,7 @@ public class AuthControllerTest extends SysuiTestCase { () -> mLogContextInteractor, () -> mPromptSelectionInteractor, () -> mCredentialViewModel, () -> mPromptViewModel, mInteractionJankMonitor, mHandler, mBackgroundExecutor, mUdfpsUtils, mVibratorHelper, mKeyguardManager, - mLazyViewCapture, mMSDLPlayer); + mMSDLPlayer, mWindowManagerProvider); } @Override diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index a2f5a30a20ff..675c9deaff23 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -45,12 +45,12 @@ import android.view.LayoutInflater; import android.view.Surface; import android.view.View; import android.view.ViewRootImpl; +import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.logging.InstanceIdSequence; import com.android.internal.util.LatencyTracker; import com.android.keyguard.KeyguardUpdateMonitor; @@ -129,7 +129,7 @@ public class UdfpsControllerTest extends SysuiTestCase { @Mock private FingerprintManager mFingerprintManager; @Mock - private ViewCaptureAwareWindowManager mWindowManager; + private WindowManager mWindowManager; @Mock private StatusBarStateController mStatusBarStateController; @Mock 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/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt index 95334b5aaf09..856a62e3f5a7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractorTest.kt @@ -25,9 +25,10 @@ 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.FEATURE_MANUAL_OPEN import com.android.systemui.communal.data.model.SuppressionReason +import com.android.systemui.communal.posturing.data.model.PositionState import com.android.systemui.communal.posturing.data.repository.fake import com.android.systemui.communal.posturing.data.repository.posturingRepository -import com.android.systemui.communal.posturing.shared.model.PosturedState +import com.android.systemui.communal.posturing.domain.interactor.advanceTimeBySlidingWindowAndRun import com.android.systemui.dock.DockManager import com.android.systemui.dock.fakeDockManager import com.android.systemui.kosmos.Kosmos @@ -127,7 +128,12 @@ class CommunalAutoOpenInteractorTest : SysuiTestCase() { ) batteryRepository.fake.setDevicePluggedIn(true) - posturingRepository.fake.setPosturedState(PosturedState.NotPostured) + posturingRepository.fake.emitPositionState( + PositionState( + stationary = PositionState.StationaryState.Stationary(confidence = 1f), + orientation = PositionState.OrientationState.NotPostured(confidence = 1f), + ) + ) assertThat(shouldAutoOpen).isFalse() assertThat(suppressionReason) @@ -135,7 +141,13 @@ class CommunalAutoOpenInteractorTest : SysuiTestCase() { SuppressionReason.ReasonWhenToAutoShow(FEATURE_AUTO_OPEN or FEATURE_MANUAL_OPEN) ) - posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) + posturingRepository.fake.emitPositionState( + PositionState( + stationary = PositionState.StationaryState.Stationary(confidence = 1f), + orientation = PositionState.OrientationState.Postured(confidence = 1f), + ) + ) + advanceTimeBySlidingWindowAndRun() assertThat(shouldAutoOpen).isTrue() assertThat(suppressionReason).isNull() } @@ -153,7 +165,7 @@ class CommunalAutoOpenInteractorTest : SysuiTestCase() { ) batteryRepository.fake.setDevicePluggedIn(true) - posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) + posturingRepository.fake.emitPositionState(PositionState()) fakeDockManager.setIsDocked(true) assertThat(shouldAutoOpen).isFalse() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt index 0df8834618d5..b4708d97c4c3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorTest.kt @@ -16,22 +16,39 @@ package com.android.systemui.communal.posturing.domain.interactor +import android.hardware.Sensor +import android.hardware.TriggerEventListener +import android.platform.test.annotations.EnableFlags +import android.service.dreams.Flags.FLAG_ALLOW_DREAM_WHEN_POSTURED import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.posturing.data.model.PositionState import com.android.systemui.communal.posturing.data.repository.fake import com.android.systemui.communal.posturing.data.repository.posturingRepository import com.android.systemui.communal.posturing.shared.model.PosturedState +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.advanceTimeBy import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.testKosmos +import com.android.systemui.util.sensors.asyncSensorManager import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.milliseconds import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub @SmallTest @RunWith(AndroidJUnit4::class) +@EnableFlags(FLAG_ALLOW_DREAM_WHEN_POSTURED) class PosturingInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() @@ -44,8 +61,166 @@ class PosturingInteractorTest : SysuiTestCase() { val postured by collectLastValue(underTest.postured) assertThat(postured).isFalse() - posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) + posturingRepository.fake.emitPositionState( + PositionState( + stationary = PositionState.StationaryState.Stationary(confidence = 1f), + orientation = PositionState.OrientationState.Postured(confidence = 1f), + ) + ) + + advanceTimeBySlidingWindowAndRun() + assertThat(postured).isTrue() + } + + @Test + fun testLowConfidenceOrientation() = + kosmos.runTest { + val postured by collectLastValue(underTest.postured) + assertThat(postured).isFalse() + + posturingRepository.fake.emitPositionState( + PositionState( + stationary = PositionState.StationaryState.Stationary(confidence = 1f), + orientation = PositionState.OrientationState.Postured(confidence = 0.2f), + ) + ) + + advanceTimeBySlidingWindowAndRun() + assertThat(postured).isFalse() + } + + @Test + fun testLowConfidenceStationary() = + kosmos.runTest { + val postured by collectLastValue(underTest.postured) + assertThat(postured).isFalse() + + posturingRepository.fake.emitPositionState( + PositionState( + stationary = PositionState.StationaryState.Stationary(confidence = 1f), + orientation = PositionState.OrientationState.Postured(confidence = 0.2f), + ) + ) + + advanceTimeBySlidingWindowAndRun() + assertThat(postured).isFalse() + } + + @Test + fun testSlidingWindow() = + kosmos.runTest { + val postured by collectLastValue(underTest.postured) + assertThat(postured).isFalse() + + posturingRepository.fake.emitPositionState( + PositionState( + stationary = PositionState.StationaryState.Stationary(confidence = 1f), + orientation = PositionState.OrientationState.Postured(confidence = 0.2f), + ) + ) + + advanceTimeBy(PosturingInteractor.SLIDING_WINDOW_DURATION / 2) + runCurrent() + assertThat(postured).isFalse() + + posturingRepository.fake.emitPositionState( + PositionState( + stationary = PositionState.StationaryState.Stationary(confidence = 1f), + orientation = PositionState.OrientationState.Postured(confidence = 1f), + ) + ) + assertThat(postured).isFalse() + advanceTimeBy(PosturingInteractor.SLIDING_WINDOW_DURATION / 2) + runCurrent() + + // The 0.2 confidence will have fallen out of the sliding window, and we should now flip + // to true. + assertThat(postured).isTrue() + + advanceTimeBy(9999.hours) + // We should remain postured if no other updates are received. + assertThat(postured).isTrue() + } + + @Test + fun testLiftGesture_afterSlidingWindow() = + kosmos.runTest { + val triggerSensor = stubSensorManager() + val sensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)!! + + val postured by collectLastValue(underTest.postured) + assertThat(postured).isFalse() + + posturingRepository.fake.emitPositionState( + PositionState( + stationary = PositionState.StationaryState.Stationary(confidence = 1f), + orientation = PositionState.OrientationState.Postured(confidence = 1f), + ) + ) + + advanceTimeBySlidingWindowAndRun() + assertThat(postured).isTrue() + + // If we detect a lift gesture, we should transition back to not postured. + triggerSensor(sensor) + assertThat(postured).isFalse() + + advanceTimeBy(9999.hours) + assertThat(postured).isFalse() + } + + @Test + fun testLiftGesture_overridesSlidingWindow() = + kosmos.runTest { + val triggerSensor = stubSensorManager() + val sensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)!! + + val postured by collectLastValue(underTest.postured) + assertThat(postured).isFalse() + + // Add multiple stationary + postured events to the sliding window. + repeat(100) { + advanceTimeBy(1.milliseconds) + posturingRepository.fake.emitPositionState( + PositionState( + stationary = PositionState.StationaryState.Stationary(confidence = 1f), + orientation = PositionState.OrientationState.Postured(confidence = 1f), + ) + ) + } + + assertThat(postured).isTrue() + + // If we detect a lift gesture, we should transition back to not postured immediately. + triggerSensor(sensor) + assertThat(postured).isFalse() + } + + @Test + fun testSignificantMotion_afterSlidingWindow() = + kosmos.runTest { + val triggerSensor = stubSensorManager() + val sensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_SIGNIFICANT_MOTION)!! + + val postured by collectLastValue(underTest.postured) + assertThat(postured).isFalse() + + posturingRepository.fake.emitPositionState( + PositionState( + stationary = PositionState.StationaryState.Stationary(confidence = 1f), + orientation = PositionState.OrientationState.Postured(confidence = 1f), + ) + ) + + advanceTimeBySlidingWindowAndRun() assertThat(postured).isTrue() + + // If we detect motion, we should transition back to not postured. + triggerSensor(sensor) + assertThat(postured).isFalse() + + advanceTimeBy(9999.hours) + assertThat(postured).isFalse() } @Test @@ -55,12 +230,51 @@ class PosturingInteractorTest : SysuiTestCase() { assertThat(postured).isFalse() underTest.setValueForDebug(PosturedState.NotPostured) - posturingRepository.fake.setPosturedState(PosturedState.Postured(1f)) + posturingRepository.fake.emitPositionState( + PositionState( + stationary = PositionState.StationaryState.Stationary(confidence = 1f), + orientation = PositionState.OrientationState.Postured(confidence = 1f), + ) + ) // Repository value is overridden by debug value assertThat(postured).isFalse() underTest.setValueForDebug(PosturedState.Unknown) + + advanceTimeBySlidingWindowAndRun() assertThat(postured).isTrue() } + + private fun Kosmos.stubSensorManager(): (sensor: Sensor) -> Unit { + val callbacks = mutableMapOf<Sensor, List<TriggerEventListener>>() + val pickupSensor = mock<Sensor>() + val motionSensor = mock<Sensor>() + + asyncSensorManager.stub { + on { getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE) } doReturn pickupSensor + on { getDefaultSensor(Sensor.TYPE_SIGNIFICANT_MOTION) } doReturn motionSensor + on { requestTriggerSensor(any(), any()) } doAnswer + { + val callback = it.arguments[0] as TriggerEventListener + val sensor = it.arguments[1] as Sensor + callbacks[sensor] = callbacks.getOrElse(sensor) { emptyList() } + callback + true + } + on { cancelTriggerSensor(any(), any()) } doAnswer + { + val callback = it.arguments[0] as TriggerEventListener + val sensor = it.arguments[1] as Sensor + callbacks[sensor] = callbacks.getOrElse(sensor) { emptyList() } - callback + true + } + } + + return { sensor: Sensor -> + val list = callbacks.getOrElse(sensor) { emptyList() } + // Simulate a trigger sensor which unregisters callbacks after triggering. + callbacks[sensor] = emptyList() + list.forEach { it.onTrigger(mock()) } + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt index b42eddd12e4e..fd99313a17b7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt @@ -36,7 +36,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.test.filters.SmallTest -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.app.viewcapture.ViewCaptureFactory import com.android.compose.animation.scene.ObservableTransitionState import com.android.internal.logging.UiEventLogger @@ -143,7 +142,6 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() { mock<ScrimManager> { on { currentController }.thenReturn(mScrimController) } private val mSystemDialogsCloser = mock<SystemDialogsCloser>() private val mDreamOverlayCallbackController = mock<DreamOverlayCallbackController>() - private val mLazyViewCapture = lazy { viewCaptureSpy } private val mViewCaptor = argumentCaptor<View>() private val mTouchHandlersCaptor = argumentCaptor<Set<TouchHandler>>() @@ -156,7 +154,6 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() { private val gestureInteractor = spy(kosmos.gestureInteractor) private lateinit var mCommunalInteractor: CommunalInteractor - private lateinit var mViewCaptureAwareWindowManager: ViewCaptureAwareWindowManager private lateinit var environmentComponents: EnvironmentComponents private lateinit var mService: DreamOverlayService @@ -244,18 +241,12 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() { mComplicationComponentFactory, mAmbientTouchComponentFactory, ) - mViewCaptureAwareWindowManager = - ViewCaptureAwareWindowManager( - mWindowManager, - mLazyViewCapture, - isViewCaptureEnabled = false, - ) mService = DreamOverlayService( mContext, mLifecycleOwner, mMainExecutor, - mViewCaptureAwareWindowManager, + mWindowManager, mComplicationComponentFactory, mDreamComplicationComponentFactory, mDreamOverlayComponentFactory, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt index 63229dbb47a4..8fdbf9b45d2e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt @@ -229,7 +229,6 @@ class FromAlternateBouncerTransitionInteractorTest(flags: FlagsParameterization) } @Test - @EnableFlags(FLAG_GLANCEABLE_HUB_V2) fun transitionToDreaming() = kosmos.runTest { fakePowerRepository.updateWakefulness( 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/media/controls/data/repository/MediaFilterRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt index 8a11be0ab8fa..b1427f21345c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt @@ -219,17 +219,13 @@ class MediaFilterRepositoryTest : SysuiTestCase() { underTest.addSelectedUserMediaEntry(playingData1) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId1)) underTest.addSelectedUserMediaEntry(playingData2) - underTest.addMediaDataLoadingState( - MediaDataLoadingModel.Loaded(playingInstanceId2, false) - ) + underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId2)) assertThat(currentMedia?.size).isEqualTo(2) assertThat(currentMedia) .containsExactly( MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId1)), - MediaCommonModel.MediaControl( - MediaDataLoadingModel.Loaded(playingInstanceId2, false) - ), + MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId2)), ) .inOrder() @@ -238,9 +234,7 @@ class MediaFilterRepositoryTest : SysuiTestCase() { assertThat(currentMedia?.size).isEqualTo(2) assertThat(currentMedia) .containsExactly( - MediaCommonModel.MediaControl( - MediaDataLoadingModel.Loaded(playingInstanceId2, false) - ), + MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId2)), MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId1)), ) .inOrder() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/util/MediaDiffUtilTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/util/MediaDiffUtilTest.kt index faa62c2febc1..45128e23c900 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/util/MediaDiffUtilTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/util/MediaDiffUtilTest.kt @@ -21,7 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId import com.android.systemui.SysuiTestCase -import com.android.systemui.media.controls.ui.viewmodel.MediaCommonViewModel +import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel import com.android.systemui.media.controls.ui.viewmodel.mediaControlViewModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -37,8 +37,8 @@ class MediaDiffUtilTest : SysuiTestCase() { @Test fun newMediaControlAdded() { - val mediaControl = createMediaControl(InstanceId.fakeInstanceId(123), true) - val oldList = listOf<MediaCommonViewModel>() + val mediaControl = createMediaControl(InstanceId.fakeInstanceId(123)) + val oldList = listOf<MediaControlViewModel>() val newList = listOf(mediaControl) val mediaLoadedCallback = MediaViewModelCallback(oldList, newList) val mediaLoadedListUpdateCallback = @@ -56,18 +56,18 @@ class MediaDiffUtilTest : SysuiTestCase() { @Test fun updateMediaControl_contentChanged() { - val mediaControl = createMediaControl(InstanceId.fakeInstanceId(123), true) + val mediaControl = createMediaControl(InstanceId.fakeInstanceId(123)) val oldList = listOf(mediaControl) - val newList = listOf(mediaControl.copy(immediatelyUpdateUi = false)) + val newList = listOf(mediaControl.copy()) val mediaLoadedCallback = MediaViewModelCallback(oldList, newList) val mediaLoadedListUpdateCallback = MediaViewModelListUpdateCallback( oldList, newList, - { commonViewModel, _ -> fail("Unexpected to add $commonViewModel") }, - { commonViewModel, _ -> assertThat(commonViewModel).isNotEqualTo(mediaControl) }, + { controlViewModel, _ -> fail("Unexpected to add $controlViewModel") }, + { controlViewModel, _ -> assertThat(controlViewModel).isNotEqualTo(mediaControl) }, { fail("Unexpected to remove $it") }, - { commonViewModel, _, _ -> fail("Unexpected to move $commonViewModel ") }, + { controlViewModel, _, _ -> fail("Unexpected to move $controlViewModel ") }, ) DiffUtil.calculateDiff(mediaLoadedCallback).dispatchUpdatesTo(mediaLoadedListUpdateCallback) @@ -75,8 +75,8 @@ class MediaDiffUtilTest : SysuiTestCase() { @Test fun mediaControlMoved() { - val mediaControl1 = createMediaControl(InstanceId.fakeInstanceId(123), true) - val mediaControl2 = createMediaControl(InstanceId.fakeInstanceId(456), false) + val mediaControl1 = createMediaControl(InstanceId.fakeInstanceId(123)) + val mediaControl2 = createMediaControl(InstanceId.fakeInstanceId(456)) val oldList = listOf(mediaControl1, mediaControl2) val newList = listOf(mediaControl2, mediaControl1) val mediaLoadedCallback = MediaViewModelCallback(oldList, newList) @@ -84,10 +84,10 @@ class MediaDiffUtilTest : SysuiTestCase() { MediaViewModelListUpdateCallback( oldList, newList, - { commonViewModel, _ -> fail("Unexpected to add $commonViewModel") }, - { commonViewModel, _ -> fail("Unexpected to update $commonViewModel") }, + { controlViewModel, _ -> fail("Unexpected to add $controlViewModel") }, + { controlViewModel, _ -> fail("Unexpected to update $controlViewModel") }, { fail("Unexpected to remove $it") }, - { commonViewModel, _, _ -> assertThat(commonViewModel).isEqualTo(mediaControl1) }, + { controlViewModel, _, _ -> assertThat(controlViewModel).isEqualTo(mediaControl1) }, ) DiffUtil.calculateDiff(mediaLoadedCallback).dispatchUpdatesTo(mediaLoadedListUpdateCallback) @@ -95,34 +95,24 @@ class MediaDiffUtilTest : SysuiTestCase() { @Test fun mediaControlRemoved() { - val mediaControl = createMediaControl(InstanceId.fakeInstanceId(123), true) + val mediaControl = createMediaControl(InstanceId.fakeInstanceId(123)) val oldList = listOf(mediaControl) - val newList = listOf<MediaCommonViewModel>() + val newList = listOf<MediaControlViewModel>() val mediaLoadedCallback = MediaViewModelCallback(oldList, newList) val mediaLoadedListUpdateCallback = MediaViewModelListUpdateCallback( oldList, newList, - { commonViewModel, _ -> fail("Unexpected to add $commonViewModel") }, - { commonViewModel, _ -> fail("Unexpected to update $commonViewModel") }, - { commonViewModel -> assertThat(commonViewModel).isEqualTo(mediaControl) }, - { commonViewModel, _, _ -> fail("Unexpected to move $commonViewModel ") }, + { controlViewModel, _ -> fail("Unexpected to add $controlViewModel") }, + { controlViewModel, _ -> fail("Unexpected to update $controlViewModel") }, + { controlViewModel -> assertThat(controlViewModel).isEqualTo(mediaControl) }, + { controlViewModel, _, _ -> fail("Unexpected to move $controlViewModel ") }, ) DiffUtil.calculateDiff(mediaLoadedCallback).dispatchUpdatesTo(mediaLoadedListUpdateCallback) } - private fun createMediaControl( - instanceId: InstanceId, - immediatelyUpdateUi: Boolean, - ): MediaCommonViewModel.MediaControl { - return MediaCommonViewModel.MediaControl( - instanceId = instanceId, - immediatelyUpdateUi = immediatelyUpdateUi, - controlViewModel = kosmos.mediaControlViewModel, - onAdded = {}, - onRemoved = {}, - onUpdated = {}, - ) + private fun createMediaControl(instanceId: InstanceId): MediaControlViewModel { + return kosmos.mediaControlViewModel.copy(instanceId = instanceId) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelTest.kt index e56b114dc847..fab5a3cdf8fd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelTest.kt @@ -86,23 +86,23 @@ class MediaCarouselViewModelTest : SysuiTestCase() { loadMediaControl(KEY_2, instanceId2, isPlaying = true) loadMediaControl(KEY, instanceId1, isPlaying = false) - var mediaControl2 = sortedMedia?.get(0) as MediaCommonViewModel.MediaControl - var mediaControl1 = sortedMedia?.get(1) as MediaCommonViewModel.MediaControl + var mediaControl2 = sortedMedia?.get(0) as MediaControlViewModel + var mediaControl1 = sortedMedia?.get(1) as MediaControlViewModel assertThat(mediaControl2.instanceId).isEqualTo(instanceId2) assertThat(mediaControl1.instanceId).isEqualTo(instanceId1) loadMediaControl(KEY, instanceId1, isPlaying = true) loadMediaControl(KEY_2, instanceId2, isPlaying = false) - mediaControl2 = sortedMedia?.get(0) as MediaCommonViewModel.MediaControl - mediaControl1 = sortedMedia?.get(1) as MediaCommonViewModel.MediaControl + mediaControl2 = sortedMedia?.get(0) as MediaControlViewModel + mediaControl1 = sortedMedia?.get(1) as MediaControlViewModel assertThat(mediaControl2.instanceId).isEqualTo(instanceId2) assertThat(mediaControl1.instanceId).isEqualTo(instanceId1) underTest.onReorderingAllowed() - mediaControl1 = sortedMedia?.get(0) as MediaCommonViewModel.MediaControl - mediaControl2 = sortedMedia?.get(1) as MediaCommonViewModel.MediaControl + mediaControl1 = sortedMedia?.get(0) as MediaControlViewModel + mediaControl2 = sortedMedia?.get(1) as MediaControlViewModel assertThat(mediaControl1.instanceId).isEqualTo(instanceId1) assertThat(mediaControl2.instanceId).isEqualTo(instanceId2) } @@ -115,7 +115,7 @@ class MediaCarouselViewModelTest : SysuiTestCase() { loadMediaControl(KEY, instanceId) - val mediaControl = sortedMedia?.get(0) as MediaCommonViewModel.MediaControl + val mediaControl = sortedMedia?.get(0) as MediaControlViewModel assertThat(mediaControl.instanceId).isEqualTo(instanceId) // when media control is added to carousel diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt index 55e52b780488..18b14a4b3752 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt @@ -55,8 +55,6 @@ class TaskPreviewSizeProviderTest : SysuiTestCase() { @Before fun setup() { - whenever(mockContext.getSystemService(eq(WindowManager::class.java))) - .thenReturn(windowManager) whenever(mockContext.resources).thenReturn(resources) } @@ -154,7 +152,8 @@ class TaskPreviewSizeProviderTest : SysuiTestCase() { return TaskPreviewSizeProvider( mockContext, windowMetricsProvider, - testConfigurationController + testConfigurationController, + windowManager ) .also { it.addCallback(listener) } } 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/navigationbar/views/NavigationBarTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java index 09e49eb217b0..0e8c5079579e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java @@ -82,7 +82,6 @@ import android.view.inputmethod.InputMethodManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.systemui.SysuiTestCase; @@ -218,8 +217,6 @@ public class NavigationBarTest extends SysuiTestCase { @Mock private WindowManager mWindowManager; @Mock - private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; - @Mock private TelecomManager mTelecomManager; @Mock private InputMethodManager mInputMethodManager; @@ -685,7 +682,6 @@ public class NavigationBarTest extends SysuiTestCase { null, context, mWindowManager, - mViewCaptureAwareWindowManager, () -> mAssistManager, mock(AccessibilityManager.class), deviceProvisionedController, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt index 52a0a5445002..7fb66ce21919 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt @@ -22,8 +22,7 @@ import androidx.test.filters.SmallTest import com.android.compose.animation.scene.Back import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserActionResult.HideOverlay -import com.android.compose.animation.scene.UserActionResult.ShowOverlay -import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays +import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.EnableSceneContainer @@ -77,10 +76,8 @@ class NotificationsShadeOverlayActionsViewModelTest : SysuiTestCase() { underTest.activateIn(this) val action = - (actions?.get(Swipe.Down(fromSource = SceneContainerArea.EndHalf)) as? ShowOverlay) + (actions?.get(Swipe.Down(fromSource = SceneContainerArea.TopEdgeEndHalf)) + as? ReplaceByOverlay) assertThat(action?.overlay).isEqualTo(Overlays.QuickSettingsShade) - val overlaysToHide = action?.hideCurrentOverlays as? HideCurrentOverlays.Some - assertThat(overlaysToHide).isNotNull() - assertThat(overlaysToHide?.overlays).containsExactly(Overlays.NotificationsShade) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt index 80ce43d61003..ffcd95bc7a4a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt @@ -157,7 +157,7 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() { } @Test - fun showMedia_noActiveMedia_false() = + fun showMedia_InactiveMedia_false() = testScope.runTest { kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = false)) runCurrent() @@ -166,6 +166,16 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() { } @Test + fun showMedia_noMedia_false() = + testScope.runTest { + kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = true)) + kosmos.mediaFilterRepository.clearSelectedUserMedia() + runCurrent() + + assertThat(underTest.showMedia).isFalse() + } + + @Test fun showMedia_qsDisabled_false() = testScope.runTest { kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = true)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/power/PowerUITest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/power/PowerUITest.java index 338ed7596bd6..70ded2a7f21c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/power/PowerUITest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/power/PowerUITest.java @@ -40,11 +40,11 @@ import android.service.vr.IVrStateCallbacks; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; import android.testing.TestableResources; +import android.view.WindowManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCapture; import com.android.settingslib.fuelgauge.Estimate; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -54,8 +54,6 @@ import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; -import dagger.Lazy; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -96,7 +94,7 @@ public class PowerUITest extends SysuiTestCase { @Mock private BroadcastDispatcher mBroadcastDispatcher; @Mock private CommandQueue mCommandQueue; @Mock private IVrManager mVrManager; - @Mock private Lazy<ViewCapture> mLazyViewCapture; + @Mock private WindowManager mWindowManager; @Before public void setup() { @@ -709,7 +707,7 @@ public class PowerUITest extends SysuiTestCase { mWakefulnessLifecycle, mPowerManager, mUserTracker, - mLazyViewCapture); + mWindowManager); mPowerUI.mThermalService = mThermalServiceMock; } 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/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/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt index 701e55d0759d..d11701570e32 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt @@ -95,11 +95,21 @@ class QuickSettingsContainerViewModelTest : SysuiTestCase() { } @Test - fun showMedia_noActiveMedia_false() = + fun showMedia_InactiveMedia_true() = testScope.runTest { kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = false)) runCurrent() + assertThat(underTest.showMedia).isTrue() + } + + @Test + fun showMedia_noMedia_false() = + testScope.runTest { + kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = true)) + kosmos.mediaFilterRepository.clearSelectedUserMedia() + runCurrent() + assertThat(underTest.showMedia).isFalse() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt index b98059a1fe90..882d296d94b4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt @@ -22,8 +22,7 @@ import androidx.test.filters.SmallTest import com.android.compose.animation.scene.Back import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserActionResult.HideOverlay -import com.android.compose.animation.scene.UserActionResult.ShowOverlay -import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays +import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.EnableSceneContainer @@ -90,11 +89,8 @@ class QuickSettingsShadeOverlayActionsViewModelTest : SysuiTestCase() { underTest.activateIn(this) val action = - (actions?.get(Swipe.Down(fromSource = SceneContainerArea.StartHalf)) - as? ShowOverlay) + (actions?.get(Swipe.Down(fromSource = SceneContainerArea.TopEdgeStartHalf)) + as? ReplaceByOverlay) assertThat(action?.overlay).isEqualTo(Overlays.NotificationsShade) - val overlaysToHide = action?.hideCurrentOverlays as? HideCurrentOverlays.Some - assertThat(overlaysToHide).isNotNull() - assertThat(overlaysToHide?.overlays).containsExactly(Overlays.QuickSettingsShade) } } 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/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt index a09e5cd9de9b..7536bb3cc02a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt @@ -32,6 +32,8 @@ import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.LeftE import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.LeftHalf import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.RightEdge import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.RightHalf +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.TopEdgeLeftHalf +import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.TopEdgeRightHalf import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.StartEdge import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.StartHalf import com.google.common.truth.Truth.assertThat @@ -55,9 +57,9 @@ class SceneContainerSwipeDetectorTest : SysuiTestCase() { } @Test - fun source_swipeVerticallyOnTopLeft_detectsLeftHalf() { + fun source_swipeVerticallyOnTopLeft_detectsTopEdgeLeftHalf() { val detectedEdge = swipeVerticallyFrom(x = 1, y = edgeSize - 1) - assertThat(detectedEdge).isEqualTo(LeftHalf) + assertThat(detectedEdge).isEqualTo(TopEdgeLeftHalf) } @Test @@ -69,7 +71,7 @@ class SceneContainerSwipeDetectorTest : SysuiTestCase() { @Test fun source_swipeVerticallyOnTopRight_detectsRightHalf() { val detectedEdge = swipeVerticallyFrom(x = screenWidth - 1, y = edgeSize - 1) - assertThat(detectedEdge).isEqualTo(RightHalf) + assertThat(detectedEdge).isEqualTo(TopEdgeRightHalf) } @Test @@ -79,14 +81,26 @@ class SceneContainerSwipeDetectorTest : SysuiTestCase() { } @Test - fun source_swipeVerticallyToLeftOfSplit_detectsLeftHalf() { + fun source_swipeVerticallyOnTopEdge_ToLeftOfSplit_detectsTopEdgeLeftHalf() { val detectedEdge = swipeVerticallyFrom(x = (screenWidth / 2) - 1, y = edgeSize - 1) + assertThat(detectedEdge).isEqualTo(TopEdgeLeftHalf) + } + + @Test + fun source_swipeVerticallyBelowTopEdge_ToLeftOfSplit_detectsLeftHalf() { + val detectedEdge = swipeVerticallyFrom(x = (screenWidth / 2) - 1, y = edgeSize + 1) assertThat(detectedEdge).isEqualTo(LeftHalf) } @Test - fun source_swipeVerticallyToRightOfSplit_detectsRightHalf() { + fun source_swipeVerticallyOnTopEdge_toRightOfSplit_detectsTopEdgeRightHalf() { val detectedEdge = swipeVerticallyFrom(x = (screenWidth / 2) + 1, y = edgeSize - 1) + assertThat(detectedEdge).isEqualTo(TopEdgeRightHalf) + } + + @Test + fun source_swipeVerticallyBelowTopEdge_toRightOfSplit_detectsRightHalf() { + val detectedEdge = swipeVerticallyFrom(x = (screenWidth / 2) + 1, y = edgeSize + 1) assertThat(detectedEdge).isEqualTo(RightHalf) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 94db429c2225..676e1ea5321a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -172,6 +172,7 @@ import com.android.systemui.user.domain.interactor.UserSwitcherInteractor; import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.util.time.SystemClock; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import com.android.systemui.wallpapers.ui.viewmodel.WallpaperFocalAreaViewModel; import com.android.wm.shell.animation.FlingAnimationUtils; @@ -296,6 +297,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Mock private LargeScreenHeaderHelper mLargeScreenHeaderHelper; @Mock private StatusBarLongPressGestureDetector mStatusBarLongPressGestureDetector; @Mock protected SysUIStateDisplaysInteractor mSysUIStateDisplaysInteractor; + @Mock private WindowManagerProvider mWindowManagerProvider; protected final int mMaxUdfpsBurnInOffsetY = 5; protected FakeFeatureFlagsClassic mFeatureFlags = new FakeFeatureFlagsClassic(); protected KeyguardClockInteractor mKeyguardClockInteractor; @@ -672,7 +674,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mCastController, new ResourcesSplitShadeStateController(), () -> mKosmos.getCommunalTransitionViewModel(), - () -> mLargeScreenHeaderHelper + () -> mLargeScreenHeaderHelper, + mWindowManagerProvider ); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index 3788049256a2..fd17b7a32fd3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -50,7 +50,6 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.colorextraction.ColorExtractor; import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; @@ -100,7 +99,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { @Rule public final CheckFlagsRule checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); - @Mock private ViewCaptureAwareWindowManager mWindowManager; + @Mock private WindowManager mWindowManager; @Mock private DozeParameters mDozeParameters; @Spy private final NotificationShadeWindowView mNotificationShadeWindowView = spy( new NotificationShadeWindowView(mContext, null)); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java index 7433267ab3b0..db0c07c50dc6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java @@ -28,6 +28,7 @@ import android.os.Handler; import android.os.Looper; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; import com.android.internal.logging.MetricsLogger; @@ -79,6 +80,7 @@ import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupReposi import com.android.systemui.user.domain.interactor.SelectedUserInteractor; import com.android.systemui.user.domain.interactor.UserSwitcherInteractor; import com.android.systemui.util.kotlin.JavaAdapter; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import dagger.Lazy; @@ -147,6 +149,7 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { @Mock protected UserSwitcherInteractor mUserSwitcherInteractor; @Mock protected SelectedUserInteractor mSelectedUserInteractor; @Mock protected LargeScreenHeaderHelper mLargeScreenHeaderHelper; + @Mock protected WindowManagerProvider mWindowManagerProvider; protected FakeDisableFlagsRepository mDisableFlagsRepository = mKosmos.getFakeDisableFlagsRepository(); @@ -232,6 +235,9 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { mMainHandler = new Handler(Looper.getMainLooper()); + WindowManager wm = mContext.getSystemService(WindowManager.class); + when(mWindowManagerProvider.getWindowManager(mContext)).thenReturn(wm); + mQsController = new QuickSettingsControllerImpl( mPanelViewControllerLazy, mPanelView, @@ -268,7 +274,8 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { mCastController, splitShadeStateController, () -> mKosmos.getCommunalTransitionViewModel(), - () -> mLargeScreenHeaderHelper + () -> mLargeScreenHeaderHelper, + mWindowManagerProvider ); mQsController.init(); 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/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/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 6c498c8fd2f1..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 @@ -799,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/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..233efbb97d4e 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 @@ -56,16 +56,16 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @DisableFlags(StatusBarNotifChips.FLAG_NAME) - fun shownNotificationChips_flagOff_noNotifs() = + fun allNotificationChips_flagOff_noNotifs() = kosmos.runTest { - val latest by collectLastValue(underTest.shownNotificationChips) + val latest by collectLastValue(underTest.allNotificationChips) setNotifs( listOf( activeNotificationModel( key = "notif", statusBarChipIcon = mock<StatusBarIconView>(), - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -75,9 +75,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun shownNotificationChips_noNotifs_empty() = + fun allNotificationChips_noNotifs_empty() = kosmos.runTest { - val latest by collectLastValue(underTest.shownNotificationChips) + val latest by collectLastValue(underTest.allNotificationChips) setNotifs(emptyList()) @@ -87,16 +87,16 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) - fun shownNotificationChips_notifMissingStatusBarChipIconView_cdFlagOff_empty() = + fun allNotificationChips_notifMissingStatusBarChipIconView_cdFlagOff_empty() = kosmos.runTest { - val latest by collectLastValue(underTest.shownNotificationChips) + val latest by collectLastValue(underTest.allNotificationChips) setNotifs( listOf( activeNotificationModel( key = "notif", statusBarChipIcon = null, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -106,16 +106,16 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME, StatusBarConnectedDisplays.FLAG_NAME) - fun shownNotificationChips_notifMissingStatusBarChipIconView_cdFlagOn_notEmpty() = + fun allNotificationChips_notifMissingStatusBarChipIconView_cdFlagOn_notEmpty() = kosmos.runTest { - val latest by collectLastValue(underTest.shownNotificationChips) + val latest by collectLastValue(underTest.allNotificationChips) setNotifs( listOf( activeNotificationModel( key = "notif", statusBarChipIcon = null, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -125,9 +125,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun shownNotificationChips_onePromotedNotif_statusBarIconViewMatches() = + fun allNotificationChips_onePromotedNotif_statusBarIconViewMatches() = kosmos.runTest { - val latest by collectLastValue(underTest.shownNotificationChips) + val latest by collectLastValue(underTest.allNotificationChips) val icon = mock<StatusBarIconView>() setNotifs( @@ -135,7 +135,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = icon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -147,9 +147,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun shownNotificationChips_onlyForPromotedNotifs() = + fun allNotificationChips_onlyForPromotedNotifs() = kosmos.runTest { - val latest by collectLastValue(underTest.shownNotificationChips) + val latest by collectLastValue(underTest.allNotificationChips) val firstIcon = mock<StatusBarIconView>() val secondIcon = mock<StatusBarIconView>() @@ -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", @@ -182,11 +182,11 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun shownNotificationChips_onlyForNotVisibleApps() = + fun allNotificationChips_appVisibilityInfoCorrect() = kosmos.runTest { activityManagerRepository.fake.startingIsAppVisibleValue = false - val latest by collectLastValue(underTest.shownNotificationChips) + val latest by collectLastValue(underTest.allNotificationChips) val uid = 433 setNotifs( @@ -195,27 +195,30 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { key = "notif", uid = uid, statusBarChipIcon = mock<StatusBarIconView>(), - promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) activityManagerRepository.fake.setIsAppVisible(uid, isAppVisible = false) assertThat(latest).hasSize(1) + assertThat(latest!![0].isAppVisible).isFalse() activityManagerRepository.fake.setIsAppVisible(uid, isAppVisible = true) - assertThat(latest).isEmpty() + assertThat(latest).hasSize(1) + assertThat(latest!![0].isAppVisible).isTrue() activityManagerRepository.fake.setIsAppVisible(uid, isAppVisible = false) assertThat(latest).hasSize(1) + assertThat(latest!![0].isAppVisible).isFalse() } /** Regression test for b/388521980. */ @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun shownNotificationChips_callNotifIsAlsoPromoted_callNotifExcluded() = + fun allNotificationChips_callNotifIsAlsoPromoted_callNotifExcluded() = kosmos.runTest { - val latest by collectLastValue(underTest.shownNotificationChips) + val latest by collectLastValue(underTest.allNotificationChips) setNotifs( listOf( @@ -223,14 +226,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, ), ) @@ -243,9 +246,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun shownNotificationChips_notifUpdatesGoThrough() = + fun allNotificationChips_notifUpdatesGoThrough() = kosmos.runTest { - val latest by collectLastValue(underTest.shownNotificationChips) + val latest by collectLastValue(underTest.allNotificationChips) val firstIcon = mock<StatusBarIconView>() val secondIcon = mock<StatusBarIconView>() @@ -256,7 +259,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = firstIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -269,7 +272,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = secondIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -282,7 +285,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = thirdIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -293,16 +296,16 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun shownNotificationChips_promotedNotifDisappearsThenReappears() = + fun allNotificationChips_promotedNotifDisappearsThenReappears() = kosmos.runTest { - val latest by collectLastValue(underTest.shownNotificationChips) + val latest by collectLastValue(underTest.allNotificationChips) setNotifs( listOf( activeNotificationModel( key = "notif", statusBarChipIcon = mock(), - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -325,7 +328,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = mock(), - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -335,9 +338,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun shownNotificationChips_sortedByFirstAppearanceTime() = + fun allNotificationChips_sortedByFirstAppearanceTime() = kosmos.runTest { - val latest by collectLastValue(underTest.shownNotificationChips) + val latest by collectLastValue(underTest.allNotificationChips) val firstIcon = mock<StatusBarIconView>() val secondIcon = mock<StatusBarIconView>() @@ -348,7 +351,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 +362,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif2", statusBarChipIcon = secondIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif2").build(), + promotedContent = PromotedNotificationContentBuilder("notif2").build(), ) setNotifs(listOf(notif1, notif2)) @@ -380,7 +383,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { // WHEN notif1 gets an update val notif1NewPromotedContent = - PromotedNotificationContentModel.Builder("notif1").apply { + PromotedNotificationContentBuilder("notif1").applyToShared { this.shortCriticalText = "Arrived" } setNotifs( @@ -411,9 +414,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun shownNotificationChips_lastAppVisibleTimeMaintainedAcrossNotifAddsAndRemoves() = + fun allNotificationChips_lastAppVisibleTimeMaintainedAcrossNotifAddsAndRemoves() = kosmos.runTest { - val latest by collectLastValue(underTest.shownNotificationChips) + val latest by collectLastValue(underTest.allNotificationChips) val notif1Info = NotifInfo("notif1", mock<StatusBarIconView>(), uid = 100) val notif2Info = NotifInfo("notif2", mock<StatusBarIconView>(), uid = 200) @@ -426,8 +429,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 +445,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) @@ -468,9 +469,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun shownNotificationChips_sortedByLastAppVisibleTime() = + fun allNotificationChips_sortedByLastAppVisibleTime() = kosmos.runTest { - val latest by collectLastValue(underTest.shownNotificationChips) + val latest by collectLastValue(underTest.allNotificationChips) val notif1Info = NotifInfo("notif1", mock<StatusBarIconView>(), uid = 100) val notif2Info = NotifInfo("notif2", mock<StatusBarIconView>(), uid = 200) @@ -482,16 +483,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() @@ -500,15 +499,19 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { fakeSystemClock.advanceTime(1000) activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = true) - // THEN notif2 is no longer shown - assertThat(latest!!.map { it.key }).containsExactly("notif1").inOrder() + // THEN notif2 is ranked above notif1 because it was more recently visible + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() + assertThat(latest!![0].isAppVisible).isTrue() // notif2 + assertThat(latest!![1].isAppVisible).isFalse() // notif1 // WHEN notif2's app is no longer visible fakeSystemClock.advanceTime(1000) activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false) - // THEN notif2 is ranked above notif1 because it was more recently visible + // THEN notif2 is still ranked above notif1 assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() + assertThat(latest!![0].isAppVisible).isFalse() // notif2 + assertThat(latest!![1].isAppVisible).isFalse() // notif1 // WHEN the app associated with notif1 becomes visible then un-visible fakeSystemClock.advanceTime(1000) @@ -522,9 +525,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun shownNotificationChips_newNotificationTakesPriorityOverLastAppVisible() = + fun allNotificationChips_newNotificationTakesPriorityOverLastAppVisible() = kosmos.runTest { - val latest by collectLastValue(underTest.shownNotificationChips) + val latest by collectLastValue(underTest.allNotificationChips) val notif1Info = NotifInfo("notif1", mock<StatusBarIconView>(), uid = 100) val notif2Info = NotifInfo("notif2", mock<StatusBarIconView>(), uid = 200) @@ -537,16 +540,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 +568,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)) @@ -581,9 +581,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun shownNotificationChips_fullSort() = + fun allNotificationChips_fullSort() = kosmos.runTest { - val latest by collectLastValue(underTest.shownNotificationChips) + val latest by collectLastValue(underTest.allNotificationChips) val notif1Info = NotifInfo("notif1", mock<StatusBarIconView>(), uid = 100) val notif2Info = NotifInfo("notif2", mock<StatusBarIconView>(), uid = 200) @@ -597,8 +597,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 +608,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 +635,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { // WHEN notif2 gets an update val notif2NewPromotedContent = - PromotedNotificationContentModel.Builder("notif2").apply { + PromotedNotificationContentBuilder("notif2").applyToShared { this.shortCriticalText = "Arrived" } setNotifs( @@ -662,8 +660,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)) @@ -699,9 +696,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun shownNotificationChips_notifChangesKey() = + fun allNotificationChips_notifChangesKey() = kosmos.runTest { - val latest by collectLastValue(underTest.shownNotificationChips) + val latest by collectLastValue(underTest.allNotificationChips) val firstIcon = mock<StatusBarIconView>() val secondIcon = mock<StatusBarIconView>() @@ -710,8 +707,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { activeNotificationModel( key = "notif|uid1", statusBarChipIcon = firstIcon, - promotedContent = - PromotedNotificationContentModel.Builder("notif|uid1").build(), + promotedContent = PromotedNotificationContentBuilder("notif|uid1").build(), ) ) ) @@ -725,8 +721,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 8368fa671ea8..660f0b52f464 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 @@ -24,6 +24,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY import com.android.systemui.SysuiTestCase +import com.android.systemui.activity.data.repository.activityManagerRepository +import com.android.systemui.activity.data.repository.fake import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository @@ -43,6 +45,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 +104,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = null, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -121,7 +124,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = null, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -142,7 +145,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { key = "notif", appName = "Fake App Name", statusBarChipIcon = icon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -172,7 +175,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { key = notifKey, appName = "Fake App Name", statusBarChipIcon = null, - promotedContent = PromotedNotificationContentModel.Builder(notifKey).build(), + promotedContent = PromotedNotificationContentBuilder(notifKey).build(), ) ) ) @@ -195,7 +198,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 +232,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 +267,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, @@ -286,6 +288,84 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + fun chips_appStartsAsVisible_isHiddenTrue() = + kosmos.runTest { + activityManagerRepository.fake.startingIsAppVisibleValue = true + + val latest by collectLastValue(underTest.chips) + + val uid = 433 + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + uid = uid, + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0].isHidden).isTrue() + } + + @Test + fun chips_appStartsAsNotVisible_isHiddenFalse() = + kosmos.runTest { + activityManagerRepository.fake.startingIsAppVisibleValue = false + + val latest by collectLastValue(underTest.chips) + + val uid = 433 + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + uid = uid, + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0].isHidden).isFalse() + } + + @Test + fun chips_isHidden_changesBasedOnAppVisibility() = + kosmos.runTest { + activityManagerRepository.fake.startingIsAppVisibleValue = false + + val latest by collectLastValue(underTest.chips) + + val uid = 433 + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + uid = uid, + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), + ) + ) + ) + + activityManagerRepository.fake.setIsAppVisible(uid, isAppVisible = false) + assertThat(latest).hasSize(1) + assertThat(latest!![0].isHidden).isFalse() + + activityManagerRepository.fake.setIsAppVisible(uid, isAppVisible = true) + assertThat(latest).hasSize(1) + assertThat(latest!![0].isHidden).isTrue() + + activityManagerRepository.fake.setIsAppVisible(uid, isAppVisible = false) + assertThat(latest).hasSize(1) + assertThat(latest!![0].isHidden).isFalse() + } + + @Test @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_hasShortCriticalText_usesTextInsteadOfTime() = kosmos.runTest { @@ -294,7 +374,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 +401,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 +426,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 +454,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 +482,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.time = When.Time(currentTime + 13.minutes.inWholeMilliseconds) } @@ -422,6 +502,42 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + fun chips_basicTime_respectsIsAppVisible() = + kosmos.runTest { + activityManagerRepository.fake.startingIsAppVisibleValue = false + + val latest by collectLastValue(underTest.chips) + val currentTime = 3.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) + + val promotedContentBuilder = + PromotedNotificationContentBuilder("notif").applyToShared { + this.time = When.Time(currentTime + 13.minutes.inWholeMilliseconds) + } + val uid = 3 + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + uid = 3, + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Active.ShortTimeDelta::class.java) + assertThat(latest!![0].isHidden).isFalse() + + activityManagerRepository.fake.setIsAppVisible(uid = uid, isAppVisible = true) + + assertThat(latest!![0].isHidden).isTrue() + } + + @Test @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_basicTime_timeLessThanOneMinInFuture_isIconOnly() = kosmos.runTest { @@ -430,7 +546,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.time = When.Time(currentTime + 500) } @@ -458,7 +574,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = - PromotedNotificationContentModel.Builder("notif").apply { + PromotedNotificationContentBuilder("notif").applyToShared { this.time = When.Time(currentTime) } @@ -486,7 +602,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 +631,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 +671,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) } @@ -579,6 +695,48 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_countUpTime_respectsIsAppVisible() = + kosmos.runTest { + activityManagerRepository.fake.startingIsAppVisibleValue = true + + val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) + + val currentElapsed = + currentTime + fakeSystemClock.elapsedRealtime() - + fakeSystemClock.currentTimeMillis() + + val whenElapsed = currentElapsed - 1.minutes.inWholeMilliseconds + + val promotedContentBuilder = + PromotedNotificationContentBuilder("notif").applyToShared { + this.time = + When.Chronometer(elapsedRealtimeMillis = whenElapsed, isCountDown = false) + } + val uid = 6 + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + uid = uid, + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java) + assertThat(latest!![0].isHidden).isTrue() + + activityManagerRepository.fake.setIsAppVisible(uid, isAppVisible = false) + + assertThat(latest!![0].isHidden).isFalse() + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_countDownTime_isTimer() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -592,7 +750,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 +781,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 +811,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 +848,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 +896,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 +939,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { activeNotificationModel( key, statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = PromotedNotificationContentModel.Builder(key).build(), + promotedContent = PromotedNotificationContentBuilder(key).build(), ) ) ) @@ -809,7 +967,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { activeNotificationModel( key, statusBarChipIcon = createStatusBarIconViewOrNull(), - promotedContent = PromotedNotificationContentModel.Builder(key).build(), + promotedContent = PromotedNotificationContentBuilder(key).build(), ) ) ) 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 608a84bdb604..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(), ) ) @@ -1178,7 +1155,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) addOngoingCallState(key = callNotificationKey) @@ -1189,7 +1166,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif2", statusBarChipIcon = notifIcon2, - promotedContent = PromotedNotificationContentModel.Builder("notif2").build(), + promotedContent = PromotedNotificationContentBuilder("notif2").build(), ) ) @@ -1214,7 +1191,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) ) @@ -1265,7 +1242,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) @@ -1304,7 +1281,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif", statusBarChipIcon = notifIcon, - promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + promotedContent = PromotedNotificationContentBuilder("notif").build(), ) ) // And everything else hidden @@ -1382,7 +1359,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { activeNotificationModel( key = "notif1", statusBarChipIcon = notif1Icon, - promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), + promotedContent = PromotedNotificationContentBuilder("notif1").build(), ) ) ) @@ -1436,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/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..873357c2201e 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 @@ -21,20 +21,21 @@ import android.service.notification.StatusBarNotification import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest import com.android.systemui.statusbar.RankingBuilder 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 import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -42,17 +43,14 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class RenderNotificationsListInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() - private val testScope = kosmos.testScope - private val notifsRepository = kosmos.activeNotificationListRepository - private val notifsInteractor = kosmos.activeNotificationsInteractor - private val underTest = - RenderNotificationListInteractor(notifsRepository, sectionStyleProvider = mock(), context) + private val Kosmos.outputInteractor by Kosmos.Fixture { activeNotificationsInteractor } + private val Kosmos.underTest by Kosmos.Fixture { renderNotificationListInteractor } @Test fun setRenderedList_preservesOrdering() = - testScope.runTest { - val notifs by collectLastValue(notifsInteractor.topLevelRepresentativeNotifications) + kosmos.runTest { + val notifs by collectLastValue(outputInteractor.topLevelRepresentativeNotifications) val keys = (1..50).shuffled().map { "$it" } val entries = keys.map { mockNotificationEntry(key = it) } underTest.setRenderedList(entries) @@ -64,8 +62,8 @@ class RenderNotificationsListInteractorTest : SysuiTestCase() { @Test fun setRenderList_flatMapsRankings() = - testScope.runTest { - val ranks by collectLastValue(notifsInteractor.activeNotificationRanks) + kosmos.runTest { + val ranks by collectLastValue(outputInteractor.activeNotificationRanks) val single = mockNotificationEntry("single", 0) val group = @@ -89,8 +87,8 @@ class RenderNotificationsListInteractorTest : SysuiTestCase() { @Test fun setRenderList_singleItems_mapsRankings() = - testScope.runTest { - val actual by collectLastValue(notifsInteractor.activeNotificationRanks) + kosmos.runTest { + val actual by collectLastValue(outputInteractor.activeNotificationRanks) val expected = (0..10).shuffled().mapIndexed { index, value -> "$value" to index }.toMap() @@ -103,8 +101,8 @@ class RenderNotificationsListInteractorTest : SysuiTestCase() { @Test fun setRenderList_groupWithNoSummary_flatMapsRankings() = - testScope.runTest { - val actual by collectLastValue(notifsInteractor.activeNotificationRanks) + kosmos.runTest { + val actual by collectLastValue(outputInteractor.activeNotificationRanks) val expected = (0..10).shuffled().mapIndexed { index, value -> "$value" to index }.toMap() @@ -123,14 +121,14 @@ class RenderNotificationsListInteractorTest : SysuiTestCase() { @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME) fun setRenderList_setsPromotionContent() = - testScope.runTest { - val actual by collectLastValue(notifsInteractor.topLevelRepresentativeNotifications) + kosmos.runTest { + val actual by collectLastValue(outputInteractor.topLevelRepresentativeNotifications) val notPromoted1 = mockNotificationEntry("key1", promotedContent = null) val promoted2 = mockNotificationEntry( "key2", - promotedContent = PromotedNotificationContentModel.Builder("key2").build(), + promotedContent = PromotedNotificationContentBuilder("key2").build(), ) underTest.setRenderedList(listOf(notPromoted1, promoted2)) @@ -149,7 +147,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 +163,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 873ab5bc9c2c..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 @@ -44,7 +44,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.domain.interactor.renderNotificationListInteractor 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.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState import com.android.systemui.testKosmos @@ -562,14 +562,14 @@ class PromotedNotificationsInteractorTest : 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(), ) ) @@ -608,14 +608,14 @@ class PromotedNotificationsInteractorTest : 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(), ) ) @@ -643,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 @@ -663,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/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/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/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryKairosAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryKairosAdapterTest.kt new file mode 100644 index 000000000000..3cf787d50d2f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryKairosAdapterTest.kt @@ -0,0 +1,70 @@ +/* + * 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.data.repository.prod + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.activated +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.launchKairosNetwork +import com.android.systemui.kairos.stateOf +import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig +import com.android.systemui.statusbar.pipeline.mobile.data.model.testCarrierConfig +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import org.junit.runner.RunWith +import org.mockito.Mockito + +@OptIn(ExperimentalKairosApi::class, ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class CarrierMergedConnectionRepositoryKairosAdapterTest : + CarrierMergedConnectionRepositoryTestBase() { + + var job: Job? = null + val kairosNetwork = testScope.backgroundScope.launchKairosNetwork() + + override fun recreateRepo(): MobileConnectionRepositoryKairosAdapter { + lateinit var adapter: MobileConnectionRepositoryKairosAdapter + job?.cancel() + Mockito.clearInvocations(telephonyManager) + job = + testScope.backgroundScope.launch { + kairosNetwork.activateSpec { + val repo = activated { + CarrierMergedConnectionRepositoryKairos( + SUB_ID, + logger, + telephonyManager, + wifiRepository, + isInEcmMode = stateOf(false), + ) + } + adapter = + MobileConnectionRepositoryKairosAdapter( + repo, + SystemUiCarrierConfig(SUB_ID, testCarrierConfig()), + ) + Unit + } + } + testScope.runCurrent() // ensure the lateinit is set + return adapter + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt index 8e55f2e9a31a..8a6829cf5d21 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel @@ -43,16 +44,30 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) -class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { +class CarrierMergedConnectionRepositoryTest : CarrierMergedConnectionRepositoryTestBase() { + override fun recreateRepo() = + CarrierMergedConnectionRepository( + SUB_ID, + logger, + telephonyManager, + testScope.backgroundScope.coroutineContext, + testScope.backgroundScope, + wifiRepository, + ) +} + +abstract class CarrierMergedConnectionRepositoryTestBase : SysuiTestCase() { - private lateinit var underTest: CarrierMergedConnectionRepository + protected lateinit var underTest: MobileConnectionRepository - private lateinit var wifiRepository: FakeWifiRepository - @Mock private lateinit var logger: TableLogBuffer - @Mock private lateinit var telephonyManager: TelephonyManager + protected lateinit var wifiRepository: FakeWifiRepository + @Mock protected lateinit var logger: TableLogBuffer + @Mock protected lateinit var telephonyManager: TelephonyManager - private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(testDispatcher) + protected val testDispatcher = UnconfinedTestDispatcher() + protected val testScope = TestScope(testDispatcher) + + abstract fun recreateRepo(): MobileConnectionRepository @Before fun setUp() { @@ -62,15 +77,7 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { wifiRepository = FakeWifiRepository() - underTest = - CarrierMergedConnectionRepository( - SUB_ID, - logger, - telephonyManager, - testScope.backgroundScope.coroutineContext, - testScope.backgroundScope, - wifiRepository, - ) + underTest = recreateRepo() } @Test @@ -121,10 +128,7 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { wifiRepository.setIsWifiDefault(true) wifiRepository.setWifiNetwork( - WifiNetworkModel.CarrierMerged.of( - subscriptionId = SUB_ID, - level = 3, - ) + WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3) ) assertThat(latest).isEqualTo(3) @@ -141,26 +145,17 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { wifiRepository.setIsWifiEnabled(true) wifiRepository.setIsWifiDefault(true) wifiRepository.setWifiNetwork( - WifiNetworkModel.CarrierMerged.of( - subscriptionId = SUB_ID, - level = 3, - ) + WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3) ) wifiRepository.setWifiActivity( - DataActivityModel( - hasActivityIn = true, - hasActivityOut = false, - ) + DataActivityModel(hasActivityIn = true, hasActivityOut = false) ) assertThat(latest!!.hasActivityIn).isTrue() assertThat(latest!!.hasActivityOut).isFalse() wifiRepository.setWifiActivity( - DataActivityModel( - hasActivityIn = false, - hasActivityOut = true, - ) + DataActivityModel(hasActivityIn = false, hasActivityOut = true) ) assertThat(latest!!.hasActivityIn).isFalse() @@ -178,10 +173,7 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { val typeJob = underTest.resolvedNetworkType.onEach { latestType = it }.launchIn(this) wifiRepository.setWifiNetwork( - WifiNetworkModel.CarrierMerged.of( - subscriptionId = SUB_ID + 10, - level = 3, - ) + WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID + 10, level = 3) ) assertThat(latestLevel).isNotEqualTo(3) @@ -199,10 +191,7 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this) wifiRepository.setWifiNetwork( - WifiNetworkModel.CarrierMerged.of( - subscriptionId = SUB_ID, - level = 3, - ) + WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3) ) wifiRepository.setIsWifiEnabled(false) @@ -219,10 +208,7 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this) wifiRepository.setWifiNetwork( - WifiNetworkModel.CarrierMerged.of( - subscriptionId = SUB_ID, - level = 3, - ) + WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3) ) wifiRepository.setIsWifiDefault(false) @@ -280,6 +266,7 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { fun networkName_usesSimOperatorNameAsInitial() = testScope.runTest { whenever(telephonyManager.simOperatorName).thenReturn("Test SIM name") + underTest = recreateRepo() var latest: NetworkNameModel? = null val job = underTest.networkName.onEach { latest = it }.launchIn(this) @@ -293,6 +280,10 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { fun networkName_updatesOnNetworkUpdate() = testScope.runTest { whenever(telephonyManager.simOperatorName).thenReturn("Test SIM name") + underTest = recreateRepo() + + wifiRepository.setIsWifiEnabled(true) + wifiRepository.setIsWifiDefault(true) var latest: NetworkNameModel? = null val job = underTest.networkName.onEach { latest = it }.launchIn(this) @@ -301,10 +292,7 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { whenever(telephonyManager.simOperatorName).thenReturn("New SIM name") wifiRepository.setWifiNetwork( - WifiNetworkModel.CarrierMerged.of( - subscriptionId = SUB_ID, - level = 3, - ) + WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3) ) assertThat(latest).isEqualTo(NetworkNameModel.SimDerived("New SIM name")) @@ -320,7 +308,7 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { assertThat(latest).isTrue() } - private companion object { + companion object { const val SUB_ID = 123 } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapterTest.kt new file mode 100644 index 000000000000..e72d0c27e632 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapterTest.kt @@ -0,0 +1,141 @@ +/* + * 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.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.SignalIcon +import com.android.settingslib.mobile.MobileIconCarrierIdOverrides +import com.android.systemui.activated +import com.android.systemui.kairos.BuildScope +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.Incremental +import com.android.systemui.kairos.State +import com.android.systemui.kairos.asIncremental +import com.android.systemui.kairos.buildSpec +import com.android.systemui.kairos.combine +import com.android.systemui.kairos.launchKairosNetwork +import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorKairosAdapterTest.Companion.wrapRepo +import com.android.systemui.util.mockito.mock +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import org.junit.runner.RunWith + +@OptIn(ExperimentalKairosApi::class, ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class MobileIconInteractorKairosAdapterTest : MobileIconInteractorTestBase() { + + var job: Job? = null + val kairosNetwork = testScope.backgroundScope.launchKairosNetwork() + + override fun createInteractor(overrides: MobileIconCarrierIdOverrides): MobileIconInteractor { + lateinit var result: MobileIconInteractor + job?.cancel() + job = + testScope.backgroundScope.launch { + kairosNetwork.activateSpec { + val wrapped = wrap(mobileIconsInteractor) + result = + MobileIconInteractorKairosAdapter( + kairosImpl = + activated { + MobileIconInteractorKairosImpl( + defaultSubscriptionHasDataEnabled = + wrapped.activeDataConnectionHasDataEnabled, + alwaysShowDataRatIcon = wrapped.alwaysShowDataRatIcon, + alwaysUseCdmaLevel = wrapped.alwaysUseCdmaLevel, + isSingleCarrier = wrapped.isSingleCarrier, + mobileIsDefault = wrapped.mobileIsDefault, + defaultMobileIconMapping = wrapped.defaultMobileIconMapping, + defaultMobileIconGroup = wrapped.defaultMobileIconGroup, + isDefaultConnectionFailed = + wrapped.isDefaultConnectionFailed, + isForceHidden = wrapped.isForceHidden, + connectionRepository = wrapRepo(connectionRepository), + context = context, + carrierIdOverrides = overrides, + ) + } + ) + Unit + } + } + testScope.runCurrent() // ensure the lateinit is set + return result + } + + /** Allows us to wrap a (likely fake) MobileIconsInteractor into a Kairos version. */ + private fun BuildScope.wrap(interactor: MobileIconsInteractor): MobileIconsInteractorKairos { + val filteredSubscriptions = interactor.filteredSubscriptions.toState(emptyList()) + val icons = interactor.icons.toState() + return InteractorWrapper( + mobileIsDefault = interactor.mobileIsDefault.toState(), + filteredSubscriptions = filteredSubscriptions, + icons = + combine(filteredSubscriptions, icons) { subs, icons -> + subs.zip(icons).associate { (subModel, icon) -> + subModel.subscriptionId to buildSpec { wrap(icon) } + } + } + .asIncremental() + .applyLatestSpecForKey(), + isStackable = interactor.isStackable.toState(), + activeDataConnectionHasDataEnabled = + interactor.activeDataConnectionHasDataEnabled.toState(), + activeDataIconInteractor = + interactor.activeDataIconInteractor.toState().mapLatestBuild { + it?.let { wrap(it) } + }, + alwaysShowDataRatIcon = interactor.alwaysShowDataRatIcon.toState(), + alwaysUseCdmaLevel = interactor.alwaysUseCdmaLevel.toState(), + isSingleCarrier = interactor.isSingleCarrier.toState(), + defaultMobileIconMapping = interactor.defaultMobileIconMapping.toState(), + defaultMobileIconGroup = interactor.defaultMobileIconGroup.toState(), + isDefaultConnectionFailed = interactor.isDefaultConnectionFailed.toState(), + isUserSetUp = interactor.isUserSetUp.toState(), + isForceHidden = interactor.isForceHidden.toState(false), + isDeviceInEmergencyCallsOnlyMode = + interactor.isDeviceInEmergencyCallsOnlyMode.toState(false), + ) + } + + private fun BuildScope.wrap(interactor: MobileIconInteractor): MobileIconInteractorKairos = + // unused in tests + mock() + + private class InteractorWrapper( + override val mobileIsDefault: State<Boolean>, + override val filteredSubscriptions: State<List<SubscriptionModel>>, + override val icons: Incremental<Int, MobileIconInteractorKairos>, + override val isStackable: State<Boolean>, + override val activeDataConnectionHasDataEnabled: State<Boolean>, + override val activeDataIconInteractor: State<MobileIconInteractorKairos?>, + override val alwaysShowDataRatIcon: State<Boolean>, + override val alwaysUseCdmaLevel: State<Boolean>, + override val isSingleCarrier: State<Boolean>, + override val defaultMobileIconMapping: State<Map<String, SignalIcon.MobileIconGroup>>, + override val defaultMobileIconGroup: State<SignalIcon.MobileIconGroup>, + override val isDefaultConnectionFailed: State<Boolean>, + override val isUserSetUp: State<Boolean>, + override val isForceHidden: State<Boolean>, + override val isDeviceInEmergencyCallsOnlyMode: State<Boolean>, + ) : MobileIconsInteractorKairos +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index 8c70da718c08..974a47587c77 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -22,6 +22,7 @@ import android.telephony.CellSignalStrength import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.internal.telephony.flags.Flags import com.android.settingslib.mobile.MobileIconCarrierIdOverrides import com.android.settingslib.mobile.MobileIconCarrierIdOverridesImpl import com.android.settingslib.mobile.TelephonyIcons @@ -58,21 +59,40 @@ import org.mockito.ArgumentMatchers.anyString @SmallTest @RunWith(AndroidJUnit4::class) -class MobileIconInteractorTest : SysuiTestCase() { - private val kosmos = testKosmos() +class MobileIconInteractorTest : MobileIconInteractorTestBase() { + override fun createInteractor(overrides: MobileIconCarrierIdOverrides) = + MobileIconInteractorImpl( + testScope.backgroundScope, + mobileIconsInteractor.activeDataConnectionHasDataEnabled, + mobileIconsInteractor.alwaysShowDataRatIcon, + mobileIconsInteractor.alwaysUseCdmaLevel, + mobileIconsInteractor.isSingleCarrier, + mobileIconsInteractor.mobileIsDefault, + mobileIconsInteractor.defaultMobileIconMapping, + mobileIconsInteractor.defaultMobileIconGroup, + mobileIconsInteractor.isDefaultConnectionFailed, + mobileIconsInteractor.isForceHidden, + connectionRepository, + context, + overrides, + ) +} - private lateinit var underTest: MobileIconInteractor - private val mobileMappingsProxy = FakeMobileMappingsProxy() - private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy, mock()) +abstract class MobileIconInteractorTestBase : SysuiTestCase() { + protected val kosmos = testKosmos() - private val connectionRepository = + protected lateinit var underTest: MobileIconInteractor + protected val mobileMappingsProxy = FakeMobileMappingsProxy() + protected val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy, mock()) + + protected val connectionRepository = FakeMobileConnectionRepository( SUB_1_ID, logcatTableLogBuffer(kosmos, "MobileIconInteractorTest"), ) - private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(testDispatcher) + protected val testDispatcher = UnconfinedTestDispatcher() + protected val testScope = TestScope(testDispatcher) @Before fun setUp() { @@ -835,24 +855,9 @@ class MobileIconInteractorTest : SysuiTestCase() { assertThat(latest!!.level).isEqualTo(0) } - private fun createInteractor( + abstract fun createInteractor( overrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl() - ) = - MobileIconInteractorImpl( - testScope.backgroundScope, - mobileIconsInteractor.activeDataConnectionHasDataEnabled, - mobileIconsInteractor.alwaysShowDataRatIcon, - mobileIconsInteractor.alwaysUseCdmaLevel, - mobileIconsInteractor.isSingleCarrier, - mobileIconsInteractor.mobileIsDefault, - mobileIconsInteractor.defaultMobileIconMapping, - mobileIconsInteractor.defaultMobileIconGroup, - mobileIconsInteractor.isDefaultConnectionFailed, - mobileIconsInteractor.isForceHidden, - connectionRepository, - context, - overrides, - ) + ): MobileIconInteractor companion object { private const val GSM_LEVEL = 1 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapterTest.kt new file mode 100644 index 000000000000..787731e14cd1 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapterTest.kt @@ -0,0 +1,223 @@ +/* + * 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.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.SignalIcon +import com.android.settingslib.mobile.MobileMappings +import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.KairosBuilder +import com.android.systemui.flags.featureFlagsClassic +import com.android.systemui.kairos.BuildScope +import com.android.systemui.kairos.Events +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.Incremental +import com.android.systemui.kairos.State +import com.android.systemui.kairos.activateKairosActivatable +import com.android.systemui.kairos.asIncremental +import com.android.systemui.kairos.buildSpec +import com.android.systemui.kairos.kairos +import com.android.systemui.kairos.map +import com.android.systemui.kairos.mapValues +import com.android.systemui.kairos.stateOf +import com.android.systemui.kairosBuilder +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.tableLogBufferFactory +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryKairos +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileMappingsProxy +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.shared.data.repository.connectivityRepository +import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@OptIn(ExperimentalKairosApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class MobileIconsInteractorKairosAdapterTest : MobileIconsInteractorTestBase() { + override fun Kosmos.createInteractor(): MobileIconsInteractor { + val userSetupRepo = FakeUserSetupRepository() + val repoK = + MobileConnectionsRepoWrapper(connectionsRepository).also { + activateKairosActivatable(it) + } + val kairosInteractor = + MobileIconsInteractorKairosImpl( + mobileConnectionsRepo = repoK, + carrierConfigTracker = carrierConfigTracker, + tableLogger = mock(), + connectivityRepository = connectivityRepository, + userSetupRepo = userSetupRepo, + context = context, + featureFlagsClassic = featureFlagsClassic, + ) + .also { activateKairosActivatable(it) } + return MobileIconsInteractorKairosAdapter( + kairosInteractor = kairosInteractor, + repo = connectionsRepository, + repoK = repoK, + kairosNetwork = kairos, + scope = applicationCoroutineScope, + context = context, + mobileMappingsProxy = mobileMappingsProxy, + userSetupRepo = userSetupRepo, + logFactory = tableLogBufferFactory, + ) + .also { + activateKairosActivatable(it) + runCurrent() + } + } + + /** Allows us to wrap a (likely fake) MobileConnectionsRepository into a Kairos version. */ + private class MobileConnectionsRepoWrapper(val unwrapped: MobileConnectionsRepository) : + MobileConnectionsRepositoryKairos, KairosBuilder by kairosBuilder() { + + override val mobileConnectionsBySubId: Incremental<Int, MobileConnectionRepositoryKairos> = + buildIncremental { + unwrapped.subscriptions + .toState() + .map { it.associate { it.subscriptionId to Unit } } + .asIncremental() + .mapValues { (subId, _) -> + buildSpec { wrapRepo(unwrapped.getRepoForSubId(subId)) } + } + .applyLatestSpecForKey() + } + override val subscriptions: State<Collection<SubscriptionModel>> = buildState { + unwrapped.subscriptions.toState() + } + override val activeMobileDataSubscriptionId: State<Int?> = buildState { + unwrapped.activeMobileDataSubscriptionId.toState() + } + override val activeMobileDataRepository: State<MobileConnectionRepositoryKairos?> = + buildState { + unwrapped.activeMobileDataRepository.toState().mapLatestBuild { + it?.let { wrapRepo(it) } + } + } + override val activeSubChangedInGroupEvent: Events<Unit> = buildEvents { + unwrapped.activeSubChangedInGroupEvent.toEvents() + } + override val defaultDataSubId: State<Int?> = buildState { + unwrapped.defaultDataSubId.toState() + } + override val mobileIsDefault: State<Boolean> = buildState { + unwrapped.mobileIsDefault.toState() + } + override val hasCarrierMergedConnection: State<Boolean> = buildState { + unwrapped.hasCarrierMergedConnection.toState(false) + } + override val defaultConnectionIsValidated: State<Boolean> = buildState { + unwrapped.defaultConnectionIsValidated.toState() + } + override val defaultDataSubRatConfig: State<MobileMappings.Config> = buildState { + unwrapped.defaultDataSubRatConfig.toState() + } + override val defaultMobileIconMapping: State<Map<String, SignalIcon.MobileIconGroup>> = + buildState { + unwrapped.defaultMobileIconMapping.toState(emptyMap()) + } + override val defaultMobileIconGroup: State<SignalIcon.MobileIconGroup> = buildState { + unwrapped.defaultMobileIconGroup.toState(TelephonyIcons.THREE_G) + } + override val isDeviceEmergencyCallCapable: State<Boolean> = buildState { + unwrapped.isDeviceEmergencyCallCapable.toState() + } + override val isAnySimSecure: State<Boolean> = buildState { + unwrapped.isDeviceEmergencyCallCapable.toState() + } + override val isInEcmMode: State<Boolean> = stateOf(false) + } + + private class MobileConnectionRepoWrapper( + override val subId: Int, + override val carrierId: State<Int>, + override val inflateSignalStrength: State<Boolean>, + override val allowNetworkSliceIndicator: State<Boolean>, + override val tableLogBuffer: TableLogBuffer, + override val isEmergencyOnly: State<Boolean>, + override val isRoaming: State<Boolean>, + override val operatorAlphaShort: State<String?>, + override val isInService: State<Boolean>, + override val isNonTerrestrial: State<Boolean>, + override val isGsm: State<Boolean>, + override val cdmaLevel: State<Int>, + override val primaryLevel: State<Int>, + override val satelliteLevel: State<Int>, + override val dataConnectionState: State<DataConnectionState>, + override val dataActivityDirection: State<DataActivityModel>, + override val carrierNetworkChangeActive: State<Boolean>, + override val resolvedNetworkType: State<ResolvedNetworkType>, + override val numberOfLevels: State<Int>, + override val dataEnabled: State<Boolean>, + override val cdmaRoaming: State<Boolean>, + override val networkName: State<NetworkNameModel>, + override val carrierName: State<NetworkNameModel>, + override val isAllowedDuringAirplaneMode: State<Boolean>, + override val hasPrioritizedNetworkCapabilities: State<Boolean>, + override val isInEcmMode: State<Boolean>, + ) : MobileConnectionRepositoryKairos + + companion object { + /** Allows us to wrap a (likely fake) MobileConnectionRepository into a Kairos version. */ + fun BuildScope.wrapRepo( + conn: MobileConnectionRepository + ): MobileConnectionRepositoryKairos = + with(conn) { + MobileConnectionRepoWrapper( + subId = subId, + carrierId = carrierId.toState(), + inflateSignalStrength = inflateSignalStrength.toState(), + allowNetworkSliceIndicator = allowNetworkSliceIndicator.toState(), + tableLogBuffer = tableLogBuffer, + isEmergencyOnly = isEmergencyOnly.toState(), + isRoaming = isRoaming.toState(), + operatorAlphaShort = operatorAlphaShort.toState(), + isInService = isInService.toState(), + isNonTerrestrial = isNonTerrestrial.toState(), + isGsm = isGsm.toState(), + cdmaLevel = cdmaLevel.toState(), + primaryLevel = primaryLevel.toState(), + satelliteLevel = satelliteLevel.toState(), + dataConnectionState = dataConnectionState.toState(), + dataActivityDirection = dataActivityDirection.toState(), + carrierNetworkChangeActive = carrierNetworkChangeActive.toState(), + resolvedNetworkType = resolvedNetworkType.toState(), + numberOfLevels = numberOfLevels.toState(), + dataEnabled = dataEnabled.toState(), + cdmaRoaming = cdmaRoaming.toState(), + networkName = networkName.toState(), + carrierName = carrierName.toState(), + isAllowedDuringAirplaneMode = isAllowedDuringAirplaneMode.toState(), + hasPrioritizedNetworkCapabilities = hasPrioritizedNetworkCapabilities.toState(), + isInEcmMode = stateOf(false), + ) + } + } +} diff --git a/packages/SystemUI/multivalentTests/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..356e5676954e 100644 --- a/packages/SystemUI/multivalentTests/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 @@ -58,8 +58,35 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) -class MobileIconsInteractorTest : SysuiTestCase() { - private val kosmos by lazy { +class MobileIconsInteractorTest : MobileIconsInteractorTestBase() { + override fun Kosmos.createInteractor() = + MobileIconsInteractorImpl( + mobileConnectionsRepository, + carrierConfigTracker, + tableLogger = mock(), + connectivityRepository, + FakeUserSetupRepository(), + testScope.backgroundScope, + context, + featureFlagsClassic, + ) + + @Test + fun iconInteractor_cachedPerSubId() = + kosmos.runTest { + connectionsRepository.setSubscriptions(listOf(SUB_1)) + runCurrent() + + val interactor1 = underTest.getMobileConnectionInteractorForSubId(SUB_1_ID) + val interactor2 = underTest.getMobileConnectionInteractorForSubId(SUB_1_ID) + + assertThat(interactor1).isNotNull() + assertThat(interactor1).isSameInstanceAs(interactor2) + } +} + +abstract class MobileIconsInteractorTestBase : SysuiTestCase() { + protected val kosmos by lazy { testKosmos().apply { mobileConnectionsRepositoryLogbufferName = "MobileIconsInteractorTest" mobileConnectionsRepository.fake.run { @@ -78,22 +105,13 @@ class MobileIconsInteractorTest : SysuiTestCase() { } // shortcut rename - private val Kosmos.connectionsRepository by Fixture { mobileConnectionsRepository.fake } + protected val Kosmos.connectionsRepository by Fixture { mobileConnectionsRepository.fake } - private val Kosmos.carrierConfigTracker by Fixture { mock<CarrierConfigTracker>() } + protected val Kosmos.carrierConfigTracker by Fixture { mock<CarrierConfigTracker>() } - private val Kosmos.underTest by Fixture { - MobileIconsInteractorImpl( - mobileConnectionsRepository, - carrierConfigTracker, - tableLogger = mock(), - connectivityRepository, - FakeUserSetupRepository(), - testScope.backgroundScope, - context, - featureFlagsClassic, - ) - } + protected val Kosmos.underTest by Fixture { createInteractor() } + + abstract fun Kosmos.createInteractor(): MobileIconsInteractor @Test fun filteredSubscriptions_default() = @@ -744,12 +762,15 @@ class MobileIconsInteractorTest : SysuiTestCase() { val latest by collectLastValue(underTest.mobileIsDefault) connectionsRepository.mobileIsDefault.value = true + runCurrent() assertThat(latest).isTrue() connectionsRepository.mobileIsDefault.value = false + runCurrent() assertThat(latest).isFalse() connectionsRepository.hasCarrierMergedConnection.value = true + runCurrent() assertThat(latest).isTrue() } @@ -874,16 +895,6 @@ class MobileIconsInteractorTest : SysuiTestCase() { } @Test - fun iconInteractor_cachedPerSubId() = - kosmos.runTest { - val interactor1 = underTest.getMobileConnectionInteractorForSubId(SUB_1_ID) - val interactor2 = underTest.getMobileConnectionInteractorForSubId(SUB_1_ID) - - assertThat(interactor1).isNotNull() - assertThat(interactor1).isSameInstanceAs(interactor2) - } - - @Test fun deviceBasedEmergencyMode_emergencyCallsOnly_followsDeviceServiceStateFromRepo() = kosmos.runTest { val latest by collectLastValue(underTest.isDeviceInEmergencyCallsOnlyMode) @@ -1007,8 +1018,8 @@ class MobileIconsInteractorTest : SysuiTestCase() { companion object { - private const val SUB_1_ID = 1 - private val SUB_1 = + const val SUB_1_ID = 1 + val SUB_1 = SubscriptionModel( subscriptionId = SUB_1_ID, carrierName = "Carrier $SUB_1_ID", diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt index 6d3813c90bfd..bcc6ea3d3b35 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt @@ -20,8 +20,6 @@ import android.view.View import android.view.WindowManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.app.viewcapture.ViewCapture -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.systemui.SysuiTestCase import com.android.systemui.keyevent.data.repository.fakeKeyEventRepository import com.android.systemui.keyevent.data.repository.keyEventRepository @@ -58,20 +56,13 @@ class TopLevelWindowEffectsTest : SysuiTestCase() { private lateinit var windowManager: WindowManager @Mock - private lateinit var viewCapture: Lazy<ViewCapture> - - @Mock private lateinit var viewModelFactory: SqueezeEffectViewModel.Factory private val Kosmos.underTest by Kosmos.Fixture { TopLevelWindowEffects( context = mContext, applicationScope = testScope.backgroundScope, - windowManager = ViewCaptureAwareWindowManager( - windowManager = windowManager, - lazyViewCapture = viewCapture, - isViewCaptureEnabled = false - ), + windowManager = windowManager, keyEventInteractor = keyEventInteractor, viewModelFactory = viewModelFactory, squeezeEffectInteractor = SqueezeEffectInteractor( 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/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt index a303da0dae1a..49c06bf77fb6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt @@ -143,6 +143,7 @@ class VolumeDialogVisibilityInteractorTest : SysuiTestCase() { assertThat(visibilityModel) .isInstanceOf(VolumeDialogVisibilityModel.Visible::class.java) + assertThat(fakeVolumeDialogController.hasUserActivity).isTrue() } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModelTest.kt new file mode 100644 index 000000000000..0847281d9b42 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModelTest.kt @@ -0,0 +1,82 @@ +/* + * 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.volume.dialog.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.accessibility.data.repository.accessibilityRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.volume.Events +import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibilityRepository +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor +import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel +import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.test.advanceTimeBy +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class VolumeDialogPluginViewModelTest : SysuiTestCase() { + + private val kosmos: Kosmos = testKosmos() + + private val underTest: VolumeDialogPluginViewModel by lazy { + kosmos.volumeDialogPluginViewModel + } + + @Before + fun setUp() = + with(kosmos) { + volumeDialogVisibilityRepository.updateVisibility { + VolumeDialogVisibilityModel.Visible(Events.SHOW_REASON_VOLUME_CHANGED, false, 0) + } + } + + @Test + fun safetyWarningAppears_timeoutReset() = + kosmos.runTest { + accessibilityRepository.setRecommendedTimeout(3.seconds) + val visibility by collectLastValue(volumeDialogVisibilityInteractor.dialogVisibility) + testScope.advanceTimeBy(2.seconds) + assertThat(visibility).isInstanceOf(VolumeDialogVisibilityModel.Visible::class.java) + + underTest.onSafetyWarningDialogShown() + testScope.advanceTimeBy(2.seconds) + assertThat(visibility).isInstanceOf(VolumeDialogVisibilityModel.Visible::class.java) + } + + @Test + fun csdWarningAppears_timeoutReset() = + kosmos.runTest { + accessibilityRepository.setRecommendedTimeout(3.seconds) + val visibility by collectLastValue(volumeDialogVisibilityInteractor.dialogVisibility) + testScope.advanceTimeBy(2.seconds) + assertThat(visibility).isInstanceOf(VolumeDialogVisibilityModel.Visible::class.java) + + underTest.onCsdWarningDialogShown() + testScope.advanceTimeBy(2.seconds) + assertThat(visibility).isInstanceOf(VolumeDialogVisibilityModel.Visible::class.java) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java index 60a15915fb77..046b5d701ddf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java @@ -57,6 +57,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.settings.UserTracker; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import org.junit.Before; import org.junit.Test; @@ -89,6 +90,8 @@ public class ImageWallpaperTest extends SysuiTestCase { private Context mMockContext; @Mock private UserTracker mUserTracker; + @Mock + private WindowManagerProvider mWindowManagerProvider; @Mock private Bitmap mWallpaperBitmap; @@ -105,7 +108,7 @@ public class ImageWallpaperTest extends SysuiTestCase { when(mWindowMetrics.getBounds()).thenReturn( new Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT)); when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics); - when(mMockContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager); + when(mWindowManagerProvider.getWindowManager(any())).thenReturn(mWindowManager); // set up display manager doNothing().when(mDisplayManager).registerDisplayListener(any(), any()); @@ -182,7 +185,7 @@ public class ImageWallpaperTest extends SysuiTestCase { } private ImageWallpaper createImageWallpaper() { - return new ImageWallpaper(mFakeExecutor, mUserTracker) { + return new ImageWallpaper(mFakeExecutor, mUserTracker, mWindowManagerProvider) { @Override public Engine onCreateEngine() { return new CanvasEngine() { 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/biometric_prompt_one_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml index 43808f215a81..4002f7808637 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml @@ -21,6 +21,9 @@ style="@style/AuthCredentialPanelStyle" android:layout_width="0dp" android:layout_height="0dp" + android:accessibilityLiveRegion="assertive" + android:importantForAccessibility="yes" + android:clickable="false" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/rightGuideline" app:layout_constraintStart_toStartOf="@id/leftGuideline" diff --git a/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml index 51117a7845df..3c8cb6860a41 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml @@ -22,6 +22,9 @@ android:layout_height="match_parent"> style="@style/AuthCredentialPanelStyle" android:layout_width="0dp" android:layout_height="0dp" + android:accessibilityLiveRegion="assertive" + android:importantForAccessibility="yes" + android:clickable="false" android:paddingHorizontal="16dp" android:paddingVertical="16dp" android:visibility="visible" 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/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml index a1fa54cf592d..e094155f9041 100644 --- a/packages/SystemUI/res/layout/volume_dialog.xml +++ b/packages/SystemUI/res/layout/volume_dialog.xml @@ -16,17 +16,18 @@ <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/volume_dialog" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="match_parent" android:alpha="0" - android:clipChildren="false"> + android:clipChildren="false" + android:minWidth="@dimen/volume_dialog_window_width"> <View android:id="@+id/volume_dialog_background" android:layout_width="@dimen/volume_dialog_width" android:layout_height="0dp" android:layout_marginTop="@dimen/volume_dialog_background_top_margin" - android:layout_marginBottom="@dimen/volume_dialog_background_vertical_margin" + android:layout_marginBottom="@dimen/volume_dialog_background_margin_negative" android:background="@drawable/volume_dialog_background" app:layout_constraintBottom_toBottomOf="@id/volume_dialog_bottom_section_container" app:layout_constraintEnd_toEndOf="@id/volume_dialog_main_slider_container" diff --git a/packages/SystemUI/res/layout/volume_dialog_slider_floating.xml b/packages/SystemUI/res/layout/volume_dialog_slider_floating.xml index db800aa4a873..1dba96cafb70 100644 --- a/packages/SystemUI/res/layout/volume_dialog_slider_floating.xml +++ b/packages/SystemUI/res/layout/volume_dialog_slider_floating.xml @@ -17,8 +17,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/volume_dialog_floating_slider_background" - android:paddingHorizontal="@dimen/volume_dialog_floating_sliders_horizontal_padding" - android:paddingVertical="@dimen/volume_dialog_floating_sliders_vertical_padding"> + android:paddingHorizontal="@dimen/volume_dialog_floating_sliders_padding" + android:paddingVertical="@dimen/volume_dialog_floating_sliders_padding"> <include layout="@layout/volume_dialog_slider" /> </FrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/volume_dialog_top_section.xml b/packages/SystemUI/res/layout/volume_dialog_top_section.xml index b7455471d9a6..9d3598800bbf 100644 --- a/packages/SystemUI/res/layout/volume_dialog_top_section.xml +++ b/packages/SystemUI/res/layout/volume_dialog_top_section.xml @@ -22,15 +22,15 @@ android:clipChildren="false" android:clipToPadding="false" android:gravity="center" - android:paddingEnd="@dimen/volume_dialog_buttons_margin" + android:paddingEnd="@dimen/volume_dialog_background_margin" app:layoutDescription="@xml/volume_dialog_ringer_drawer_motion_scene"> <View android:id="@+id/ringer_buttons_background" android:layout_width="@dimen/volume_dialog_width" android:layout_height="0dp" - android:layout_marginTop="@dimen/volume_dialog_background_vertical_margin" - android:layout_marginBottom="@dimen/volume_dialog_background_vertical_margin" + android:layout_marginTop="@dimen/volume_dialog_background_margin_negative" + android:layout_marginBottom="@dimen/volume_dialog_background_margin_negative" android:background="@drawable/volume_dialog_ringer_background" android:visibility="gone" /> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index d0ae307b6919..55e94028b95e 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> @@ -2158,28 +2158,25 @@ <dimen name="contextual_edu_dialog_elevation">2dp</dimen> <!-- Volume start --> + <dimen name="volume_dialog_window_width">176dp</dimen> <dimen name="volume_dialog_width">60dp</dimen> <dimen name="volume_dialog_background_corner_radius">30dp</dimen> - <dimen name="volume_dialog_background_vertical_margin"> - @dimen/volume_dialog_buttons_margin_negative - </dimen> <!-- top margin covers half the ringer button + components spacing --> <dimen name="volume_dialog_background_top_margin">-28dp</dimen> + <dimen name="volume_dialog_background_margin">10dp</dimen> + <dimen name="volume_dialog_background_margin_negative">-10dp</dimen> - <dimen name="volume_dialog_window_margin">14dp</dimen> + <dimen name="volume_dialog_window_margin">12dp</dimen> <dimen name="volume_dialog_components_spacing">10dp</dimen> <dimen name="volume_dialog_floating_sliders_spacing">8dp</dimen> - <dimen name="volume_dialog_floating_sliders_vertical_padding">10dp</dimen> <dimen name="volume_dialog_floating_sliders_vertical_padding_negative"> - @dimen/volume_dialog_buttons_margin_negative + @dimen/volume_dialog_background_margin_negative </dimen> - <dimen name="volume_dialog_floating_sliders_horizontal_padding">4dp</dimen> + <dimen name="volume_dialog_floating_sliders_padding">4dp</dimen> <dimen name="volume_dialog_button_size">40dp</dimen> <dimen name="volume_dialog_slider_width">52dp</dimen> <dimen name="volume_dialog_slider_height">254dp</dimen> - <dimen name="volume_dialog_buttons_margin">10dp</dimen> - <dimen name="volume_dialog_buttons_margin_negative">-10dp</dimen> <!-- A primary goal of this margin is to vertically constraint slider height in the landscape orientation when the vertical space is limited @@ -2190,8 +2187,10 @@ <dimen name="volume_dialog_background_square_corner_radius">12dp</dimen> - <dimen name="volume_dialog_ringer_drawer_margin">@dimen/volume_dialog_buttons_margin</dimen> <dimen name="volume_dialog_ringer_drawer_button_size">@dimen/volume_dialog_button_size</dimen> + <dimen name="volume_dialog_ringer_drawer_buttons_spacing"> + @dimen/volume_dialog_components_spacing + </dimen> <dimen name="volume_dialog_ringer_drawer_button_icon_radius">10dp</dimen> <dimen name="volume_dialog_ringer_selected_button_background_radius">20dp</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/biometrics/Android.bp b/packages/SystemUI/shared/biometrics/Android.bp index 63de81d4a680..208157c69adf 100644 --- a/packages/SystemUI/shared/biometrics/Android.bp +++ b/packages/SystemUI/shared/biometrics/Android.bp @@ -14,6 +14,9 @@ android_library { "src/**/*.java", "src/**/*.kt", ], + static_libs: [ + "SystemUI-shared-utils", + ], resource_dirs: [ "res", ], diff --git a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt index 5b99a3f16fc2..7aa09cf64405 100644 --- a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt +++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt @@ -41,6 +41,7 @@ import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN import com.android.systemui.biometrics.shared.model.PromptKind +import com.android.systemui.utils.windowmanager.WindowManagerUtils object Utils { private const val TAG = "SysUIBiometricUtils" @@ -117,10 +118,9 @@ object Utils { @JvmStatic fun getNavbarInsets(context: Context): Insets { - val windowManager: WindowManager? = context.getSystemService(WindowManager::class.java) - val windowMetrics: WindowMetrics? = windowManager?.maximumWindowMetrics - return windowMetrics?.windowInsets?.getInsets(WindowInsets.Type.navigationBars()) - ?: Insets.NONE + val windowManager: WindowManager = WindowManagerUtils.getWindowManager(context) + val windowMetrics: WindowMetrics = windowManager.maximumWindowMetrics + return windowMetrics.windowInsets.getInsets(WindowInsets.Type.navigationBars()) } /** Converts `drawable` to a [Bitmap]. */ 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/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java index c82243934b8b..b1fc560f406b 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java @@ -37,6 +37,7 @@ import android.view.Surface; import android.view.WindowManager; import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.utils.windowmanager.WindowManagerUtils; /* Common code */ public class Utilities { @@ -152,7 +153,7 @@ public class Utilities { /** @return whether or not {@param context} represents that of a large screen device or not */ @TargetApi(Build.VERSION_CODES.R) public static boolean isLargeScreen(Context context) { - return isLargeScreen(context.getSystemService(WindowManager.class), context.getResources()); + return isLargeScreen(WindowManagerUtils.getWindowManager(context), context.getResources()); } /** @return whether or not {@param context} represents that of a large screen device or not */ diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java index 4db6ab6ea579..570d774b95e9 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java @@ -16,9 +16,6 @@ package com.android.systemui.shared.rotation; -import static com.android.app.viewcapture.ViewCaptureFactory.getViewCaptureAwareWindowManagerInstance; -import static com.android.systemui.Flags.enableViewCaptureTracing; - import android.annotation.DimenRes; import android.annotation.IdRes; import android.annotation.LayoutRes; @@ -33,6 +30,7 @@ import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.FrameLayout; @@ -40,8 +38,8 @@ import android.widget.FrameLayout; import androidx.annotation.BoolRes; import androidx.core.view.OneShotPreDrawListener; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.shared.rotation.FloatingRotationButtonPositionCalculator.Position; +import com.android.systemui.utils.windowmanager.WindowManagerUtils; /** * Containing logic for the rotation button on the physical left bottom corner of the screen. @@ -50,7 +48,7 @@ public class FloatingRotationButton implements RotationButton { private static final int MARGIN_ANIMATION_DURATION_MILLIS = 300; - private final ViewCaptureAwareWindowManager mWindowManager; + private final WindowManager mWindowManager; private final ViewGroup mKeyButtonContainer; private final FloatingRotationButtonView mKeyButtonView; @@ -91,8 +89,7 @@ public class FloatingRotationButton implements RotationButton { @DimenRes int taskbarBottomMargin, @DimenRes int buttonDiameter, @DimenRes int rippleMaxWidth, @BoolRes int floatingRotationBtnPositionLeftResource) { mContext = context; - mWindowManager = getViewCaptureAwareWindowManagerInstance(mContext, - enableViewCaptureTracing()); + mWindowManager = WindowManagerUtils.getWindowManager(mContext); mKeyButtonContainer = (ViewGroup) LayoutInflater.from(mContext).inflate(layout, null); mKeyButtonView = mKeyButtonContainer.findViewById(keyButtonId); mKeyButtonView.setVisibility(View.VISIBLE); 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/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index 19da5de6b531..59ec6923ce91 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -66,7 +66,6 @@ import android.widget.FrameLayout; import androidx.annotation.VisibleForTesting; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.util.Preconditions; import com.android.settingslib.Utils; import com.android.systemui.biometrics.data.repository.FacePropertyRepository; @@ -167,7 +166,7 @@ public class ScreenDecorations implements ViewGroup mScreenDecorHwcWindow; @VisibleForTesting ScreenDecorHwcLayer mScreenDecorHwcLayer; - private ViewCaptureAwareWindowManager mWindowManager; + private WindowManager mWindowManager; private int mRotation; private UserSettingObserver mColorInversionSetting; private DelayableExecutor mExecutor; @@ -337,7 +336,7 @@ public class ScreenDecorations implements FacePropertyRepository facePropertyRepository, JavaAdapter javaAdapter, CameraProtectionLoader cameraProtectionLoader, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, + WindowManager windowManager, @ScreenDecorationsThread Handler handler, @ScreenDecorationsThread DelayableExecutor executor) { mContext = context; @@ -353,7 +352,7 @@ public class ScreenDecorations implements mLogger = logger; mFacePropertyRepository = facePropertyRepository; mJavaAdapter = javaAdapter; - mWindowManager = viewCaptureAwareWindowManager; + mWindowManager = windowManager; mHandler = handler; mExecutor = executor; } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java index 115242eb13aa..375137c67f7c 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java @@ -44,7 +44,6 @@ import android.window.InputTransferToken; import androidx.annotation.NonNull; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.dagger.SysUISingleton; @@ -54,6 +53,7 @@ import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import java.io.PrintWriter; import java.util.concurrent.Executor; @@ -97,20 +97,19 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks private final WindowMagnifierCallback mWindowMagnifierCallback; private final SysUiState mSysUiState; private final SecureSettings mSecureSettings; - private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; + private final WindowManagerProvider mWindowManagerProvider; WindowMagnificationControllerSupplier(Context context, Handler handler, WindowMagnifierCallback windowMagnifierCallback, DisplayManager displayManager, SysUiState sysUiState, - SecureSettings secureSettings, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + SecureSettings secureSettings, WindowManagerProvider windowManagerProvider) { super(displayManager); mContext = context; mHandler = handler; mWindowMagnifierCallback = windowMagnifierCallback; mSysUiState = sysUiState; mSecureSettings = secureSettings; - mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager; + mWindowManagerProvider = windowManagerProvider; } @Override @@ -118,6 +117,8 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks final Context windowContext = mContext.createWindowContext(display, TYPE_ACCESSIBILITY_OVERLAY, /* options */ null); + final WindowManager windowManager = mWindowManagerProvider + .getWindowManager(windowContext); windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI); Supplier<SurfaceControlViewHost> scvhSupplier = () -> @@ -133,7 +134,8 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks mWindowMagnifierCallback, mSysUiState, mSecureSettings, - scvhSupplier); + scvhSupplier, + windowManager); } } @@ -148,17 +150,20 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks private final Executor mExecutor; private final DisplayManager mDisplayManager; private final IWindowManager mIWindowManager; + private final WindowManagerProvider mWindowManagerProvider; FullscreenMagnificationControllerSupplier(Context context, DisplayManager displayManager, Handler handler, - Executor executor, IWindowManager iWindowManager) { + Executor executor, IWindowManager iWindowManager, + WindowManagerProvider windowManagerProvider) { super(displayManager); mContext = context; mHandler = handler; mExecutor = executor; mDisplayManager = displayManager; mIWindowManager = iWindowManager; + mWindowManagerProvider = windowManagerProvider; } @Override @@ -174,7 +179,7 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks mExecutor, mDisplayManager, windowContext.getSystemService(AccessibilityManager.class), - windowContext.getSystemService(WindowManager.class), + mWindowManagerProvider.getWindowManager(windowContext), mIWindowManager, scvhSupplier); } @@ -190,31 +195,32 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks private final Context mContext; private final MagnificationSettingsController.Callback mSettingsControllerCallback; private final SecureSettings mSecureSettings; - private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; + private final WindowManagerProvider mWindowManagerProvider; SettingsSupplier(Context context, MagnificationSettingsController.Callback settingsControllerCallback, DisplayManager displayManager, - SecureSettings secureSettings, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + SecureSettings secureSettings, WindowManagerProvider windowManagerProvider) { super(displayManager); mContext = context; mSettingsControllerCallback = settingsControllerCallback; mSecureSettings = secureSettings; - mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager; + mWindowManagerProvider = windowManagerProvider; } @Override protected MagnificationSettingsController createInstance(Display display) { final Context windowContext = mContext.createWindowContext(display, TYPE_ACCESSIBILITY_OVERLAY, /* options */ null); + final WindowManager windowManager = mWindowManagerProvider + .getWindowManager(windowContext); windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI); return new MagnificationSettingsController( windowContext, new SfVsyncFrameCallbackProvider(), mSettingsControllerCallback, mSecureSettings, - mViewCaptureAwareWindowManager); + windowManager); } } @@ -229,11 +235,11 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks SecureSettings secureSettings, DisplayTracker displayTracker, DisplayManager displayManager, AccessibilityLogger a11yLogger, IWindowManager iWindowManager, AccessibilityManager accessibilityManager, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + WindowManagerProvider windowManagerProvider) { this(context, mainHandler.getLooper(), executor, commandQueue, modeSwitchesController, sysUiState, launcherProxyService, secureSettings, displayTracker, displayManager, a11yLogger, iWindowManager, accessibilityManager, - viewCaptureAwareWindowManager); + windowManagerProvider); } @VisibleForTesting @@ -244,7 +250,7 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks DisplayManager displayManager, AccessibilityLogger a11yLogger, IWindowManager iWindowManager, AccessibilityManager accessibilityManager, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + WindowManagerProvider windowManagerProvider) { mHandler = new Handler(looper) { @Override public void handleMessage(@NonNull Message msg) { @@ -263,12 +269,13 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks mA11yLogger = a11yLogger; mWindowMagnificationControllerSupplier = new WindowMagnificationControllerSupplier(context, mHandler, mWindowMagnifierCallback, - displayManager, sysUiState, secureSettings, viewCaptureAwareWindowManager); + displayManager, sysUiState, secureSettings, windowManagerProvider); mFullscreenMagnificationControllerSupplier = new FullscreenMagnificationControllerSupplier( - context, displayManager, mHandler, mExecutor, iWindowManager); + context, displayManager, mHandler, mExecutor, iWindowManager, + windowManagerProvider); mMagnificationSettingsSupplier = new SettingsSupplier(context, mMagnificationSettingsControllerCallback, displayManager, secureSettings, - viewCaptureAwareWindowManager); + windowManagerProvider); mModeSwitchesController.setClickListenerDelegate( displayId -> mHandler.post(() -> { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java index 4723ab958f86..9eb01de239bc 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java @@ -46,7 +46,6 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.widget.ImageView; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.res.R; @@ -77,7 +76,6 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL private final Context mContext; private final AccessibilityManager mAccessibilityManager; private final WindowManager mWindowManager; - private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private final ImageView mImageView; private final Runnable mWindowInsetChangeRunnable; private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider; @@ -101,21 +99,20 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL void onClick(int displayId); } - MagnificationModeSwitch(@UiContext Context context, ClickListener clickListener, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { - this(context, createView(context), new SfVsyncFrameCallbackProvider(), clickListener, - viewCaptureAwareWindowManager); + MagnificationModeSwitch(@UiContext Context context, WindowManager windowManager, + ClickListener clickListener) { + this(context, windowManager, createView(context), new SfVsyncFrameCallbackProvider(), + clickListener); } @VisibleForTesting - MagnificationModeSwitch(Context context, @NonNull ImageView imageView, - SfVsyncFrameCallbackProvider sfVsyncFrameProvider, ClickListener clickListener, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + MagnificationModeSwitch(Context context, WindowManager windowManager, + @NonNull ImageView imageView, SfVsyncFrameCallbackProvider sfVsyncFrameProvider, + ClickListener clickListener) { mContext = context; mConfiguration = new Configuration(context.getResources().getConfiguration()); mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); - mWindowManager = mContext.getSystemService(WindowManager.class); - mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager; + mWindowManager = windowManager; mSfVsyncFrameProvider = sfVsyncFrameProvider; mClickListener = clickListener; mParams = createLayoutParams(context); @@ -282,7 +279,7 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL mImageView.animate().cancel(); mIsFadeOutAnimating = false; mImageView.setAlpha(0f); - mViewCaptureAwareWindowManager.removeView(mImageView); + mWindowManager.removeView(mImageView); mContext.unregisterComponentCallbacks(this); mIsVisible = false; } @@ -316,7 +313,7 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL mParams.y = mDraggableWindowBounds.bottom; mToLeftScreenEdge = false; } - mViewCaptureAwareWindowManager.addView(mImageView, mParams); + mWindowManager.addView(mImageView, mParams); // Exclude magnification switch button from system gesture area. setSystemGestureExclusion(); mIsVisible = true; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java index fc7535a712e3..2d5dc8d23383 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java @@ -26,7 +26,6 @@ import android.content.res.Configuration; import android.util.Range; import android.view.WindowManager; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.accessibility.common.MagnificationConstants; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; @@ -62,9 +61,9 @@ public class MagnificationSettingsController implements ComponentCallbacks { SfVsyncFrameCallbackProvider sfVsyncFrameProvider, @NonNull Callback settingsControllerCallback, SecureSettings secureSettings, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { - this(context, sfVsyncFrameProvider, settingsControllerCallback, secureSettings, null, - viewCaptureAwareWindowManager); + WindowManager windowManager) { + this(context, sfVsyncFrameProvider, settingsControllerCallback, secureSettings, + windowManager, null); } @VisibleForTesting @@ -73,8 +72,8 @@ public class MagnificationSettingsController implements ComponentCallbacks { SfVsyncFrameCallbackProvider sfVsyncFrameProvider, @NonNull Callback settingsControllerCallback, SecureSettings secureSettings, - WindowMagnificationSettings windowMagnificationSettings, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + WindowManager windowManager, + WindowMagnificationSettings windowMagnificationSettings) { mContext = context.createWindowContext( context.getDisplay(), WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, @@ -88,7 +87,7 @@ public class MagnificationSettingsController implements ComponentCallbacks { } else { mWindowMagnificationSettings = new WindowMagnificationSettings(mContext, mWindowMagnificationSettingsCallback, - sfVsyncFrameProvider, secureSettings, viewCaptureAwareWindowManager); + sfVsyncFrameProvider, secureSettings, windowManager); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java b/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java index eb4de6837d41..7f3a869d8222 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java @@ -18,9 +18,6 @@ package com.android.systemui.accessibility; import static android.view.WindowManager.LayoutParams; -import static com.android.app.viewcapture.ViewCaptureFactory.getViewCaptureAwareWindowManagerInstance; -import static com.android.systemui.Flags.enableViewCaptureTracing; - import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; @@ -32,8 +29,8 @@ import android.util.MathUtils; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; +import android.view.WindowManager; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.res.R; /** @@ -73,12 +70,11 @@ public abstract class MirrorWindowControl { * @see #setDefaultPosition(LayoutParams) */ private final Point mControlPosition = new Point(); - private final ViewCaptureAwareWindowManager mWindowManager; + private final WindowManager mWindowManager; - MirrorWindowControl(Context context) { + MirrorWindowControl(Context context, WindowManager windowManager) { mContext = context; - mWindowManager = getViewCaptureAwareWindowManagerInstance(mContext, - enableViewCaptureTracing()); + mWindowManager = windowManager; } public void setWindowDelegate(@Nullable MirrorWindowDelegate windowDelegate) { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/ModeSwitchesController.java b/packages/SystemUI/src/com/android/systemui/accessibility/ModeSwitchesController.java index 53827e65344a..7d9f8674457c 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/ModeSwitchesController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/ModeSwitchesController.java @@ -24,10 +24,11 @@ import android.annotation.MainThread; import android.content.Context; import android.hardware.display.DisplayManager; import android.view.Display; +import android.view.WindowManager; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import javax.inject.Inject; @@ -49,9 +50,9 @@ public class ModeSwitchesController implements ClickListener { @Inject public ModeSwitchesController(Context context, DisplayManager displayManager, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + WindowManagerProvider windowManagerProvider) { mSwitchSupplier = new SwitchSupplier(context, displayManager, this::onClick, - viewCaptureAwareWindowManager); + windowManagerProvider); } @VisibleForTesting @@ -118,7 +119,7 @@ public class ModeSwitchesController implements ClickListener { private final Context mContext; private final ClickListener mClickListener; - private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; + private final WindowManagerProvider mWindowManagerProvider; /** * Supplies the switch for the given display. @@ -128,20 +129,20 @@ public class ModeSwitchesController implements ClickListener { * @param clickListener The callback that will run when the switch is clicked */ SwitchSupplier(Context context, DisplayManager displayManager, - ClickListener clickListener, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + ClickListener clickListener, WindowManagerProvider windowManagerProvider) { super(displayManager); mContext = context; mClickListener = clickListener; - mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager; + mWindowManagerProvider = windowManagerProvider; } @Override protected MagnificationModeSwitch createInstance(Display display) { final Context uiContext = mContext.createWindowContext(display, TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, /* options */ null); - return new MagnificationModeSwitch(uiContext, mClickListener, - mViewCaptureAwareWindowManager); + final WindowManager uiWindowManager = mWindowManagerProvider + .getWindowManager(uiContext); + return new MagnificationModeSwitch(uiContext, uiWindowManager, mClickListener); } } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SimpleMirrorWindowControl.java b/packages/SystemUI/src/com/android/systemui/accessibility/SimpleMirrorWindowControl.java index bc469eed7359..3cde033bf56a 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/SimpleMirrorWindowControl.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/SimpleMirrorWindowControl.java @@ -27,6 +27,7 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; +import android.view.WindowManager; import com.android.systemui.res.R; @@ -48,8 +49,8 @@ class SimpleMirrorWindowControl extends MirrorWindowControl implements View.OnCl private final PointF mLastDrag = new PointF(); private final Handler mHandler; - SimpleMirrorWindowControl(Context context, Handler handler) { - super(context); + SimpleMirrorWindowControl(Context context, Handler handler, WindowManager windowManager) { + super(context, windowManager); mHandler = handler; final Resources resource = context.getResources(); mMoveFrameAmountShort = resource.getDimensionPixelSize( diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index 8734d05bc894..9cd77e790e8c 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -249,7 +249,8 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold @NonNull WindowMagnifierCallback callback, SysUiState sysUiState, SecureSettings secureSettings, - Supplier<SurfaceControlViewHost> scvhSupplier) { + Supplier<SurfaceControlViewHost> scvhSupplier, + WindowManager windowManager) { mContext = context; mHandler = handler; mAnimationController = animationController; @@ -265,7 +266,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold mDisplayId = mContext.getDisplayId(); mRotation = display.getRotation(); - mWm = context.getSystemService(WindowManager.class); + mWm = windowManager; mWindowBounds = new Rect(mWm.getCurrentWindowMetrics().getBounds()); mResources = mContext.getResources(); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java index 3b6f8f87a1a8..bd4b6420bf37 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java @@ -56,7 +56,6 @@ import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.Switch; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView; @@ -75,7 +74,6 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest private final Context mContext; private final AccessibilityManager mAccessibilityManager; private final WindowManager mWindowManager; - private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private final SecureSettings mSecureSettings; private final Runnable mWindowInsetChangeRunnable; @@ -137,11 +135,10 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest @VisibleForTesting WindowMagnificationSettings(Context context, WindowMagnificationSettingsCallback callback, SfVsyncFrameCallbackProvider sfVsyncFrameProvider, SecureSettings secureSettings, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + WindowManager windowManager) { mContext = context; mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); - mWindowManager = mContext.getSystemService(WindowManager.class); - mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager; + mWindowManager = windowManager; mSfVsyncFrameProvider = sfVsyncFrameProvider; mCallback = callback; mSecureSettings = secureSettings; @@ -324,7 +321,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest // Unregister observer before removing view mSecureSettings.unregisterContentObserverSync(mMagnificationCapabilityObserver); - mViewCaptureAwareWindowManager.removeView(mSettingView); + mWindowManager.removeView(mSettingView); mIsVisible = false; if (resetPosition) { mParams.x = 0; @@ -382,7 +379,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mParams.y = mDraggableWindowBounds.bottom; } - mViewCaptureAwareWindowManager.addView(mSettingView, mParams); + mWindowManager.addView(mSettingView, mParams); mSecureSettings.registerContentObserverForUserSync( Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY, diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java index f8e4bda15d01..ef42837ba776 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java @@ -31,7 +31,6 @@ import android.view.accessibility.IUserInitializationCompleteCallback; import androidx.annotation.MainThread; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.annotations.VisibleForTesting; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; @@ -60,7 +59,6 @@ public class AccessibilityFloatingMenuController implements private final Context mContext; private final WindowManager mWindowManager; - private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private final DisplayManager mDisplayManager; private final AccessibilityManager mAccessibilityManager; private final HearingAidDeviceManager mHearingAidDeviceManager; @@ -105,7 +103,6 @@ public class AccessibilityFloatingMenuController implements @Inject public AccessibilityFloatingMenuController(Context context, WindowManager windowManager, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, DisplayManager displayManager, AccessibilityManager accessibilityManager, AccessibilityButtonTargetsObserver accessibilityButtonTargetsObserver, @@ -118,7 +115,6 @@ public class AccessibilityFloatingMenuController implements @Main Handler handler) { mContext = context; mWindowManager = windowManager; - mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager; mDisplayManager = displayManager; mAccessibilityManager = accessibilityManager; mAccessibilityButtonTargetsObserver = accessibilityButtonTargetsObserver; @@ -205,8 +201,8 @@ public class AccessibilityFloatingMenuController implements final Context windowContext = mContext.createWindowContext(defaultDisplay, TYPE_NAVIGATION_BAR_PANEL, /* options= */ null); mFloatingMenu = new MenuViewLayerController(windowContext, mWindowManager, - mViewCaptureAwareWindowManager, mAccessibilityManager, mSecureSettings, - mNavigationModeController, mHearingAidDeviceManager); + mAccessibilityManager, mSecureSettings, mNavigationModeController, + mHearingAidDeviceManager); } mFloatingMenu.show(); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt index 13c1a450832f..52e69efdbc19 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt @@ -48,7 +48,7 @@ import com.android.wm.shell.shared.bubbles.DismissView * * @note [setup] method should be called after initialisation */ -class DragToInteractView(context: Context) : FrameLayout(context) { +class DragToInteractView(context: Context, windowManager: WindowManager) : FrameLayout(context) { /** * The configuration is used to provide module specific resource ids * @@ -86,8 +86,7 @@ class DragToInteractView(context: Context) : FrameLayout(context) { private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY) private val INTERACT_SCRIM_FADE_MS = 200L - private var wm: WindowManager = - context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + private var wm: WindowManager = windowManager private var gradientDrawable: GradientDrawable? = null private val GRADIENT_ALPHA: IntProperty<GradientDrawable> = diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java index 81095220b4a6..8fb260c2df36 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java @@ -212,7 +212,7 @@ class MenuViewLayer extends FrameLayout implements mMenuAnimationController = mMenuView.getMenuAnimationController(); mMenuAnimationController.setSpringAnimationsEndAction(this::onSpringAnimationsEndAction); mDismissView = new DismissView(context); - mDragToInteractView = new DragToInteractView(context); + mDragToInteractView = new DragToInteractView(context, windowManager); DismissViewUtils.setup(mDismissView); mDismissView.getCircle().setId(R.id.action_remove_menu); mNotificationFactory = new MenuNotificationFactory(context); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java index 102efcf7badd..7bf7e23b5df5 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java @@ -25,7 +25,6 @@ import android.graphics.PixelFormat; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.settingslib.bluetooth.HearingAidDeviceManager; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.util.settings.SecureSettings; @@ -35,16 +34,15 @@ import com.android.systemui.util.settings.SecureSettings; * of {@link IAccessibilityFloatingMenu}. */ class MenuViewLayerController implements IAccessibilityFloatingMenu { - private final ViewCaptureAwareWindowManager mWindowManager; + private final WindowManager mWindowManager; private final MenuViewLayer mMenuViewLayer; private boolean mIsShowing; MenuViewLayerController(Context context, WindowManager windowManager, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, AccessibilityManager accessibilityManager, SecureSettings secureSettings, NavigationModeController navigationModeController, HearingAidDeviceManager hearingAidDeviceManager) { - mWindowManager = viewCaptureAwareWindowManager; + mWindowManager = windowManager; MenuViewModel menuViewModel = new MenuViewModel( context, accessibilityManager, secureSettings, hearingAidDeviceManager); 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/AssistDisclosure.java b/packages/SystemUI/src/com/android/systemui/assist/AssistDisclosure.java index f4e2b82f1773..14ce2cf60919 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistDisclosure.java +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistDisclosure.java @@ -33,7 +33,6 @@ import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import com.android.app.animation.Interpolators; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.res.R; /** @@ -41,17 +40,16 @@ import com.android.systemui.res.R; */ public class AssistDisclosure { private final Context mContext; - private final ViewCaptureAwareWindowManager mWm; + private final WindowManager mWm; private final Handler mHandler; private AssistDisclosureView mView; private boolean mViewAdded; - public AssistDisclosure(Context context, Handler handler, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + public AssistDisclosure(Context context, Handler handler, WindowManager windowManager) { mContext = context; mHandler = handler; - mWm = viewCaptureAwareWindowManager; + mWm = windowManager; } public void postShow() { diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java index 2d44d401b0b0..75ec8dfd881e 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java @@ -24,8 +24,8 @@ import android.provider.Settings; import android.service.voice.VisualQueryAttentionResult; import android.service.voice.VoiceInteractionSession; import android.util.Log; +import android.view.WindowManager; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.app.AssistUtils; import com.android.internal.app.IVisualQueryDetectionAttentionListener; import com.android.internal.app.IVisualQueryRecognitionStatusListener; @@ -199,12 +199,12 @@ public class AssistManager { SelectedUserInteractor selectedUserInteractor, ActivityManager activityManager, AssistInteractor interactor, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + WindowManager windowManager) { mContext = context; mDeviceProvisionedController = controller; mCommandQueue = commandQueue; mAssistUtils = assistUtils; - mAssistDisclosure = new AssistDisclosure(context, uiHandler, viewCaptureAwareWindowManager); + mAssistDisclosure = new AssistDisclosure(context, uiHandler, windowManager); mLauncherProxyService = launcherProxyService; mPhoneStateMonitor = phoneStateMonitor; mAssistLogger = assistLogger; diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java index 6e257442d139..56273eb9a2cf 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java +++ b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java @@ -33,7 +33,6 @@ import android.view.WindowManager; import android.view.animation.PathInterpolator; import android.widget.FrameLayout; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.assist.AssistLogger; @@ -67,7 +66,7 @@ public class DefaultUiController implements AssistManager.UiController { protected InvocationLightsView mInvocationLightsView; protected final AssistLogger mAssistLogger; - private final ViewCaptureAwareWindowManager mWindowManager; + private final WindowManager mWindowManager; private final MetricsLogger mMetricsLogger; private final Lazy<AssistManager> mAssistManagerLazy; private final WindowManager.LayoutParams mLayoutParams; @@ -81,12 +80,12 @@ public class DefaultUiController implements AssistManager.UiController { @Inject public DefaultUiController(Context context, AssistLogger assistLogger, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, - MetricsLogger metricsLogger, Lazy<AssistManager> assistManagerLazy, + WindowManager windowManager, MetricsLogger metricsLogger, + Lazy<AssistManager> assistManagerLazy, NavigationBarController navigationBarController) { mAssistLogger = assistLogger; mRoot = new FrameLayout(context); - mWindowManager = viewCaptureAwareWindowManager; + mWindowManager = windowManager; mMetricsLogger = metricsLogger; mAssistManagerLazy = assistManagerLazy; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index b8e95ee1dbf0..7bb08edd4773 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -20,7 +20,6 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static com.android.internal.jank.InteractionJankMonitor.CUJ_BIOMETRIC_PROMPT_TRANSITION; -import static com.android.systemui.Flags.enableViewCaptureTracing; import android.animation.Animator; import android.annotation.IntDef; @@ -57,8 +56,6 @@ import android.window.OnBackInvokedDispatcher; import androidx.constraintlayout.widget.ConstraintLayout; import com.android.app.animation.Interpolators; -import com.android.app.viewcapture.ViewCapture; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.widget.LockPatternUtils; @@ -78,11 +75,10 @@ import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.res.R; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.util.concurrency.DelayableExecutor; +import com.android.systemui.utils.windowmanager.WindowManagerUtils; import com.google.android.msdl.domain.MSDLPlayer; -import kotlin.Lazy; - import kotlinx.coroutines.CoroutineScope; import java.io.PrintWriter; @@ -132,7 +128,7 @@ public class AuthContainerView extends LinearLayout private final Config mConfig; private final int mEffectiveUserId; private final IBinder mWindowToken = new Binder(); - private final ViewCaptureAwareWindowManager mWindowManager; + private final WindowManager mWindowManager; @Nullable private final AuthContextPlugins mAuthContextPlugins; private final Interpolator mLinearOutSlowIn; private final LockPatternUtils mLockPatternUtils; @@ -298,16 +294,13 @@ public class AuthContainerView extends LinearLayout @NonNull Provider<CredentialViewModel> credentialViewModelProvider, @NonNull @Background DelayableExecutor bgExecutor, @NonNull VibratorHelper vibratorHelper, - Lazy<ViewCapture> lazyViewCapture, @NonNull MSDLPlayer msdlPlayer) { super(config.mContext); mConfig = config; mLockPatternUtils = lockPatternUtils; mEffectiveUserId = userManager.getCredentialOwnerProfile(mConfig.mUserId); - WindowManager wm = getContext().getSystemService(WindowManager.class); - mWindowManager = new ViewCaptureAwareWindowManager(wm, lazyViewCapture, - enableViewCaptureTracing()); + mWindowManager = WindowManagerUtils.getWindowManager(getContext()); mAuthContextPlugins = authContextPlugins; mWakefulnessLifecycle = wakefulnessLifecycle; mApplicationCoroutineScope = applicationCoroutineScope; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 68a282018ba4..f2c071f14466 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -22,7 +22,6 @@ import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR import static android.view.Display.INVALID_DISPLAY; import static com.android.systemui.Flags.contAuthPlugin; -import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy; import android.annotation.NonNull; import android.annotation.Nullable; @@ -67,7 +66,6 @@ import android.view.DisplayInfo; import android.view.MotionEvent; import android.view.WindowManager; -import com.android.app.viewcapture.ViewCapture; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; @@ -93,6 +91,7 @@ import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.concurrency.Execution; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import com.google.android.msdl.domain.MSDLPlayer; @@ -194,7 +193,7 @@ public class AuthController implements @NonNull private final VibratorHelper mVibratorHelper; @NonNull private final MSDLPlayer mMSDLPlayer; - private final kotlin.Lazy<ViewCapture> mLazyViewCapture; + private final WindowManagerProvider mWindowManagerProvider; @VisibleForTesting final TaskStackListener mTaskStackListener = new TaskStackListener() { @@ -693,8 +692,8 @@ public class AuthController implements @NonNull UdfpsUtils udfpsUtils, @NonNull VibratorHelper vibratorHelper, @NonNull KeyguardManager keyguardManager, - Lazy<ViewCapture> daggerLazyViewCapture, - @NonNull MSDLPlayer msdlPlayer) { + @NonNull MSDLPlayer msdlPlayer, + WindowManagerProvider windowManagerProvider) { mContext = context; mExecution = execution; mUserManager = userManager; @@ -755,7 +754,7 @@ public class AuthController implements context.registerReceiver(mBroadcastReceiver, filter, Context.RECEIVER_EXPORTED_UNAUDITED); mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class); - mLazyViewCapture = toKotlinLazy(daggerLazyViewCapture); + mWindowManagerProvider = windowManagerProvider; } // TODO(b/229290039): UDFPS controller should manage its dimensions on its own. Remove this. @@ -1278,7 +1277,7 @@ public class AuthController implements Log.e(TAG, "unable to get Display for user=" + userId); return null; } - return mContext.createDisplayContext(display).getSystemService(WindowManager.class); + return mWindowManagerProvider.getWindowManager(mContext.createDisplayContext(display)); } private void onDialogDismissed(@BiometricPrompt.DismissedReason int reason) { @@ -1337,8 +1336,7 @@ public class AuthController implements return new AuthContainerView(config, mApplicationCoroutineScope, mFpProps, mFaceProps, wakefulnessLifecycle, userManager, mContextPlugins, lockPatternUtils, mInteractionJankMonitor, mPromptSelectorInteractor, viewModel, - mCredentialViewModelProvider, bgExecutor, mVibratorHelper, - mLazyViewCapture, mMSDLPlayer); + mCredentialViewModelProvider, bgExecutor, mVibratorHelper, mMSDLPlayer); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index dfe8eb28b2a6..b16c416fb9df 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -57,12 +57,12 @@ import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; +import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; @@ -145,7 +145,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { private final Execution mExecution; private final FingerprintManager mFingerprintManager; @NonNull private final LayoutInflater mInflater; - private final ViewCaptureAwareWindowManager mWindowManager; + private final WindowManager mWindowManager; private final DelayableExecutor mFgExecutor; @NonNull private final Executor mBiometricExecutor; @NonNull private final StatusBarStateController mStatusBarStateController; @@ -659,7 +659,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull Execution execution, @NonNull @ShadeDisplayAware LayoutInflater inflater, @Nullable FingerprintManager fingerprintManager, - @NonNull ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, + @NonNull @Main WindowManager windowManager, @NonNull StatusBarStateController statusBarStateController, @Main DelayableExecutor fgExecutor, @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager, @@ -705,7 +705,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { // The fingerprint manager is queried for UDFPS before this class is constructed, so the // fingerprint manager should never be null. mFingerprintManager = checkNotNull(fingerprintManager); - mWindowManager = viewCaptureAwareWindowManager; + mWindowManager = windowManager; mFgExecutor = fgExecutor; mStatusBarStateController = statusBarStateController; mKeyguardStateController = keyguardStateController; @@ -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/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index bdf58275effa..61b670715572 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -42,7 +42,6 @@ import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener import androidx.annotation.VisibleForTesting import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor @@ -88,7 +87,7 @@ class UdfpsControllerOverlay constructor( private val context: Context, private val inflater: LayoutInflater, - private val windowManager: ViewCaptureAwareWindowManager, + private val windowManager: WindowManager, private val accessibilityManager: AccessibilityManager, private val statusBarStateController: StatusBarStateController, private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index 54c52b533da4..3b22e13f29a2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -397,7 +397,7 @@ object BiometricViewBinder { // Talkback directional guidance udfpsGuidanceView.setOnHoverListener { _, event -> launch { - viewModel.onAnnounceAccessibilityHint( + viewModel.onUpdateAccessibilityHint( event, accessibilityManager.isTouchExplorationEnabled, ) @@ -406,7 +406,9 @@ object BiometricViewBinder { } launch { viewModel.accessibilityHint.collect { message -> - if (message.isNotBlank()) view.announceForAccessibility(message) + if (message.isNotBlank()) { + udfpsGuidanceView.contentDescription = message + } } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt index 02c378417f3d..fcc0121e8f93 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt @@ -27,7 +27,6 @@ import android.view.View import android.view.ViewGroup import android.view.ViewOutlineProvider import android.view.WindowInsets -import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.widget.ImageView import android.widget.TextView @@ -48,6 +47,7 @@ import com.android.systemui.biometrics.ui.viewmodel.isSmall import com.android.systemui.biometrics.ui.viewmodel.isTop import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R +import com.android.systemui.utils.windowmanager.WindowManagerUtils import kotlin.math.abs import kotlinx.coroutines.flow.combine import com.android.app.tracing.coroutines.launchTraced as launch @@ -66,7 +66,7 @@ object BiometricViewSizeBinder { viewsToHideWhenSmall: List<View>, jankListener: BiometricJankListener, ) { - val windowManager = requireNotNull(view.context.getSystemService(WindowManager::class.java)) + val windowManager = WindowManagerUtils.getWindowManager(view.context) val accessibilityManager = requireNotNull(view.context.getSystemService(AccessibilityManager::class.java)) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index 0902d19b6787..4e17a2658ee7 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -886,7 +886,7 @@ constructor( } /** Sets the message used for UDFPS directional guidance */ - suspend fun onAnnounceAccessibilityHint( + suspend fun onUpdateAccessibilityHint( event: MotionEvent, touchExplorationEnabled: Boolean, ): Boolean { 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/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt index 7d518f4f7e78..718ef51aa161 100644 --- a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt @@ -23,20 +23,19 @@ import android.os.SystemProperties import android.view.Surface import android.view.View import android.view.WindowManager -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.internal.annotations.VisibleForTesting import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger import com.android.settingslib.Utils +import com.android.systemui.res.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import com.android.systemui.res.R +import com.android.systemui.surfaceeffects.ripple.RippleView import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.surfaceeffects.ripple.RippleView import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import javax.inject.Inject @@ -58,7 +57,6 @@ class WiredChargingRippleController @Inject constructor( featureFlags: FeatureFlags, private val context: Context, private val windowManager: WindowManager, - private val viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, private val systemClock: SystemClock, private val uiEventLogger: UiEventLogger ) { @@ -163,12 +161,12 @@ class WiredChargingRippleController @Inject constructor( override fun onViewAttachedToWindow(view: View) { layoutRipple() rippleView.startRipple(Runnable { - viewCaptureAwareWindowManager.removeView(rippleView) + windowManager.removeView(rippleView) }) rippleView.removeOnAttachStateChangeListener(this) } }) - viewCaptureAwareWindowManager.addView(rippleView, windowLayoutParams) + windowManager.addView(rippleView, windowLayoutParams) uiEventLogger.log(WiredChargingRippleEvent.CHARGING_RIPPLE_PLAYED) } diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java index e5e9c4685264..4ca55400561e 100644 --- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java +++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java @@ -28,10 +28,10 @@ import android.util.Slog; import android.view.Gravity; import android.view.WindowManager; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; /** * A WirelessChargingAnimation is a view containing view + animation for wireless charging. @@ -60,11 +60,11 @@ public class WirelessChargingAnimation { */ private WirelessChargingAnimation(@NonNull Context context, @Nullable Looper looper, int transmittingBatteryLevel, int batteryLevel, Callback callback, boolean isDozing, - RippleShape rippleShape, UiEventLogger uiEventLogger, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + RippleShape rippleShape, UiEventLogger uiEventLogger, WindowManager windowManager, + WindowManagerProvider windowManagerProvider) { mCurrentWirelessChargingView = new WirelessChargingView(context, looper, transmittingBatteryLevel, batteryLevel, callback, isDozing, - rippleShape, uiEventLogger, viewCaptureAwareWindowManager); + rippleShape, uiEventLogger, windowManager, windowManagerProvider); } /** @@ -75,11 +75,11 @@ public class WirelessChargingAnimation { public static WirelessChargingAnimation makeWirelessChargingAnimation(@NonNull Context context, @Nullable Looper looper, int transmittingBatteryLevel, int batteryLevel, Callback callback, boolean isDozing, RippleShape rippleShape, - UiEventLogger uiEventLogger, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + UiEventLogger uiEventLogger, WindowManager windowManager, + WindowManagerProvider windowManagerProvider) { return new WirelessChargingAnimation(context, looper, transmittingBatteryLevel, - batteryLevel, callback, isDozing, rippleShape, uiEventLogger, - viewCaptureAwareWindowManager); + batteryLevel, callback, isDozing, rippleShape, uiEventLogger, windowManager, + windowManagerProvider); } /** @@ -88,10 +88,10 @@ public class WirelessChargingAnimation { */ public static WirelessChargingAnimation makeChargingAnimationWithNoBatteryLevel( @NonNull Context context, RippleShape rippleShape, UiEventLogger uiEventLogger, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + WindowManager windowManager, WindowManagerProvider windowManagerProvider) { return makeWirelessChargingAnimation(context, null, UNKNOWN_BATTERY_LEVEL, UNKNOWN_BATTERY_LEVEL, null, false, - rippleShape, uiEventLogger, viewCaptureAwareWindowManager); + rippleShape, uiEventLogger, windowManager, windowManagerProvider); } /** @@ -123,19 +123,21 @@ public class WirelessChargingAnimation { private int mGravity; private WirelessChargingLayout mView; private WirelessChargingLayout mNextView; - private ViewCaptureAwareWindowManager mWM; + private WindowManager mWM; private Callback mCallback; + private WindowManagerProvider mWindowManagerProvider; public WirelessChargingView(Context context, @Nullable Looper looper, int transmittingBatteryLevel, int batteryLevel, Callback callback, boolean isDozing, RippleShape rippleShape, UiEventLogger uiEventLogger, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + WindowManager windowManager, WindowManagerProvider windowManagerProvider) { mCallback = callback; mNextView = new WirelessChargingLayout(context, transmittingBatteryLevel, batteryLevel, isDozing, rippleShape); mGravity = Gravity.CENTER_HORIZONTAL | Gravity.CENTER; mUiEventLogger = uiEventLogger; - mWM = viewCaptureAwareWindowManager; + mWM = windowManager; + mWindowManagerProvider = windowManagerProvider; final WindowManager.LayoutParams params = mParams; params.height = WindowManager.LayoutParams.MATCH_PARENT; @@ -207,6 +209,7 @@ public class WirelessChargingAnimation { if (context == null) { context = mView.getContext(); } + mWM = mWindowManagerProvider.getWindowManager(context); mParams.packageName = packageName; mParams.hideTimeoutMilliseconds = DURATION; diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java index dc3b50c93298..059ea3271235 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayWindow.java @@ -27,7 +27,6 @@ import android.view.Window; import android.view.WindowInsets; import android.view.WindowManager; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.policy.PhoneWindow; import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.OverlayWindowContext; import com.android.systemui.screenshot.FloatingWindowUtil; @@ -45,7 +44,6 @@ public class ClipboardOverlayWindow extends PhoneWindow private final Context mContext; private final WindowManager mWindowManager; - private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private final WindowManager.LayoutParams mWindowLayoutParams; private boolean mKeyboardVisible; @@ -55,7 +53,6 @@ public class ClipboardOverlayWindow extends PhoneWindow @Inject ClipboardOverlayWindow(@OverlayWindowContext Context context, - @OverlayWindowContext ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, @OverlayWindowContext WindowManager windowManager) { super(context); mContext = context; @@ -66,10 +63,9 @@ public class ClipboardOverlayWindow extends PhoneWindow requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS); setBackgroundDrawableResource(android.R.color.transparent); mWindowManager = windowManager; - mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager; mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams(); mWindowLayoutParams.setTitle("ClipboardOverlay"); - setWindowManager(windowManager, null, null); + setWindowManager(mWindowManager, null, null); setWindowFocusable(false); } @@ -86,12 +82,10 @@ public class ClipboardOverlayWindow extends PhoneWindow attach(); withWindowAttached(() -> { - WindowInsets currentInsets = mWindowManager.getCurrentWindowMetrics() - .getWindowInsets(); + WindowInsets currentInsets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); mKeyboardVisible = currentInsets.isVisible(WindowInsets.Type.ime()); peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener(() -> { - WindowInsets insets = mWindowManager.getCurrentWindowMetrics() - .getWindowInsets(); + WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime()); if (keyboardVisible != mKeyboardVisible) { mKeyboardVisible = keyboardVisible; @@ -112,7 +106,7 @@ public class ClipboardOverlayWindow extends PhoneWindow void remove() { final View decorView = peekDecorView(); if (decorView != null && decorView.isAttachedToWindow()) { - mViewCaptureAwareWindowManager.removeViewImmediate(decorView); + mWindowManager.removeViewImmediate(decorView); } } @@ -146,7 +140,7 @@ public class ClipboardOverlayWindow extends PhoneWindow if (decorView.isAttachedToWindow()) { return; } - mViewCaptureAwareWindowManager.addView(decorView, mWindowLayoutParams); + mWindowManager.addView(decorView, mWindowLayoutParams); decorView.requestApplyInsets(); } @@ -167,7 +161,7 @@ public class ClipboardOverlayWindow extends PhoneWindow } final View decorView = peekDecorView(); if (decorView != null && decorView.isAttachedToWindow()) { - mViewCaptureAwareWindowManager.updateViewLayout(decorView, mWindowLayoutParams); + mWindowManager.updateViewLayout(decorView, mWindowLayoutParams); } } } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java index c86a84b17efe..7a60cce63a33 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java @@ -19,9 +19,7 @@ package com.android.systemui.clipboardoverlay.dagger; import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; import static com.android.systemui.Flags.clipboardOverlayMultiuser; -import static com.android.systemui.Flags.enableViewCaptureTracing; import static com.android.systemui.shared.Flags.usePreferredImageEditor; -import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -31,8 +29,6 @@ import android.view.Display; import android.view.LayoutInflater; import android.view.WindowManager; -import com.android.app.viewcapture.ViewCapture; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.clipboardoverlay.ActionIntentCreator; import com.android.systemui.clipboardoverlay.ClipboardOverlayView; import com.android.systemui.clipboardoverlay.DefaultIntentCreator; @@ -40,6 +36,7 @@ import com.android.systemui.clipboardoverlay.IntentCreator; import com.android.systemui.res.R; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.UserTracker; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import dagger.Lazy; import dagger.Module; @@ -89,21 +86,9 @@ public interface ClipboardOverlayModule { */ @Provides @OverlayWindowContext - static WindowManager provideWindowManager(@OverlayWindowContext Context context) { - return context.getSystemService(WindowManager.class); - } - - /** - * - */ - @Provides - @OverlayWindowContext - static ViewCaptureAwareWindowManager provideViewCaptureAwareWindowManager( - @OverlayWindowContext WindowManager windowManager, - Lazy<ViewCapture> daggerLazyViewCapture) { - return new ViewCaptureAwareWindowManager(windowManager, - /* lazyViewCapture= */ toKotlinLazy(daggerLazyViewCapture), - /* isViewCaptureEnabled= */ enableViewCaptureTracing()); + static WindowManager provideWindowManager(@OverlayWindowContext Context context, + WindowManagerProvider windowManagerProvider) { + return windowManagerProvider.getWindowManager(context); } @Provides 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/DevicePosturingListener.kt b/packages/SystemUI/src/com/android/systemui/communal/DevicePosturingListener.kt index af8a5fa23ccb..8bfec0a5dac2 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/DevicePosturingListener.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/DevicePosturingListener.kt @@ -21,8 +21,11 @@ import android.app.DreamManager import android.service.dreams.Flags.allowDreamWhenPostured import com.android.app.tracing.coroutines.launchInTraced import com.android.systemui.CoreStartable +import com.android.systemui.common.domain.interactor.BatteryInteractor +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.posturing.domain.interactor.PosturingInteractor import com.android.systemui.communal.posturing.shared.model.PosturedState +import com.android.systemui.communal.shared.model.WhenToDream import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.dagger.CommunalTableLog @@ -30,10 +33,14 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry +import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @SysUISingleton @@ -42,19 +49,36 @@ class DevicePosturingListener constructor( private val commandRegistry: CommandRegistry, private val dreamManager: DreamManager, - private val interactor: PosturingInteractor, + private val posturingInteractor: PosturingInteractor, + communalSettingsInteractor: CommunalSettingsInteractor, + batteryInteractor: BatteryInteractor, @Background private val bgScope: CoroutineScope, @CommunalTableLog private val tableLogBuffer: TableLogBuffer, ) : CoreStartable { private val command = DevicePosturingCommand() + // Only subscribe to posturing if applicable to avoid running the posturing CHRE nanoapp + // if posturing signal is not needed. + private val postured = + allOf( + batteryInteractor.isDevicePluggedIn, + communalSettingsInteractor.whenToDream.map { it == WhenToDream.WHILE_POSTURED }, + ) + .flatMapLatestConflated { shouldListen -> + if (shouldListen) { + posturingInteractor.postured + } else { + flowOf(false) + } + } + @SuppressLint("MissingPermission") override fun start() { if (!allowDreamWhenPostured()) { return } - interactor.postured + postured .distinctUntilChanged() .logDiffsForTable( tableLogBuffer = tableLogBuffer, @@ -78,7 +102,7 @@ constructor( val state = when (arg.lowercase()) { - "true" -> PosturedState.Postured(confidence = 1f) + "true" -> PosturedState.Postured "false" -> PosturedState.NotPostured "clear" -> PosturedState.Unknown else -> { @@ -87,7 +111,7 @@ constructor( null } } - state?.let { interactor.setValueForDebug(it) } + state?.let { posturingInteractor.setValueForDebug(it) } } override fun help(pw: PrintWriter) { diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt index 20bfabdc5fb9..678a5e2f6a1c 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalAutoOpenInteractor.kt @@ -57,7 +57,15 @@ constructor( allOf(batteryInteractor.isDevicePluggedIn, dockManager.retrieveIsDocked()) } WhenToStartHub.WHILE_CHARGING_AND_POSTURED -> { - allOf(batteryInteractor.isDevicePluggedIn, posturingInteractor.postured) + // Only listen to posturing if applicable to avoid running the posturing + // CHRE nanoapp when not needed. + batteryInteractor.isDevicePluggedIn.flatMapLatestConflated { isCharging -> + if (isCharging) { + posturingInteractor.postured + } else { + flowOf(false) + } + } } WhenToStartHub.NEVER -> flowOf(false) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/posturing/data/model/PositionState.kt b/packages/SystemUI/src/com/android/systemui/communal/posturing/data/model/PositionState.kt new file mode 100644 index 000000000000..21b8dd785f53 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/posturing/data/model/PositionState.kt @@ -0,0 +1,48 @@ +/* + * 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.communal.posturing.data.model + +import androidx.annotation.FloatRange + +data class PositionState( + val stationary: StationaryState = StationaryState.Unknown, + val orientation: OrientationState = OrientationState.Unknown, +) { + sealed interface StationaryState { + @get:FloatRange(from = 0.0, to = 1.0) val confidence: Float + + data object Unknown : StationaryState { + override val confidence: Float = 0f + } + + data class Stationary(override val confidence: Float) : StationaryState + + data class NotStationary(override val confidence: Float) : StationaryState + } + + sealed interface OrientationState { + @get:FloatRange(from = 0.0, to = 1.0) val confidence: Float + + data object Unknown : OrientationState { + override val confidence: Float = 0f + } + + data class Postured(override val confidence: Float) : OrientationState + + data class NotPostured(override val confidence: Float) : OrientationState + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/posturing/data/repository/NoOpPosturingRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/posturing/data/repository/NoOpPosturingRepository.kt index c5f357f556ca..d826685886d9 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/posturing/data/repository/NoOpPosturingRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/posturing/data/repository/NoOpPosturingRepository.kt @@ -16,7 +16,7 @@ package com.android.systemui.communal.posturing.data.repository -import com.android.systemui.communal.posturing.shared.model.PosturedState +import com.android.systemui.communal.posturing.data.model.PositionState import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -25,6 +25,6 @@ import kotlinx.coroutines.flow.asStateFlow @SysUISingleton class NoOpPosturingRepository @Inject constructor() : PosturingRepository { - override val posturedState: Flow<PosturedState> = - MutableStateFlow(PosturedState.Unknown).asStateFlow() + override val positionState: Flow<PositionState> = + MutableStateFlow(PositionState()).asStateFlow() } diff --git a/packages/SystemUI/src/com/android/systemui/communal/posturing/data/repository/PosturingRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/posturing/data/repository/PosturingRepository.kt index dae1a47f5be0..4de0a1e21d35 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/posturing/data/repository/PosturingRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/posturing/data/repository/PosturingRepository.kt @@ -16,7 +16,7 @@ package com.android.systemui.communal.posturing.data.repository -import com.android.systemui.communal.posturing.shared.model.PosturedState +import com.android.systemui.communal.posturing.data.model.PositionState import kotlinx.coroutines.flow.Flow /** @@ -25,5 +25,5 @@ import kotlinx.coroutines.flow.Flow */ interface PosturingRepository { /** Whether the device is currently stationary and upright. */ - val posturedState: Flow<PosturedState> + val positionState: Flow<PositionState> } diff --git a/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt index cd81dea9cad1..e487590d87d7 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt @@ -16,26 +16,211 @@ package com.android.systemui.communal.posturing.domain.interactor +import android.annotation.SuppressLint +import android.hardware.Sensor +import android.hardware.TriggerEvent +import android.hardware.TriggerEventListener +import android.service.dreams.Flags.allowDreamWhenPostured +import com.android.systemui.communal.posturing.data.model.PositionState import com.android.systemui.communal.posturing.data.repository.PosturingRepository import com.android.systemui.communal.posturing.shared.model.PosturedState import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalLog +import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf +import com.android.systemui.util.kotlin.slidingWindow +import com.android.systemui.util.sensors.AsyncSensorManager +import com.android.systemui.util.time.SystemClock +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineDispatcher +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.combine +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn @SysUISingleton -class PosturingInteractor @Inject constructor(repository: PosturingRepository) { - private val debugPostured = MutableStateFlow<PosturedState>(PosturedState.Unknown) +class PosturingInteractor +@Inject +constructor( + repository: PosturingRepository, + private val asyncSensorManager: AsyncSensorManager, + @Application private val applicationScope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher, + @CommunalLog private val logBuffer: LogBuffer, + clock: SystemClock, +) { + private val logger = Logger(logBuffer, TAG) - val postured: Flow<Boolean> = - combine(repository.posturedState, debugPostured) { postured, debugValue -> - debugValue.asBoolean() ?: postured.asBoolean() ?: false - } + private val debugPostured = MutableStateFlow<PosturedState>(PosturedState.Unknown) fun setValueForDebug(value: PosturedState) { debugPostured.value = value } + + /** + * Detects whether or not the device is stationary, applying a sliding window smoothing + * algorithm. + */ + private val stationarySmoothed: Flow<Boolean> = + merge( + observeTriggerSensor(Sensor.TYPE_PICK_UP_GESTURE) + // If pickup detected, avoid triggering posturing at all within the sliding + // window by emitting a negative infinity value. + .map { Float.NEGATIVE_INFINITY } + .onEach { logger.i("pickup gesture detected") }, + observeTriggerSensor(Sensor.TYPE_SIGNIFICANT_MOTION) + // If motion detected, avoid triggering posturing at all within the sliding + // window by emitting a negative infinity value. + .map { Float.NEGATIVE_INFINITY } + .onEach { logger.i("significant motion detected") }, + repository.positionState + .map { it.stationary } + .filterNot { it is PositionState.StationaryState.Unknown } + .map { stationaryState -> + if (stationaryState is PositionState.StationaryState.Stationary) { + stationaryState.confidence + } else { + // If not stationary, then we should effectively disable posturing by + // emitting the lowest possible confidence. + Float.NEGATIVE_INFINITY + } + }, + ) + .slidingWindow(SLIDING_WINDOW_DURATION, clock) + .filterNot { it.isEmpty() } + .map { window -> + val avgStationaryConfidence = window.average() + logger.i({ "stationary confidence: $double1 | window: $str1" }) { + str1 = window.formatWindowForDebugging() + double1 = avgStationaryConfidence + } + avgStationaryConfidence > CONFIDENCE_THRESHOLD + } + + /** + * Detects whether or not the device is in an upright orientation, applying a sliding window + * smoothing algorithm. + */ + private val orientationSmoothed: Flow<Boolean> = + repository.positionState + .map { it.orientation } + .filterNot { it is PositionState.OrientationState.Unknown } + .map { orientationState -> + if (orientationState is PositionState.OrientationState.Postured) { + orientationState.confidence + } else { + // If not postured, then we should effectively disable posturing by + // emitting the lowest possible confidence. + Float.NEGATIVE_INFINITY + } + } + .slidingWindow(SLIDING_WINDOW_DURATION, clock) + .filterNot { it.isEmpty() } + .map { window -> + val avgOrientationConfidence = window.average() + logger.i({ "orientation confidence: $double1 | window: $str1" }) { + str1 = window.formatWindowForDebugging() + double1 = avgOrientationConfidence + } + avgOrientationConfidence > CONFIDENCE_THRESHOLD + } + + /** + * Posturing is composed of the device being stationary and in the correct orientation. If both + * conditions are met, then consider it postured. + */ + private val posturedSmoothed: Flow<PosturedState> = + allOf(stationarySmoothed, orientationSmoothed) + .map { postured -> + if (postured) { + PosturedState.Postured + } else { + PosturedState.NotPostured + } + } + .flowOn(bgDispatcher) + .stateIn( + scope = applicationScope, + // Avoid losing the smoothing history if the user plug/unplugs rapidly. + started = + SharingStarted.WhileSubscribed( + stopTimeoutMillis = STOP_TIMEOUT_AFTER_UNSUBSCRIBE.inWholeMilliseconds, + replayExpirationMillis = 0, + ), + initialValue = PosturedState.Unknown, + ) + + /** + * Whether the device is postured. + * + * NOTE: Due to smoothing, this signal may be delayed to ensure we have a stable reading before + * being considered postured. + */ + val postured: Flow<Boolean> by lazy { + if (allowDreamWhenPostured()) { + combine(posturedSmoothed, debugPostured) { postured, debugValue -> + debugValue.asBoolean() ?: postured.asBoolean() ?: false + } + } else { + MutableStateFlow(false) + } + } + + /** + * Helper for observing a trigger sensor, which automatically unregisters itself after it + * executes once. + */ + private fun observeTriggerSensor(type: Int): Flow<Unit> = conflatedCallbackFlow { + val sensor = asyncSensorManager.getDefaultSensor(type) + val isRegistered = AtomicBoolean(false) + + fun registerCallbackInternal(callback: TriggerEventListener) { + if (isRegistered.compareAndSet(false, true)) { + asyncSensorManager.requestTriggerSensor(callback, sensor) + } + } + + val callback = + object : TriggerEventListener() { + override fun onTrigger(event: TriggerEvent) { + trySend(Unit) + if (isRegistered.getAndSet(false)) { + registerCallbackInternal(this) + } + } + } + + if (sensor != null) { + registerCallbackInternal(callback) + } + + awaitClose { + if (isRegistered.getAndSet(false)) { + asyncSensorManager.cancelTriggerSensor(callback, sensor) + } + } + } + + companion object { + const val TAG = "PosturingInteractor" + val SLIDING_WINDOW_DURATION = 10.seconds + const val CONFIDENCE_THRESHOLD = 0.8f + val STOP_TIMEOUT_AFTER_UNSUBSCRIBE = 5.seconds + } } fun PosturedState.asBoolean(): Boolean? { @@ -45,3 +230,8 @@ fun PosturedState.asBoolean(): Boolean? { PosturedState.Unknown -> null } } + +@SuppressLint("DefaultLocale") +fun List<Float>.formatWindowForDebugging(): String { + return joinToString(prefix = "[", postfix = "]") { String.format("%.2f", it) } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/posturing/shared/model/PosturedState.kt b/packages/SystemUI/src/com/android/systemui/communal/posturing/shared/model/PosturedState.kt index 431ca67315eb..c71cf14c4b52 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/posturing/shared/model/PosturedState.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/posturing/shared/model/PosturedState.kt @@ -18,7 +18,7 @@ package com.android.systemui.communal.posturing.shared.model sealed interface PosturedState { /** Represents postured state */ - data class Postured(val confidence: Float) : PosturedState + data object Postured : PosturedState /** Represents unknown/uninitialized state */ data object Unknown : PosturedState 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/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 7354f4096801..f45bafdfb17e 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -16,9 +16,6 @@ package com.android.systemui.dagger; -import static com.android.systemui.Flags.enableViewCaptureTracing; -import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy; - import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; @@ -103,7 +100,6 @@ import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.telephony.satellite.SatelliteManager; -import android.view.Choreographer; import android.view.CrossWindowBlurListeners; import android.view.IWindowManager; import android.view.LayoutInflater; @@ -115,13 +111,9 @@ import android.view.accessibility.CaptioningManager; import android.view.inputmethod.InputMethodManager; import android.view.textclassifier.TextClassificationManager; -import androidx.annotation.NonNull; import androidx.asynclayoutinflater.view.AsyncLayoutInflater; import androidx.core.app.NotificationManagerCompat; -import com.android.app.viewcapture.ViewCapture; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; -import com.android.app.viewcapture.ViewCaptureFactory; import com.android.internal.app.IBatteryStats; import com.android.internal.appwidget.IAppWidgetService; import com.android.internal.jank.InteractionJankMonitor; @@ -135,8 +127,9 @@ import com.android.systemui.dagger.qualifiers.TestHarness; import com.android.systemui.shared.system.PackageManagerWrapper; import com.android.systemui.user.utils.UserScopedService; import com.android.systemui.user.utils.UserScopedServiceImpl; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; +import com.android.systemui.utils.windowmanager.WindowManagerProviderImpl; -import dagger.Lazy; import dagger.Module; import dagger.Provides; @@ -713,38 +706,23 @@ public class FrameworkServicesModule { @Provides @Singleton - static WindowManager provideWindowManager(Context context) { - return context.getSystemService(WindowManager.class); - } - - /** A window manager working for the default display only. */ - @Provides - @Singleton - @Main - static WindowManager provideMainWindowManager(WindowManager windowManager) { - return windowManager; + static WindowManagerProvider provideWindowManagerProvider() { + return new WindowManagerProviderImpl(); } @Provides @Singleton - static ViewCaptureAwareWindowManager provideViewCaptureAwareWindowManager( - WindowManager windowManager, Lazy<ViewCapture> daggerLazyViewCapture) { - return new ViewCaptureAwareWindowManager(windowManager, - /* lazyViewCapture= */ toKotlinLazy(daggerLazyViewCapture), - /* isViewCaptureEnabled= */ enableViewCaptureTracing()); + static WindowManager provideWindowManager(Context context, + WindowManagerProvider windowManagerProvider) { + return windowManagerProvider.getWindowManager(context); } + /** A window manager working for the default display only. */ @Provides @Singleton - static ViewCaptureAwareWindowManager.Factory viewCaptureAwareWindowManagerFactory( - Lazy<ViewCapture> daggerLazyViewCapture) { - return new ViewCaptureAwareWindowManager.Factory() { - @NonNull - @Override - public ViewCaptureAwareWindowManager create(@NonNull WindowManager windowManager) { - return provideViewCaptureAwareWindowManager(windowManager, daggerLazyViewCapture); - } - }; + @Main + static WindowManager provideMainWindowManager(WindowManager windowManager) { + return windowManager; } @Provides @@ -835,12 +813,6 @@ public class FrameworkServicesModule { @Provides @Singleton - static ViewCapture provideViewCapture(Context context) { - return ViewCaptureFactory.getInstance(context); - } - - @Provides - @Singleton @Nullable static SupervisionManager provideSupervisionManager(Context context) { return (SupervisionManager) context.getSystemService(Context.SUPERVISION_SERVICE); diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt index 7be323073692..9c3b9b273ab5 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/view/UdfpsAccessibilityOverlay.kt @@ -20,4 +20,8 @@ import android.content.Context import android.view.View /** Overlay to handle under-fingerprint sensor accessibility events. */ -class UdfpsAccessibilityOverlay(context: Context?) : View(context) +class UdfpsAccessibilityOverlay(context: Context?) : View(context) { + init { + accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_ASSERTIVE + } +} diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt index fa849bf5e413..1849bf20abdb 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/viewmodel/UdfpsAccessibilityOverlayViewModel.kt @@ -56,7 +56,7 @@ abstract class UdfpsAccessibilityOverlayViewModel( event.getPointerId(0), event, overlayParams, /* rotateToPortrait */ - false + false, ) if ( @@ -64,7 +64,7 @@ abstract class UdfpsAccessibilityOverlayViewModel( event.getPointerId(0), event, overlayParams, - /* rotateTouchToPortrait */ false + /* rotateTouchToPortrait */ false, ) ) { // view only receives motionEvents when [visible] which requires touchExplorationEnabled @@ -75,10 +75,10 @@ abstract class UdfpsAccessibilityOverlayViewModel( scaledTouch.x, scaledTouch.y, overlayParams, - /* touchRotatedToPortrait */ false + /* touchRotatedToPortrait */ false, ) if (announceStr != null) { - v.announceForAccessibility(announceStr) + v.contentDescription = announceStr } } // always let the motion events go through to underlying views diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt index 3390640fa6c6..aaaaacef001a 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt @@ -31,6 +31,7 @@ import com.android.systemui.display.shared.model.DisplayWindowProperties import com.android.systemui.res.R import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.utils.windowmanager.WindowManagerUtils import com.google.common.collect.HashBasedTable import com.google.common.collect.Table import java.io.PrintWriter @@ -110,7 +111,7 @@ constructor( return null } @SuppressLint("NonInjectedService") // Need to manually get the service - val windowManager = context.getSystemService(WindowManager::class.java) as WindowManager + val windowManager = WindowManagerUtils.getWindowManager(context) val layoutInflater = LayoutInflater.from(context) DisplayWindowProperties(displayId, windowType, context, windowManager, layoutInflater) } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index a2bcb98e36fe..fd716eea799a 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -46,7 +46,6 @@ import androidx.lifecycle.LifecycleService; import androidx.lifecycle.ServiceLifecycleDispatcher; import androidx.lifecycle.ViewModelStore; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.compose.animation.scene.OverlayKey; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; @@ -117,7 +116,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ @Nullable private final ComponentName mHomeControlPanelDreamComponent; private final UiEventLogger mUiEventLogger; - private final ViewCaptureAwareWindowManager mWindowManager; + private final WindowManager mWindowManager; private final String mWindowTitle; // A reference to the {@link Window} used to hold the dream overlay. @@ -378,7 +377,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ Context context, DreamOverlayLifecycleOwner lifecycleOwner, @Main DelayableExecutor executor, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, + WindowManager windowManager, ComplicationComponent.Factory complicationComponentFactory, DreamComplicationComponent.Factory dreamComplicationComponentFactory, DreamOverlayComponent.Factory dreamOverlayComponentFactory, @@ -403,7 +402,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ super(executor); mContext = context; mExecutor = executor; - mWindowManager = viewCaptureAwareWindowManager; + mWindowManager = windowManager; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mScrimManager = scrimManager; mLowLightDreamComponent = lowLightDreamComponent; diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/docking/binder/KeyboardDockingIndicationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyboard/docking/binder/KeyboardDockingIndicationViewBinder.kt index c2974a8d5429..c08a8e297174 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/docking/binder/KeyboardDockingIndicationViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/docking/binder/KeyboardDockingIndicationViewBinder.kt @@ -20,7 +20,6 @@ import android.content.Context import android.graphics.Paint import android.graphics.PixelFormat import android.view.WindowManager -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyboard.docking.ui.KeyboardDockingIndicationView @@ -38,7 +37,7 @@ constructor( context: Context, @Application private val applicationScope: CoroutineScope, private val viewModel: KeyboardDockingIndicationViewModel, - private val windowManager: ViewCaptureAwareWindowManager, + private val windowManager: WindowManager ) { private val windowLayoutParams = 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/FromAlternateBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt index a3796ab5ee27..cc03e49b462b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt @@ -145,7 +145,7 @@ constructor( KeyguardState.GLANCEABLE_HUB } else if (isOccluded && !isDreaming) { KeyguardState.OCCLUDED - } else if (hubV2 && isDreaming) { + } else if (isDreaming) { KeyguardState.DREAMING } else if (hubV2 && isIdleOnCommunal) { if (SceneContainerFlag.isEnabled) return@collect 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/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/SideFpsProgressBar.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/SideFpsProgressBar.kt index 1fd609dba637..853f1769994e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/SideFpsProgressBar.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/SideFpsProgressBar.kt @@ -26,7 +26,6 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.WindowManager import android.widget.ProgressBar import androidx.core.view.isGone -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.res.R import javax.inject.Inject @@ -38,7 +37,7 @@ class SideFpsProgressBar @Inject constructor( private val layoutInflater: LayoutInflater, - private val windowManager: ViewCaptureAwareWindowManager, + private val windowManager: WindowManager, ) { private var overlayView: View? = null @@ -91,7 +90,7 @@ constructor( ) { if (overlayView == null) { overlayView = layoutInflater.inflate(R.layout.sidefps_progress_bar, null, false) - windowManager.addView(requireNotNull(overlayView), overlayViewParams) + windowManager.addView(overlayView, overlayViewParams) progressBar?.pivotX = 0.0f progressBar?.pivotY = 0.0f } 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/controls/domain/pipeline/MediaDataFilterImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt index 2cd5016cb206..d053dd2b54d1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt @@ -248,7 +248,7 @@ constructor( // Update loading state with actual active value mediaFilterRepository.selectedUserEntries.value[lastActiveId]?.let { mediaFilterRepository.addMediaDataLoadingState( - MediaDataLoadingModel.Loaded(lastActiveId, immediately) + MediaDataLoadingModel.Loaded(lastActiveId) ) mediaLogger.logMediaLoaded(lastActiveId, it.active, "expiring reactivated id") listeners.forEach { listener -> diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt index c8a02faea58a..323e5cb76aba 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt @@ -26,13 +26,10 @@ sealed class MediaDataLoadingModel { /** Media data has been loaded. */ data class Loaded( override val instanceId: InstanceId, - val immediatelyUpdateUi: Boolean = true, val receivedSmartspaceCardLatency: Int = 0, val isSsReactivated: Boolean = false, ) : MediaDataLoadingModel() /** Media data has been removed. */ - data class Removed( - override val instanceId: InstanceId, - ) : MediaDataLoadingModel() + data class Removed(override val instanceId: InstanceId) : MediaDataLoadingModel() } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt index 71b3223b77be..800220ee962a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt @@ -68,7 +68,7 @@ import com.android.systemui.media.controls.ui.view.MediaHostState import com.android.systemui.media.controls.ui.view.MediaScrollView import com.android.systemui.media.controls.ui.view.MediaViewHolder import com.android.systemui.media.controls.ui.viewmodel.MediaCarouselViewModel -import com.android.systemui.media.controls.ui.viewmodel.MediaCommonViewModel +import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager @@ -324,8 +324,8 @@ constructor( private var widthInSceneContainerPx = 0 private var heightInSceneContainerPx = 0 - private val controllerById = mutableMapOf<String, MediaViewController>() - private val commonViewModels = mutableListOf<MediaCommonViewModel>() + private val controllerById = mutableMapOf<InstanceId, MediaViewController>() + private val controlViewModels = mutableListOf<MediaControlViewModel>() private val isOnGone = keyguardTransitionInteractor @@ -566,10 +566,10 @@ constructor( private fun listenForMediaItemsChanges(scope: CoroutineScope): Job { return scope.launch { mediaCarouselViewModel.mediaItems.collectLatest { - val diffUtilCallback = MediaViewModelCallback(commonViewModels, it) + val diffUtilCallback = MediaViewModelCallback(controlViewModels, it) val listUpdateCallback = MediaViewModelListUpdateCallback( - old = commonViewModels, + old = controlViewModels, new = it, onAdded = this@MediaCarouselController::onAdded, onUpdated = this@MediaCarouselController::onUpdated, @@ -590,7 +590,7 @@ constructor( } private fun onAdded( - commonViewModel: MediaCommonViewModel, + controlViewModel: MediaControlViewModel, position: Int, configChanged: Boolean = false, ) { @@ -601,64 +601,52 @@ constructor( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, ) - when (commonViewModel) { - is MediaCommonViewModel.MediaControl -> { - val viewHolder = MediaViewHolder.create(LayoutInflater.from(context), mediaContent) - viewController.widthInSceneContainerPx = widthInSceneContainerPx - viewController.heightInSceneContainerPx = heightInSceneContainerPx - viewController.attachPlayer(viewHolder) - viewController.mediaViewHolder?.player?.layoutParams = lp - if (configChanged) { - commonViewModel.controlViewModel.onMediaConfigChanged() - } - MediaControlViewBinder.bind( - viewHolder, - commonViewModel.controlViewModel, - viewController, - falsingManager, - backgroundDispatcher, - mainDispatcher, - ) - mediaContent.addView(viewHolder.player, position) - controllerById[commonViewModel.instanceId.toString()] = viewController - } + val viewHolder = MediaViewHolder.create(LayoutInflater.from(context), mediaContent) + viewController.widthInSceneContainerPx = widthInSceneContainerPx + viewController.heightInSceneContainerPx = heightInSceneContainerPx + viewController.attachPlayer(viewHolder) + viewController.mediaViewHolder?.player?.layoutParams = lp + if (configChanged) { + controlViewModel.onMediaConfigChanged() } + MediaControlViewBinder.bind( + viewHolder, + controlViewModel, + viewController, + falsingManager, + backgroundDispatcher, + mainDispatcher, + ) + mediaContent.addView(viewHolder.player, position) + controllerById[controlViewModel.instanceId] = viewController viewController.setListening(mediaCarouselScrollHandler.visibleToUser && currentlyExpanded) updateViewControllerToState(viewController, noAnimation = true) updatePageIndicator() - if ( - commonViewModel is MediaCommonViewModel.MediaControl && commonViewModel.isMediaFromRec - ) { - mediaCarouselScrollHandler.scrollToPlayer( - mediaCarouselScrollHandler.visibleMediaIndex, - destIndex = 0, - ) - } mediaCarouselScrollHandler.onPlayersChanged() mediaFrame.requiresRemeasuring = true - commonViewModel.onAdded(commonViewModel) + controlViewModel.onAdded(controlViewModel) } - private fun onUpdated(commonViewModel: MediaCommonViewModel, position: Int) { - commonViewModel.onUpdated(commonViewModel) + private fun onUpdated(controlViewModel: MediaControlViewModel, position: Int) { + controlViewModel.onUpdated(controlViewModel) updatePageIndicator() mediaCarouselScrollHandler.onPlayersChanged() } - private fun onRemoved(commonViewModel: MediaCommonViewModel) { - val id = (commonViewModel as MediaCommonViewModel.MediaControl).instanceId.toString() + private fun onRemoved(controlViewModel: MediaControlViewModel) { + val id = controlViewModel.instanceId controllerById.remove(id)?.let { mediaCarouselScrollHandler.onPrePlayerRemoved(it.mediaViewHolder!!.player) mediaContent.removeView(it.mediaViewHolder!!.player) it.onDestroy() mediaCarouselScrollHandler.onPlayersChanged() updatePageIndicator() - commonViewModel.onRemoved(true) + controlViewModel.onRemoved(true) } } - private fun onMoved(commonViewModel: MediaCommonViewModel, from: Int, to: Int) { - val id = (commonViewModel as MediaCommonViewModel.MediaControl).instanceId.toString() + private fun onMoved(controlViewModel: MediaControlViewModel, from: Int, to: Int) { + val id = controlViewModel.instanceId controllerById[id]?.let { mediaContent.removeViewAt(from) mediaContent.addView(it.mediaViewHolder!!.player, to) @@ -667,19 +655,12 @@ constructor( mediaCarouselScrollHandler.onPlayersChanged() } - private fun setNewViewModelsList(viewModels: List<MediaCommonViewModel>) { - commonViewModels.clear() - commonViewModels.addAll(viewModels) + private fun setNewViewModelsList(viewModels: List<MediaControlViewModel>) { + controlViewModels.clear() + controlViewModels.addAll(viewModels) // Ensure we only show the needed UMOs in media carousel. - val viewIds = - viewModels - .map { mediaCommonViewModel -> - (mediaCommonViewModel as MediaCommonViewModel.MediaControl) - .instanceId - .toString() - } - .toHashSet() + val viewIds = viewModels.map { controlViewModel -> controlViewModel.instanceId }.toHashSet() controllerById .filter { !viewIds.contains(it.key) } .forEach { @@ -976,9 +957,8 @@ constructor( ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator)) if (recreateMedia) { mediaContent.removeAllViews() - commonViewModels.forEachIndexed { index, viewModel -> - val mediaControlViewModel = (viewModel as MediaCommonViewModel.MediaControl) - controllerById[mediaControlViewModel.instanceId.toString()]?.onDestroy() + controlViewModels.forEachIndexed { index, viewModel -> + controllerById[viewModel.instanceId]?.onDestroy() onAdded(viewModel, index, configChanged = true) } } @@ -1309,7 +1289,7 @@ constructor( println("dataKeys: ${MediaPlayerData.dataKeys()}") println("orderedPlayerSortKeys: ${MediaPlayerData.playerKeys()}") println("visiblePlayerSortKeys: ${MediaPlayerData.visiblePlayerKeys()}") - println("commonViewModels: $commonViewModels") + println("controlViewModels: $controlViewModels") println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}") println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}") println("current size: $currentCarouselWidth x $currentCarouselHeight") diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelCallback.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelCallback.kt index 2fc44ad3cce6..ad2d46263faf 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelCallback.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelCallback.kt @@ -17,12 +17,12 @@ package com.android.systemui.media.controls.ui.util import androidx.recyclerview.widget.DiffUtil -import com.android.systemui.media.controls.ui.viewmodel.MediaCommonViewModel +import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel /** A [DiffUtil.Callback] to calculate difference between old and new media view-model list. */ class MediaViewModelCallback( - private val old: List<MediaCommonViewModel>, - private val new: List<MediaCommonViewModel>, + private val old: List<MediaControlViewModel>, + private val new: List<MediaControlViewModel>, ) : DiffUtil.Callback() { override fun getOldListSize(): Int { @@ -36,27 +36,12 @@ class MediaViewModelCallback( override fun areItemsTheSame(oldIndex: Int, newIndex: Int): Boolean { val oldItem = old[oldIndex] val newItem = new[newIndex] - return if ( - oldItem is MediaCommonViewModel.MediaControl && - newItem is MediaCommonViewModel.MediaControl - ) { - oldItem.instanceId == newItem.instanceId - } else { - false - } + return oldItem.instanceId == newItem.instanceId } override fun areContentsTheSame(oldIndex: Int, newIndex: Int): Boolean { val oldItem = old[oldIndex] val newItem = new[newIndex] - return if ( - oldItem is MediaCommonViewModel.MediaControl && - newItem is MediaCommonViewModel.MediaControl - ) { - oldItem.immediatelyUpdateUi == newItem.immediatelyUpdateUi && - oldItem.updateTime == newItem.updateTime - } else { - false - } + return oldItem.updateTime == newItem.updateTime } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt index 6022b7b1fc13..1db8c58d4140 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt @@ -17,17 +17,17 @@ package com.android.systemui.media.controls.ui.util import androidx.recyclerview.widget.ListUpdateCallback -import com.android.systemui.media.controls.ui.viewmodel.MediaCommonViewModel +import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel import kotlin.math.min /** A [ListUpdateCallback] to apply media events needed to reach the new state. */ class MediaViewModelListUpdateCallback( - private val old: List<MediaCommonViewModel>, - private val new: List<MediaCommonViewModel>, - private val onAdded: (MediaCommonViewModel, Int) -> Unit, - private val onUpdated: (MediaCommonViewModel, Int) -> Unit, - private val onRemoved: (MediaCommonViewModel) -> Unit, - private val onMoved: (MediaCommonViewModel, Int, Int) -> Unit, + private val old: List<MediaControlViewModel>, + private val new: List<MediaControlViewModel>, + private val onAdded: (MediaControlViewModel, Int) -> Unit, + private val onUpdated: (MediaControlViewModel, Int) -> Unit, + private val onRemoved: (MediaControlViewModel) -> Unit, + private val onMoved: (MediaControlViewModel, Int, Int) -> Unit, ) : ListUpdateCallback { override fun onInserted(position: Int, count: Int) { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt index dfaee4434bcf..54d3151694bd 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt @@ -56,7 +56,7 @@ constructor( val hasAnyMediaOrRecommendations: StateFlow<Boolean> = interactor.hasAnyMediaOrRecommendation val hasActiveMediaOrRecommendations: StateFlow<Boolean> = interactor.hasActiveMediaOrRecommendation - val mediaItems: StateFlow<List<MediaCommonViewModel>> = + val mediaItems: StateFlow<List<MediaControlViewModel>> = interactor.currentMedia .map { sortedItems -> val mediaList = buildList { @@ -91,8 +91,7 @@ constructor( var updateHostVisibility: () -> Unit = {} - private val mediaControlByInstanceId = - mutableMapOf<InstanceId, MediaCommonViewModel.MediaControl>() + private val mediaControlByInstanceId = mutableMapOf<InstanceId, MediaControlViewModel>() private var modelsPendingRemoval: MutableSet<MediaCommonModel> = mutableSetOf() @@ -108,18 +107,16 @@ constructor( interactor.reorderMedia() } - private fun toViewModel( - commonModel: MediaCommonModel.MediaControl - ): MediaCommonViewModel.MediaControl { + private fun toViewModel(commonModel: MediaCommonModel.MediaControl): MediaControlViewModel { val instanceId = commonModel.mediaLoadedModel.instanceId - return mediaControlByInstanceId[instanceId]?.copy( - immediatelyUpdateUi = commonModel.mediaLoadedModel.immediatelyUpdateUi, - updateTime = commonModel.updateTime, - ) - ?: MediaCommonViewModel.MediaControl( + return mediaControlByInstanceId[instanceId]?.copy(updateTime = commonModel.updateTime) + ?: MediaControlViewModel( + applicationContext = applicationContext, + backgroundDispatcher = backgroundDispatcher, + backgroundExecutor = backgroundExecutor, + interactor = controlInteractorFactory.create(instanceId), + logger = logger, instanceId = instanceId, - immediatelyUpdateUi = commonModel.mediaLoadedModel.immediatelyUpdateUi, - controlViewModel = createMediaControlViewModel(instanceId), onAdded = { mediaLogger.logMediaCardAdded(instanceId) onMediaControlAddedOrUpdated(it, commonModel) @@ -130,31 +127,20 @@ constructor( mediaLogger.logMediaCardRemoved(instanceId) }, onUpdated = { onMediaControlAddedOrUpdated(it, commonModel) }, - isMediaFromRec = commonModel.isMediaFromRec, updateTime = commonModel.updateTime, ) .also { mediaControlByInstanceId[instanceId] = it } } - private fun createMediaControlViewModel(instanceId: InstanceId): MediaControlViewModel { - return MediaControlViewModel( - applicationContext = applicationContext, - backgroundDispatcher = backgroundDispatcher, - backgroundExecutor = backgroundExecutor, - interactor = controlInteractorFactory.create(instanceId), - logger = logger, - ) - } - private fun onMediaControlAddedOrUpdated( - commonViewModel: MediaCommonViewModel, + controlViewModel: MediaControlViewModel, commonModel: MediaCommonModel.MediaControl, ) { if (commonModel.canBeRemoved && !Utils.useMediaResumption(applicationContext)) { // This media control is due for removal as it is now paused + timed out, and resumption // setting is off. if (isReorderingAllowed()) { - commonViewModel.onRemoved(true) + controlViewModel.onRemoved(true) } else { modelsPendingRemoval.add(commonModel) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCommonViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCommonViewModel.kt deleted file mode 100644 index d493d57051f7..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCommonViewModel.kt +++ /dev/null @@ -1,38 +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.media.controls.ui.viewmodel - -import com.android.internal.logging.InstanceId - -/** Models media view model UI state. */ -sealed class MediaCommonViewModel { - - abstract val onAdded: (MediaCommonViewModel) -> Unit - abstract val onRemoved: (Boolean) -> Unit - abstract val onUpdated: (MediaCommonViewModel) -> Unit - - data class MediaControl( - val instanceId: InstanceId, - val immediatelyUpdateUi: Boolean, - val controlViewModel: MediaControlViewModel, - override val onAdded: (MediaCommonViewModel) -> Unit, - override val onRemoved: (Boolean) -> Unit, - override val onUpdated: (MediaCommonViewModel) -> Unit, - val isMediaFromRec: Boolean = false, - val updateTime: Long = 0, - ) : MediaCommonViewModel() -} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt index 015274a10330..31cfb8479dd1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt @@ -45,12 +45,17 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map /** Models UI state and handles user input for a media control. */ -class MediaControlViewModel( +data class MediaControlViewModel( @Application private val applicationContext: Context, @Background private val backgroundDispatcher: CoroutineDispatcher, @Background private val backgroundExecutor: Executor, private val interactor: MediaControlInteractor, private val logger: MediaUiEventLogger, + val instanceId: InstanceId, + val onAdded: (MediaControlViewModel) -> Unit, + val onRemoved: (Boolean) -> Unit, + val onUpdated: (MediaControlViewModel) -> Unit, + val updateTime: Long = 0, ) { val player: Flow<MediaPlayerViewModel?> = interactor.mediaControl diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java index ac1672db9375..e3990d25f94e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java @@ -237,8 +237,7 @@ public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<Recycl clickListener = v -> onItemClick(v, device); } } else { - deviceStatusIcon = getDeviceStatusIcon(device, - device.hasOngoingSession()); + deviceStatusIcon = getDeviceStatusIcon(device); clickListener = getClickListenerBasedOnSelectionBehavior(device); } deviceDisabled = clickListener == null; @@ -302,12 +301,8 @@ public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<Recycl } @Nullable - private Drawable getDeviceStatusIcon(MediaDevice device, boolean hasOngoingSession) { - if (hasOngoingSession) { - return mContext.getDrawable(R.drawable.ic_sound_bars_anim); - } else { - return Api34Impl.getDeviceStatusIconBasedOnSelectionBehavior(device, mContext); - } + private Drawable getDeviceStatusIcon(MediaDevice device) { + return Api34Impl.getDeviceStatusIconBasedOnSelectionBehavior(device, mContext); } protected void onExpandGroupButtonClicked() { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java index 6ab4a52dc919..7ab6b3cfb8b1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java @@ -231,7 +231,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { updateFullItemClickListener(clickListener); updateContentAlpha(deviceDisabled); updateSubtitle(subtitle); - updateDeviceStatusIcon(deviceStatusIcon); + updateDeviceStatusIcon(deviceStatusIcon, ongoingSessionStatus, connectionState); updateItemBackground(connectionState); } @@ -523,11 +523,20 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { mStatusIcon.setAlpha(alphaValue); } - private void updateDeviceStatusIcon(@Nullable Drawable deviceStatusIcon) { - if (deviceStatusIcon == null) { + private void updateDeviceStatusIcon(@Nullable Drawable deviceStatusIcon, + @Nullable OngoingSessionStatus ongoingSessionStatus, + ConnectionState connectionState) { + boolean showOngoingSession = + ongoingSessionStatus != null && connectionState == ConnectionState.DISCONNECTED; + if (deviceStatusIcon == null && !showOngoingSession) { mStatusIcon.setVisibility(View.GONE); } else { - mStatusIcon.setImageDrawable(deviceStatusIcon); + if (showOngoingSession) { + mStatusIcon.setImageDrawable( + mContext.getDrawable(R.drawable.ic_sound_bars_anim)); + } else { + mStatusIcon.setImageDrawable(deviceStatusIcon); + } mStatusIcon.setImageTintList(ColorStateList.valueOf( mController.getColorSchemeLegacy().getColorItemContent())); if (deviceStatusIcon instanceof AnimatedVectorDrawable) { diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt index 2a23620839e5..6a2c4519e1a5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt @@ -35,7 +35,6 @@ import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager import com.android.app.animation.Interpolators -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.internal.logging.InstanceId import com.android.internal.widget.CachingIconView import com.android.systemui.common.shared.model.ContentDescription @@ -73,7 +72,7 @@ constructor( private val commandQueue: CommandQueue, context: Context, logger: MediaTttReceiverLogger, - viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, + windowManager: WindowManager, @Main private val mainExecutor: DelayableExecutor, accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, @@ -90,7 +89,7 @@ constructor( TemporaryViewDisplayController<ChipReceiverInfo, MediaTttReceiverLogger>( context, logger, - viewCaptureAwareWindowManager, + windowManager, mainExecutor, accessibilityManager, configurationController, diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt index 9265bfb2f66b..5ffa7fa0ef8d 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt @@ -26,13 +26,13 @@ import android.graphics.Shader import android.util.AttributeSet import android.view.View import android.view.WindowManager -import androidx.core.content.getSystemService import androidx.core.content.res.use import com.android.systemui.res.R import com.android.systemui.mediaprojection.appselector.data.RecentTask import com.android.systemui.shared.recents.model.ThumbnailData import com.android.systemui.shared.recents.utilities.PreviewPositionHelper import com.android.systemui.shared.recents.utilities.Utilities.isLargeScreen +import com.android.systemui.utils.windowmanager.WindowManagerUtils /** * Custom view that shows a thumbnail preview of one recent task based on [ThumbnailData]. @@ -53,7 +53,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 } } - private val windowManager: WindowManager = context.getSystemService()!! + private val windowManager: WindowManager = WindowManagerUtils.getWindowManager(context) private val paint = Paint(Paint.ANTI_ALIAS_FLAG) private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = defaultBackgroundColor } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProvider.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProvider.kt index c829471f53f3..57fd1e790f0b 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProvider.kt @@ -19,6 +19,7 @@ package com.android.systemui.mediaprojection.appselector.view import android.content.Context import android.content.res.Configuration import android.graphics.Rect +import android.view.WindowManager import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorScope @@ -36,6 +37,7 @@ constructor( private val context: Context, private val windowMetricsProvider: WindowMetricsProvider, private val configurationController: ConfigurationController, + private val windowManager: WindowManager, ) : CallbackController<TaskPreviewSizeListener>, ConfigurationListener, DefaultLifecycleObserver { /** Returns the size of the task preview on the screen in pixels */ @@ -65,7 +67,7 @@ constructor( val width = maxWindowBounds.width() var height = maximumWindowHeight - val isLargeScreen = isLargeScreen(context) + val isLargeScreen = isLargeScreen(windowManager, context.resources) if (isLargeScreen) { val taskbarSize = windowMetricsProvider.currentWindowInsets.bottom height -= taskbarSize 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/navigationbar/NavigationBarModule.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarModule.java index 39482bea0111..c5c8c01f8b39 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarModule.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarModule.java @@ -16,17 +16,12 @@ package com.android.systemui.navigationbar; -import static com.android.systemui.Flags.enableViewCaptureTracing; -import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy; - import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; import com.android.app.displaylib.PerDisplayRepository; -import com.android.app.viewcapture.ViewCapture; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.NavigationBarComponent.NavigationBarScope; @@ -34,8 +29,8 @@ import com.android.systemui.navigationbar.views.NavigationBarFrame; import com.android.systemui.navigationbar.views.NavigationBarView; import com.android.systemui.res.R; import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; -import dagger.Lazy; import dagger.Module; import dagger.Provides; @@ -70,8 +65,9 @@ public interface NavigationBarModule { @Provides @NavigationBarScope @DisplayId - static WindowManager provideWindowManager(@DisplayId Context context) { - return context.getSystemService(WindowManager.class); + static WindowManager provideWindowManager(@DisplayId Context context, + WindowManagerProvider windowManagerProvider) { + return windowManagerProvider.getWindowManager(context); } /** A SysUiState for the navigation bar display. */ @@ -87,15 +83,4 @@ public interface NavigationBarModule { return defaultState; } } - - /** A ViewCaptureAwareWindowManager specific to the display's context. */ - @Provides - @NavigationBarScope - @DisplayId - static ViewCaptureAwareWindowManager provideViewCaptureAwareWindowManager( - @DisplayId WindowManager windowManager, Lazy<ViewCapture> daggerLazyViewCapture) { - return new ViewCaptureAwareWindowManager(windowManager, - /* lazyViewCapture= */ toKotlinLazy(daggerLazyViewCapture), - /* isViewCaptureEnabled= */ enableViewCaptureTracing()); - } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt index 44c828731e24..6d7492686985 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt @@ -33,7 +33,6 @@ import androidx.annotation.VisibleForTesting import androidx.core.os.postDelayed import androidx.core.view.isVisible import androidx.dynamicanimation.animation.DynamicAnimation -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.internal.jank.Cuj import com.android.internal.jank.InteractionJankMonitor import com.android.internal.util.LatencyTracker @@ -85,7 +84,7 @@ class BackPanelController @AssistedInject constructor( @Assisted context: Context, - private val windowManager: ViewCaptureAwareWindowManager, + private val windowManager: WindowManager, private val viewConfiguration: ViewConfiguration, @Assisted private val mainHandler: Handler, private val systemClock: SystemClock, diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java index 8b5b3adeef1f..ad0acbdaf702 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java @@ -104,7 +104,6 @@ import android.view.inputmethod.InputMethodManager; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; @@ -199,7 +198,6 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private final Context mContext; private final Bundle mSavedState; private final WindowManager mWindowManager; - private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private final AccessibilityManager mAccessibilityManager; private final DeviceProvisionedController mDeviceProvisionedController; private final StatusBarStateController mStatusBarStateController; @@ -560,7 +558,6 @@ public class NavigationBar extends ViewController<NavigationBarView> implements @Nullable Bundle savedState, @DisplayId Context context, @DisplayId WindowManager windowManager, - @DisplayId ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, Lazy<AssistManager> assistManagerLazy, AccessibilityManager accessibilityManager, DeviceProvisionedController deviceProvisionedController, @@ -605,7 +602,6 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mContext = context; mSavedState = savedState; mWindowManager = windowManager; - mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager; mAccessibilityManager = accessibilityManager; mDeviceProvisionedController = deviceProvisionedController; mStatusBarStateController = statusBarStateController; @@ -726,7 +722,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements if (DEBUG) Log.v(TAG, "addNavigationBar: about to add " + mView); try { - mViewCaptureAwareWindowManager.addView( + mWindowManager.addView( mFrame, getBarLayoutParams( mContext.getResources() @@ -783,7 +779,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mCommandQueue.removeCallback(this); Trace.beginSection("NavigationBar#removeViewImmediate"); try { - mViewCaptureAwareWindowManager.removeViewImmediate(mView.getRootView()); + mWindowManager.removeViewImmediate(mView.getRootView()); } catch (IllegalArgumentException e) { // Wrapping this in a try/catch to avoid crashes when a display is instantly removed // after being added, and initialization hasn't finished yet. @@ -888,7 +884,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements resetSecondaryHandle(); getBarTransitions().removeDarkIntensityListener(mOrientationHandleIntensityListener); try { - mViewCaptureAwareWindowManager.removeView(mOrientationHandle); + mWindowManager.removeView(mOrientationHandle); } catch (IllegalArgumentException e) { // Wrapping this in a try/catch to avoid crashes when a display is instantly removed // after being added, and initialization hasn't finished yet. @@ -967,7 +963,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mOrientationParams.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT; try { - mViewCaptureAwareWindowManager.addView(mOrientationHandle, mOrientationParams); + mWindowManager.addView(mOrientationHandle, mOrientationParams); } catch (WindowManager.InvalidDisplayException e) { // Wrapping this in a try/catch to avoid crashes when a display is instantly removed // after being added, and initialization hasn't finished yet. diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java index cbc4c26b2f94..d2974e9f90bd 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java @@ -87,6 +87,7 @@ import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.statusbar.phone.AutoHideController; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.LightBarTransitionsController; +import com.android.systemui.utils.windowmanager.WindowManagerUtils; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.pip.Pip; @@ -729,7 +730,7 @@ public class NavigationBarView extends FrameLayout { } else { return; } - WindowManager wm = getContext().getSystemService(WindowManager.class); + WindowManager wm = WindowManagerUtils.getWindowManager(getContext()); wm.updateViewLayout((View) getParent(), lp); } } 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/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt index 9319961f5b68..0a5222e3df44 100644 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt @@ -21,8 +21,7 @@ import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.UserActionResult.HideOverlay -import com.android.compose.animation.scene.UserActionResult.ShowOverlay -import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays +import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.ui.viewmodel.SceneContainerArea import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel @@ -38,11 +37,8 @@ class NotificationsShadeOverlayActionsViewModel @AssistedInject constructor() : mapOf( Swipe.Up to HideOverlay(Overlays.NotificationsShade), Back to HideOverlay(Overlays.NotificationsShade), - Swipe.Down(fromSource = SceneContainerArea.EndHalf) to - ShowOverlay( - Overlays.QuickSettingsShade, - hideCurrentOverlays = HideCurrentOverlays.Some(Overlays.NotificationsShade), - ), + Swipe.Down(fromSource = SceneContainerArea.TopEdgeEndHalf) to + ReplaceByOverlay(Overlays.QuickSettingsShade), ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/power/InattentiveSleepWarningView.java b/packages/SystemUI/src/com/android/systemui/power/InattentiveSleepWarningView.java index 2ecca2d8c776..f37a86e2b9d5 100644 --- a/packages/SystemUI/src/com/android/systemui/power/InattentiveSleepWarningView.java +++ b/packages/SystemUI/src/com/android/systemui/power/InattentiveSleepWarningView.java @@ -16,8 +16,6 @@ package com.android.systemui.power; -import static com.android.systemui.Flags.enableViewCaptureTracing; - import android.animation.Animator; import android.animation.AnimatorInflater; import android.animation.AnimatorListenerAdapter; @@ -30,28 +28,21 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; - -import com.android.app.viewcapture.ViewCapture; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.res.R; -import kotlin.Lazy; - /** * View that shows a warning shortly before the device goes into sleep * after prolonged user inactivity when bound to. */ public class InattentiveSleepWarningView extends FrameLayout { private final IBinder mWindowToken = new Binder(); - private final ViewCaptureAwareWindowManager mWindowManager; + private final WindowManager mWindowManager; private Animator mFadeOutAnimator; private boolean mDismissing; - InattentiveSleepWarningView(Context context, Lazy<ViewCapture> lazyViewCapture) { + InattentiveSleepWarningView(Context context, WindowManager windowManager) { super(context); - WindowManager wm = mContext.getSystemService(WindowManager.class); - mWindowManager = new ViewCaptureAwareWindowManager(wm, lazyViewCapture, - enableViewCaptureTracing()); + mWindowManager = windowManager; final LayoutInflater layoutInflater = LayoutInflater.from(mContext); layoutInflater.inflate(R.layout.inattentive_sleep_warning, this, true /* attachToRoot */); diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java index 861a7ce282af..4e1bfd15e58c 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java @@ -16,8 +16,6 @@ package com.android.systemui.power; -import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy; - import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; @@ -42,11 +40,11 @@ import android.service.vr.IVrStateCallbacks; import android.text.format.DateUtils; import android.util.Log; import android.util.Slog; +import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.app.viewcapture.ViewCapture; import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.fuelgauge.Estimate; import com.android.settingslib.utils.ThreadUtils; @@ -59,8 +57,6 @@ import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.policy.ConfigurationController; -import kotlin.Lazy; - import java.io.PrintWriter; import java.util.Arrays; import java.util.concurrent.Future; @@ -120,9 +116,9 @@ public class PowerUI implements private IThermalEventListener mSkinThermalEventListener; private IThermalEventListener mUsbThermalEventListener; private final Context mContext; + private final WindowManager mWindowManager; private final BroadcastDispatcher mBroadcastDispatcher; private final CommandQueue mCommandQueue; - private final Lazy<ViewCapture> mLazyViewCapture; @Nullable private final IVrManager mVrManager; private final WakefulnessLifecycle.Observer mWakefulnessObserver = @@ -164,7 +160,7 @@ public class PowerUI implements WakefulnessLifecycle wakefulnessLifecycle, PowerManager powerManager, UserTracker userTracker, - dagger.Lazy<ViewCapture> daggerLazyViewCapture) { + WindowManager windowManager) { mContext = context; mBroadcastDispatcher = broadcastDispatcher; mCommandQueue = commandQueue; @@ -174,7 +170,7 @@ public class PowerUI implements mPowerManager = powerManager; mWakefulnessLifecycle = wakefulnessLifecycle; mUserTracker = userTracker; - mLazyViewCapture = toKotlinLazy(daggerLazyViewCapture); + mWindowManager = windowManager; } public void start() { @@ -649,7 +645,7 @@ public class PowerUI implements @Override public void showInattentiveSleepWarning() { if (mOverlayView == null) { - mOverlayView = new InattentiveSleepWarningView(mContext, mLazyViewCapture); + mOverlayView = new InattentiveSleepWarningView(mContext, mWindowManager); } mOverlayView.show(); 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 b889c3e61837..9f04f69bd0bf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -19,6 +19,8 @@ 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 @@ -125,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 @@ -241,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 }, @@ -276,11 +277,6 @@ constructor( } } .graphicsLayer { alpha = viewModel.viewAlpha } - .thenIf(notificationScrimClippingParams.isEnabled) { - Modifier.notificationScrimClip { - notificationScrimClippingParams.params - } - } .thenIf(!Flags.notificationShadeBlur()) { Modifier.offset { IntOffset( @@ -1061,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) 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/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/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/ModesDndTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesDndTile.kt index 43e3de2040d7..0236256ec9ae 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesDndTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesDndTile.kt @@ -37,6 +37,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.asQSTileIcon +import com.android.systemui.qs.flags.QsInCompose import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.qs.tiles.base.shared.model.QSTileConfigProvider @@ -88,7 +89,11 @@ constructor( init { lifecycle.coroutineScope.launch { - lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { + lifecycle.repeatOnLifecycle( + // TODO: b/403434908 - Workaround for "not listening to tile updates". Can be reset + // to RESUMED if either b/403434908 is fixed or QsInCompose is inlined. + if (QsInCompose.isEnabled) Lifecycle.State.RESUMED else Lifecycle.State.CREATED + ) { dataInteractor.tileData().collect { refreshState(it) } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt index fcfa46f07ab4..6a842c31a3aa 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt @@ -31,7 +31,6 @@ import com.android.systemui.animation.Expandable import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.modes.shared.ModesUi -import com.android.systemui.modes.shared.ModesUiIcons import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.qs.QSTile @@ -40,6 +39,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.asQSTileIcon +import com.android.systemui.qs.flags.QsInCompose import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.qs.tiles.base.shared.model.QSTileConfigProvider @@ -88,10 +88,12 @@ constructor( private val config = qsTileConfigProvider.getConfig(TILE_SPEC) init { - /* Check if */ ModesUiIcons.isUnexpectedlyInLegacyMode() - lifecycle.coroutineScope.launch { - lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { + lifecycle.repeatOnLifecycle( + // TODO: b/403434908 - Workaround for "not listening to tile updates". Can be reset + // to RESUMED if either b/403434908 is fixed or QsInCompose is inlined. + if (QsInCompose.isEnabled) Lifecycle.State.RESUMED else Lifecycle.State.CREATED + ) { dataInteractor.tileData().collect { refreshState(it) } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java index 6b5a22a4fc09..945e051606b9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentController.java @@ -65,7 +65,6 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.logging.UiEventLogger; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; @@ -186,7 +185,7 @@ public class InternetDetailsContentController implements AccessPointController.A private GlobalSettings mGlobalSettings; private int mDefaultDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; private ConnectivityManager.NetworkCallback mConnectivityManagerNetworkCallback; - private ViewCaptureAwareWindowManager mWindowManager; + private WindowManager mWindowManager; private ToastFactory mToastFactory; private SignalDrawable mSignalDrawable; private SignalDrawable mSecondarySignalDrawable; // For the secondary mobile data sub in DSDS @@ -252,7 +251,7 @@ public class InternetDetailsContentController implements AccessPointController.A @Main Handler handler, @Main Executor mainExecutor, BroadcastDispatcher broadcastDispatcher, KeyguardUpdateMonitor keyguardUpdateMonitor, GlobalSettings globalSettings, KeyguardStateController keyguardStateController, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, ToastFactory toastFactory, + @ShadeDisplayAware WindowManager windowManager, ToastFactory toastFactory, @Background Handler workerHandler, CarrierConfigTracker carrierConfigTracker, LocationController locationController, @@ -284,7 +283,7 @@ public class InternetDetailsContentController implements AccessPointController.A mAccessPointController = accessPointController; mWifiIconInjector = new WifiUtils.InternetIconInjector(mContext); mConnectivityManagerNetworkCallback = new DataConnectivityListener(); - mWindowManager = viewCaptureAwareWindowManager; + mWindowManager = windowManager; mToastFactory = toastFactory; mSignalDrawable = new SignalDrawable(mContext); mSecondarySignalDrawable = new SignalDrawable(mContext); diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt index f9b1a36621b2..11622be3d417 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt @@ -77,7 +77,7 @@ constructor( val showMedia: Boolean by hydrator.hydratedStateOf( traceName = "showMedia", - source = mediaCarouselInteractor.hasActiveMediaOrRecommendation, + source = mediaCarouselInteractor.hasAnyMediaOrRecommendation, ) override suspend fun onActivated(): Nothing { diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt index 52c4e2fac6d5..2f7bc0992411 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt @@ -21,8 +21,7 @@ import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.UserActionResult.HideOverlay -import com.android.compose.animation.scene.UserActionResult.ShowOverlay -import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays +import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.ui.viewmodel.SceneContainerArea @@ -47,12 +46,8 @@ constructor(private val editModeViewModel: EditModeViewModel) : UserActionsViewM put(Back, HideOverlay(Overlays.QuickSettingsShade)) } put( - Swipe.Down(fromSource = SceneContainerArea.StartHalf), - ShowOverlay( - Overlays.NotificationsShade, - hideCurrentOverlays = - HideCurrentOverlays.Some(Overlays.QuickSettingsShade), - ), + Swipe.Down(fromSource = SceneContainerArea.TopEdgeStartHalf), + ReplaceByOverlay(Overlays.NotificationsShade), ) } } 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/recents/ScreenPinningRequest.java b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java index 432a35a1a3dd..a1281ec23f92 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java +++ b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java @@ -16,9 +16,7 @@ package com.android.systemui.recents; -import static com.android.systemui.Flags.enableViewCaptureTracing; import static com.android.systemui.shared.recents.utilities.Utilities.isLargeScreen; -import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy; import static com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE; import static com.android.systemui.util.leak.RotationUtils.ROTATION_NONE; import static com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE; @@ -55,8 +53,6 @@ import android.widget.TextView; import androidx.annotation.NonNull; -import com.android.app.viewcapture.ViewCapture; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.CoreStartable; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; @@ -87,7 +83,6 @@ public class ScreenPinningRequest implements private final Lazy<NavigationBarController> mNavigationBarControllerLazy; private final AccessibilityManager mAccessibilityService; private final WindowManager mWindowManager; - private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private final BroadcastDispatcher mBroadcastDispatcher; private final UserTracker mUserTracker; @@ -112,15 +107,12 @@ public class ScreenPinningRequest implements Lazy<NavigationBarController> navigationBarControllerLazy, BroadcastDispatcher broadcastDispatcher, UserTracker userTracker, - Lazy<ViewCapture> daggerLazyViewCapture) { + WindowManager windowManager) { mContext = context; mNavigationBarControllerLazy = navigationBarControllerLazy; mAccessibilityService = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); - mWindowManager = (WindowManager) - mContext.getSystemService(Context.WINDOW_SERVICE); - mViewCaptureAwareWindowManager = new ViewCaptureAwareWindowManager(mWindowManager, - toKotlinLazy(daggerLazyViewCapture), enableViewCaptureTracing()); + mWindowManager = windowManager; mNavBarMode = navigationModeController.addListener(this); mBroadcastDispatcher = broadcastDispatcher; mUserTracker = userTracker; @@ -131,7 +123,7 @@ public class ScreenPinningRequest implements public void clearPrompt() { if (mRequestWindow != null) { - mViewCaptureAwareWindowManager.removeView(mRequestWindow); + mWindowManager.removeView(mRequestWindow); mRequestWindow = null; } } @@ -152,7 +144,7 @@ public class ScreenPinningRequest implements // show the confirmation WindowManager.LayoutParams lp = getWindowLayoutParams(); - mViewCaptureAwareWindowManager.addView(mRequestWindow, lp); + mWindowManager.addView(mRequestWindow, lp); } @Override 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/scene/ui/viewmodel/SceneContainerSwipeDetector.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt index ede453dbe6b3..760b97e6245b 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt @@ -58,6 +58,22 @@ sealed class SceneContainerArea(private val resolveArea: (LayoutDirection) -> Re } ) + data object TopEdgeStartHalf : + SceneContainerArea( + resolveArea = { + if (it == LayoutDirection.Ltr) Resolved.TopEdgeLeftHalf + else Resolved.TopEdgeRightHalf + } + ) + + data object TopEdgeEndHalf : + SceneContainerArea( + resolveArea = { + if (it == LayoutDirection.Ltr) Resolved.TopEdgeRightHalf + else Resolved.TopEdgeLeftHalf + } + ) + override fun resolve(layoutDirection: LayoutDirection): Resolved { return resolveArea(layoutDirection) } @@ -72,6 +88,12 @@ sealed class SceneContainerArea(private val resolveArea: (LayoutDirection) -> Re data object RightEdge : Resolved data object RightHalf : Resolved + + /** The left half of the top edge of the display. */ + data object TopEdgeLeftHalf : Resolved + + /** The right half of the top edge of the display. */ + data object TopEdgeRightHalf : Resolved } } @@ -108,9 +130,14 @@ class SceneContainerSwipeDetector(val edgeSize: Dp) : SwipeSourceDetector { Edge.Resolved.Left -> SceneContainerArea.Resolved.LeftEdge Edge.Resolved.Bottom -> SceneContainerArea.Resolved.BottomEdge Edge.Resolved.Right -> SceneContainerArea.Resolved.RightEdge - else -> { - // Note: This intentionally includes Edge.Resolved.Top. At the moment, we don't need - // to detect swipes on the top edge, and consider them part of the right/left half. + Edge.Resolved.Top -> { + if (position.x < layoutSize.width * 0.5f) { + SceneContainerArea.Resolved.TopEdgeLeftHalf + } else { + SceneContainerArea.Resolved.TopEdgeRightHalf + } + } + null -> { if (position.x < layoutSize.width * 0.5f) { SceneContainerArea.Resolved.LeftHalf } else { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotWindow.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotWindow.kt index c4fe7a428084..644e12cba6fc 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotWindow.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotWindow.kt @@ -31,7 +31,6 @@ import android.view.Window import android.view.WindowInsets import android.view.WindowManager import android.window.WindowContext -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.internal.policy.PhoneWindow import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -42,7 +41,6 @@ class ScreenshotWindow @AssistedInject constructor( private val windowManager: WindowManager, - private val viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, private val context: Context, @Assisted private val display: Display, ) { @@ -97,7 +95,7 @@ constructor( Log.d(TAG, "attachWindow") } attachRequested = true - viewCaptureAwareWindowManager.addView(decorView, params) + windowManager.addView(decorView, params) decorView.requestApplyInsets() decorView.requireViewById<ViewGroup>(R.id.content).apply { @@ -135,7 +133,7 @@ constructor( if (LogConfig.DEBUG_WINDOW) { Log.d(TAG, "Removing screenshot window") } - viewCaptureAwareWindowManager.removeViewImmediate(decorView) + windowManager.removeViewImmediate(decorView) detachRequested = false } if (attachRequested && !detachRequested) { 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/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index c671f7d9db14..4ad4ab3410d6 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -102,6 +102,7 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.SplitShadeStateController; import com.android.systemui.util.LargeScreenUtils; import com.android.systemui.util.kotlin.JavaAdapter; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import dalvik.annotation.optimization.NeverCompile; @@ -299,6 +300,8 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum private final Runnable mQsCollapseExpandAction = this::collapseOrExpandQs; private final QS.ScrollListener mQsScrollListener = this::onScroll; + private final WindowManagerProvider mWindowManagerProvider; + @Inject public QuickSettingsControllerImpl( Lazy<NotificationPanelViewController> panelViewControllerLazy, @@ -336,7 +339,8 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum CastController castController, SplitShadeStateController splitShadeStateController, Lazy<CommunalTransitionViewModel> communalTransitionViewModelLazy, - Lazy<LargeScreenHeaderHelper> largeScreenHeaderHelperLazy + Lazy<LargeScreenHeaderHelper> largeScreenHeaderHelperLazy, + WindowManagerProvider windowManagerProvider ) { SceneContainerFlag.assertInLegacyMode(); mPanelViewControllerLazy = panelViewControllerLazy; @@ -387,6 +391,8 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum mLockscreenShadeTransitionController.addCallback(new LockscreenShadeTransitionCallback()); dumpManager.registerDumpable(this); + + mWindowManagerProvider = windowManagerProvider; } @VisibleForTesting @@ -532,7 +538,7 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum * on ACTION_DOWN, and safely queried repeatedly thereafter during ACTION_MOVE events. */ void updateGestureInsetsCache() { - WindowManager wm = this.mPanelView.getContext().getSystemService(WindowManager.class); + WindowManager wm = mWindowManagerProvider.getWindowManager(this.mPanelView.getContext()); WindowMetrics windowMetrics = wm.getCurrentWindowMetrics(); mCachedGestureInsets = windowMetrics.getWindowInsets().getInsets( WindowInsets.Type.systemGestures()); 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/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt index cd224735cc62..446d4b450edc 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt @@ -49,6 +49,8 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackRebind import com.android.systemui.statusbar.phone.ConfigurationControllerImpl import com.android.systemui.statusbar.phone.ConfigurationForwarder import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.utils.windowmanager.WindowManagerProvider +import com.android.systemui.utils.windowmanager.WindowManagerUtils import dagger.Module import dagger.Provides import dagger.multibindings.ClassKey @@ -111,9 +113,10 @@ object ShadeDisplayAwareModule { fun provideShadeWindowManager( defaultWindowManager: WindowManager, @ShadeDisplayAware context: Context, + windowManagerProvider: WindowManagerProvider ): WindowManager { return if (ShadeWindowGoesAround.isEnabled) { - context.getSystemService(WindowManager::class.java) as WindowManager + windowManagerProvider.getWindowManager(context) } else { defaultWindowManager } 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 88242762da78..c264e3525026 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 @@ -276,15 +276,17 @@ constructor( data object Weak : HeaderChipHighlight { override fun backgroundColor(colorScheme: ColorScheme): Color = - colorScheme.primary.copy(alpha = 0.1f) + colorScheme.surface.copy(alpha = 0.1f) - override fun foregroundColor(colorScheme: ColorScheme): Color = colorScheme.primary + override fun foregroundColor(colorScheme: ColorScheme): Color = colorScheme.onSurface } data object Strong : HeaderChipHighlight { - override fun backgroundColor(colorScheme: ColorScheme): Color = colorScheme.secondary + override fun backgroundColor(colorScheme: ColorScheme): Color = + colorScheme.primaryContainer - override fun foregroundColor(colorScheme: ColorScheme): Color = colorScheme.onSecondary + override fun foregroundColor(colorScheme: ColorScheme): Color = + colorScheme.onPrimaryContainer } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt index cf3b08c041be..8d2d9efb56eb 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt @@ -71,6 +71,8 @@ fun dualShadeActions(): Array<Pair<UserAction, UserActionResult>> { Swipe.Down to ShowOverlay(Overlays.NotificationsShade), Swipe.Down(fromSource = SceneContainerArea.EndHalf) to ShowOverlay(Overlays.QuickSettingsShade), + Swipe.Down(fromSource = SceneContainerArea.TopEdgeEndHalf) to + ShowOverlay(Overlays.QuickSettingsShade), ) } 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/ImmersiveModeConfirmation.java b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java index 97e62d79b374..2a9a47d83dd4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java @@ -28,9 +28,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION; import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED; import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID; -import static com.android.systemui.Flags.enableViewCaptureTracing; -import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy; - import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; import android.annotation.NonNull; @@ -76,16 +73,13 @@ import android.widget.Button; import android.widget.FrameLayout; import android.widget.RelativeLayout; -import com.android.app.viewcapture.ViewCapture; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.CoreStartable; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.res.R; import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.util.settings.SecureSettings; - -import kotlin.Lazy; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import javax.inject.Inject; @@ -112,13 +106,14 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca private long mShowDelayMs = 0L; private final IBinder mWindowToken = new Binder(); private final CommandQueue mCommandQueue; + private final WindowManagerProvider mWindowManagerProvider; private ClingWindowView mClingWindow; /** The wrapper on the last {@link WindowManager} used to add the confirmation window. */ @Nullable - private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; + private WindowManager mWindowManager; /** - * The WindowContext that is registered with {@link #mViewCaptureAwareWindowManager} with + * The WindowContext that is registered with {@link #mWindowManager} with * options to specify the {@link RootDisplayArea} to attach the confirmation window. */ @Nullable @@ -136,21 +131,18 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca private ContentObserver mContentObserver; - private Lazy<ViewCapture> mLazyViewCapture; - @Inject public ImmersiveModeConfirmation(Context context, CommandQueue commandQueue, - SecureSettings secureSettings, - dagger.Lazy<ViewCapture> daggerLazyViewCapture, - @Background Handler backgroundHandler) { + SecureSettings secureSettings, @Background Handler backgroundHandler, + WindowManagerProvider windowManagerProvider) { mSysUiContext = context; final Display display = mSysUiContext.getDisplay(); mDisplayContext = display.getDisplayId() == DEFAULT_DISPLAY ? mSysUiContext : mSysUiContext.createDisplayContext(display); mCommandQueue = commandQueue; mSecureSettings = secureSettings; - mLazyViewCapture = toKotlinLazy(daggerLazyViewCapture); mBackgroundHandler = backgroundHandler; + mWindowManagerProvider = windowManagerProvider; } boolean loadSetting(int currentUserId) { @@ -257,14 +249,14 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca private void handleHide() { if (mClingWindow != null) { if (DEBUG) Log.d(TAG, "Hiding immersive mode confirmation"); - if (mViewCaptureAwareWindowManager != null) { + if (mWindowManager != null) { try { - mViewCaptureAwareWindowManager.removeView(mClingWindow); + mWindowManager.removeView(mClingWindow); } catch (WindowManager.InvalidDisplayException e) { Log.w(TAG, "Fail to hide the immersive confirmation window because of " + e); } - mViewCaptureAwareWindowManager = null; + mWindowManager = null; mWindowContext = null; } mClingWindow = null; @@ -525,8 +517,8 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca * confirmation window. */ @NonNull - private ViewCaptureAwareWindowManager createWindowManager(int rootDisplayAreaId) { - if (mViewCaptureAwareWindowManager != null) { + private WindowManager createWindowManager(int rootDisplayAreaId) { + if (mWindowManager != null) { throw new IllegalStateException( "Must not create a new WindowManager while there is an existing one"); } @@ -535,10 +527,8 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca mWindowContextRootDisplayAreaId = rootDisplayAreaId; mWindowContext = mDisplayContext.createWindowContext( IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE, options); - WindowManager wm = mWindowContext.getSystemService(WindowManager.class); - mViewCaptureAwareWindowManager = new ViewCaptureAwareWindowManager(wm, mLazyViewCapture, - enableViewCaptureTracing()); - return mViewCaptureAwareWindowManager; + mWindowManager = mWindowManagerProvider.getWindowManager(mWindowContext); + return mWindowManager; } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java index ef0660fbcd1c..3d7d08910502 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java @@ -79,6 +79,7 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; import com.android.systemui.res.R; import com.android.systemui.statusbar.phone.CentralSurfaces; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetDialog; @@ -148,43 +149,44 @@ public final class KeyboardShortcutListSearch { private KeyCharacterMap mBackupKeyCharacterMap; @VisibleForTesting - KeyboardShortcutListSearch(Context context, WindowManager windowManager, int deviceId) { + KeyboardShortcutListSearch(Context context, @NonNull WindowManager windowManager, + int deviceId) { this.mContext = new ContextThemeWrapper( context, R.style.KeyboardShortcutHelper); this.mPackageManager = AppGlobals.getPackageManager(); - if (windowManager != null) { - this.mWindowManager = windowManager; - } else { - this.mWindowManager = mContext.getSystemService(WindowManager.class); - } + this.mWindowManager = windowManager; loadResources(this.mContext); createHardcodedShortcuts(deviceId); } - private static KeyboardShortcutListSearch getInstance(Context context, int deviceId) { + private static KeyboardShortcutListSearch getInstance(Context context, int deviceId, + WindowManagerProvider windowManagerProvider) { if (sInstance == null) { - sInstance = new KeyboardShortcutListSearch(context, null, deviceId); + WindowManager windowManager = windowManagerProvider.getWindowManager(context); + sInstance = new KeyboardShortcutListSearch(context, windowManager, deviceId); } return sInstance; } - public static void show(Context context, int deviceId) { + public static void show(Context context, int deviceId, + WindowManagerProvider windowManagerProvider) { MetricsLogger.visible(context, MetricsProto.MetricsEvent.KEYBOARD_SHORTCUTS_HELPER); synchronized (sLock) { if (sInstance != null && !sInstance.mContext.equals(context)) { dismiss(); } - getInstance(context, deviceId).showKeyboardShortcuts(deviceId); + getInstance(context, deviceId, windowManagerProvider).showKeyboardShortcuts(deviceId); } } - public static void toggle(Context context, int deviceId) { + public static void toggle(Context context, int deviceId, + WindowManagerProvider windowManagerProvider) { synchronized (sLock) { if (isShowing()) { dismiss(); } else { - show(context, deviceId); + show(context, deviceId, windowManagerProvider); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java index 2157d754ce87..bd6006c8faa6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java @@ -70,6 +70,7 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; import com.android.settingslib.Utils; import com.android.systemui.res.R; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import java.util.ArrayList; import java.util.Collections; @@ -141,38 +142,38 @@ public final class KeyboardShortcuts { this.mContext = new ContextThemeWrapper( context, android.R.style.Theme_DeviceDefault_Settings); this.mPackageManager = AppGlobals.getPackageManager(); - if (windowManager != null) { - this.mWindowManager = windowManager; - } else { - this.mWindowManager = mContext.getSystemService(WindowManager.class); - } + this.mWindowManager = windowManager; loadResources(context); } - private static KeyboardShortcuts getInstance(Context context) { + private static KeyboardShortcuts getInstance(Context context, + WindowManagerProvider windowManagerProvider) { if (sInstance == null) { - sInstance = new KeyboardShortcuts(context, null); + WindowManager windowManager = windowManagerProvider.getWindowManager(context); + sInstance = new KeyboardShortcuts(context, windowManager); } return sInstance; } - public static void show(Context context, int deviceId) { + public static void show(Context context, int deviceId, + WindowManagerProvider windowManagerProvider) { MetricsLogger.visible(context, MetricsProto.MetricsEvent.KEYBOARD_SHORTCUTS_HELPER); synchronized (sLock) { if (sInstance != null && !sInstance.mContext.equals(context)) { dismiss(); } - getInstance(context).showKeyboardShortcuts(deviceId); + getInstance(context, windowManagerProvider).showKeyboardShortcuts(deviceId); } } - public static void toggle(Context context, int deviceId) { + public static void toggle(Context context, int deviceId, + WindowManagerProvider windowManagerProvider) { synchronized (sLock) { if (isShowing()) { dismiss(); } else { - show(context, deviceId); + show(context, deviceId, windowManagerProvider); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java index 815f1fcfdec6..54c84aa139cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java @@ -24,6 +24,7 @@ import android.content.Intent; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.shared.recents.utilities.Utilities; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import javax.inject.Inject; @@ -31,10 +32,13 @@ import javax.inject.Inject; public class KeyboardShortcutsReceiver extends BroadcastReceiver { private final FeatureFlags mFeatureFlags; + private final WindowManagerProvider mWindowManagerProvider; @Inject - public KeyboardShortcutsReceiver(FeatureFlags featureFlags) { + public KeyboardShortcutsReceiver(FeatureFlags featureFlags, + WindowManagerProvider windowManagerProvider) { mFeatureFlags = featureFlags; + mWindowManagerProvider = windowManagerProvider; } @Override @@ -44,13 +48,14 @@ public class KeyboardShortcutsReceiver extends BroadcastReceiver { } if (isTabletLayoutFlagEnabled() && Utilities.isLargeScreen(context)) { if (Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS.equals(intent.getAction())) { - KeyboardShortcutListSearch.show(context, -1 /* deviceId unknown */); + KeyboardShortcutListSearch.show(context, -1 /* deviceId unknown */, + mWindowManagerProvider); } else if (Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS.equals(intent.getAction())) { KeyboardShortcutListSearch.dismiss(); } } else { if (Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS.equals(intent.getAction())) { - KeyboardShortcuts.show(context, -1 /* deviceId unknown */); + KeyboardShortcuts.show(context, -1 /* deviceId unknown */, mWindowManagerProvider); } else if (Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS.equals(intent.getAction())) { KeyboardShortcuts.dismiss(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index fce5a16bd85d..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()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt index 2d9ccb7b09b0..33ff6c014125 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.chips.mediaprojection.ui.view import android.app.ActivityManager import android.content.DialogInterface import android.content.pm.PackageManager +import android.util.Log import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.dagger.SysUISingleton import com.android.systemui.mediaprojection.data.model.MediaProjectionState @@ -54,10 +55,6 @@ constructor( // dialog to animate back into the chip just for the chip to disappear in a few frames. dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations() stopAction.invoke() - // TODO(b/332662551): If the projection is stopped, there's a brief moment where the - // dialog closes and the chip re-shows because the system APIs haven't come back and - // told SysUI that the projection has officially stopped. It would be great for the chip - // to not re-show at all. } } @@ -85,8 +82,17 @@ constructor( val appInfo = packageManager.getApplicationInfo(packageName, 0) appInfo.loadLabel(packageManager) } catch (e: PackageManager.NameNotFoundException) { - // TODO(b/332662551): Log this error. + Log.w( + TAG, + "Failed to find application info for package: $packageName when creating " + + "end media projection dialog", + e, + ) null } } + + companion object { + private const val TAG = "EndMediaProjectionDialogHelper" + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt index 9380dfe32bf5..0560c9e1f774 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt @@ -145,7 +145,9 @@ constructor( /** * Emits all notifications that are eligible to show as chips in the status bar. This is - * different from which chips will *actually* show, see [shownNotificationChips] for that. + * different from which chips will *actually* show, because + * [com.android.systemui.statusbar.chips.notification.ui.viewmodel.NotifChipsViewModel] will + * hide chips that have [NotificationChipModel.isAppVisible] as true. */ val allNotificationChips: Flow<List<NotificationChipModel>> = if (StatusBarNotifChips.isEnabled) { @@ -186,15 +188,6 @@ constructor( initialValue = emptyList(), ) - /** Emits the notifications that should actually be *shown* as chips in the status bar. */ - val shownNotificationChips: Flow<List<NotificationChipModel>> = - allNotificationChips.map { chipsList -> - // If the app that posted this notification is visible, we want to hide the chip - // because information between the status bar chip and the app itself could be - // out-of-sync (like a timer that's slightly off) - chipsList.filter { !it.isAppVisible } - } - /* Stable sort the promoted notifications by two criteria: Criteria #1: Whichever app was most recently visible has higher ranking. 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 b303751b4d6e..e45524b59837 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 @@ -55,12 +55,12 @@ constructor( private val systemClock: SystemClock, ) { /** - * A flow modeling the notification chips that should be shown. Emits an empty list if there are - * no notifications that should show a status bar chip. + * A flow modeling the current notification chips. Emits an empty list if there are no + * notifications that are eligible to show a status bar chip. */ val chips: Flow<List<OngoingActivityChipModel.Active>> = combine( - notifChipsInteractor.shownNotificationChips, + notifChipsInteractor.allNotificationChips, headsUpNotificationInteractor.statusBarHeadsUpState, ) { notifications, headsUpState -> notifications.map { it.toActivityChipModel(headsUpState) } @@ -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) { @@ -96,6 +98,10 @@ constructor( notifChipsInteractor.onPromotedNotificationChipTapped(this@toActivityChipModel.key) } } + // If the app that posted this notification is visible, we want to hide the chip + // because information between the status bar chip and the app itself could be + // out-of-sync (like a timer that's slightly off) + val isHidden = this.isAppVisible val onClickListenerLegacy = View.OnClickListener { StatusBarChipsModernization.assertInLegacyMode() @@ -120,24 +126,23 @@ constructor( colors = colors, onClickListenerLegacy = onClickListenerLegacy, clickBehavior = clickBehavior, + isHidden = isHidden, ) } - if (this.promotedContent.shortCriticalText != null) { + if (chipContent.shortCriticalText != null) { return OngoingActivityChipModel.Active.Text( key = this.key, icon = icon, colors = colors, - text = this.promotedContent.shortCriticalText, + text = chipContent.shortCriticalText, onClickListenerLegacy = onClickListenerLegacy, clickBehavior = clickBehavior, + isHidden = isHidden, ) } - 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 @@ -148,32 +153,35 @@ constructor( colors = colors, onClickListenerLegacy = onClickListenerLegacy, clickBehavior = clickBehavior, + isHidden = isHidden, ) } - if (this.promotedContent.time == null) { + if (chipContent.time == null) { return OngoingActivityChipModel.Active.IconOnly( key = this.key, icon = icon, colors = colors, onClickListenerLegacy = onClickListenerLegacy, clickBehavior = clickBehavior, + isHidden = isHidden, ) } - 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( key = this.key, icon = icon, colors = colors, - time = this.promotedContent.time.currentTimeMillis, + time = chipContent.time.currentTimeMillis, onClickListenerLegacy = onClickListenerLegacy, clickBehavior = clickBehavior, + isHidden = isHidden, ) } else { // Don't show a `when` time that's close to now or in the past because it's @@ -190,6 +198,7 @@ constructor( colors = colors, onClickListenerLegacy = onClickListenerLegacy, clickBehavior = clickBehavior, + isHidden = isHidden, ) } } @@ -198,10 +207,11 @@ constructor( key = this.key, icon = icon, colors = colors, - startTimeMs = this.promotedContent.time.elapsedRealtimeMillis, - isEventInFuture = this.promotedContent.time.isCountDown, + startTimeMs = chipContent.time.elapsedRealtimeMillis, + isEventInFuture = chipContent.time.isCountDown, onClickListenerLegacy = onClickListenerLegacy, clickBehavior = clickBehavior, + isHidden = isHidden, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt index b2683762cb2a..fe6065d0bb42 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt @@ -46,8 +46,9 @@ sealed interface ColorsModel { } /** The chip should match the system theme main color. */ - // TODO(b/347717946): The chip's color isn't getting updated when the user switches theme, it - // only gets updated when a different configuration change happens, like a rotation. + // Note: When StatusBarChipsModernization is disabled, the chip's color doesn't get + // updated when the user switches theme. It only gets updated when a different + // configuration change happens, like a rotation. data object SystemThemed : ColorsModel { override fun background(context: Context): ColorStateList = ColorStateList.valueOf( 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 e28f3684b0fa..ba60e2ce2aa6 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 @@ -235,7 +235,6 @@ sealed class OngoingActivityChipModel { override val isImportantForPrivacy: Boolean = false, override val icon: ChipIcon, override val colors: ColorsModel, - // TODO(b/361346412): Enforce a max length requirement? val text: String, override val onClickListenerLegacy: View.OnClickListener? = null, override val clickBehavior: ClickBehavior, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/PrivacyDotWindowControllerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/PrivacyDotWindowControllerStore.kt index 7fc5e8abe904..587d80fc3848 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/PrivacyDotWindowControllerStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/PrivacyDotWindowControllerStore.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.data.repository import android.view.Display import android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -48,7 +47,6 @@ constructor( private val windowControllerFactory: PrivacyDotWindowController.Factory, private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository, private val privacyDotViewControllerStore: PrivacyDotViewControllerStore, - private val viewCaptureAwareWindowManagerFactory: ViewCaptureAwareWindowManager.Factory, ) : PrivacyDotWindowControllerStore, StatusBarPerDisplayStoreImpl<PrivacyDotWindowController>( @@ -72,8 +70,7 @@ constructor( return windowControllerFactory.create( displayId = displayId, privacyDotViewController = privacyDotViewController, - viewCaptureAwareWindowManager = - viewCaptureAwareWindowManagerFactory.create(displayWindowProperties.windowManager), + windowManager = displayWindowProperties.windowManager, inflater = displayWindowProperties.layoutInflater, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotWindowController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotWindowController.kt index e2bcfb752e6c..7999ca9fd3e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotWindowController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotWindowController.kt @@ -24,10 +24,10 @@ import android.view.DisplayCutout.BOUNDS_POSITION_RIGHT import android.view.DisplayCutout.BOUNDS_POSITION_TOP import android.view.LayoutInflater import android.view.View +import android.view.WindowManager import android.view.WindowManager.InvalidDisplayException import android.view.WindowManager.LayoutParams.WRAP_CONTENT import android.widget.FrameLayout -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.systemui.ScreenDecorations import com.android.systemui.ScreenDecorationsThread import com.android.systemui.decor.DecorProvider @@ -54,7 +54,7 @@ class PrivacyDotWindowController constructor( @Assisted private val displayId: Int, @Assisted private val privacyDotViewController: PrivacyDotViewController, - @Assisted private val viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, + @Assisted private val windowManager: WindowManager, @Assisted private val inflater: LayoutInflater, @ScreenDecorationsThread private val uiExecutor: Executor, private val dotFactory: PrivacyDotDecorProviderFactory, @@ -106,7 +106,7 @@ constructor( try { // Wrapping this in a try/catch to avoid crashes when a display is instantly removed // after being added, and initialization hasn't finished yet. - viewCaptureAwareWindowManager.addView(rootView, params) + windowManager.addView(rootView, params) } catch (e: InvalidDisplayException) { Log.e( TAG, @@ -118,7 +118,7 @@ constructor( } fun stop() { - dotViews.forEach { viewCaptureAwareWindowManager.removeView(it) } + dotViews.forEach { windowManager.removeView(it) } } @AssistedFactory @@ -126,7 +126,7 @@ constructor( fun create( displayId: Int, privacyDotViewController: PrivacyDotViewController, - viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, + windowManager: WindowManager, inflater: LayoutInflater, ): PrivacyDotWindowController } 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/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 2f9d86b45d1f..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 @@ -36,6 +36,7 @@ import com.android.systemui.statusbar.notification.collection.provider.SectionSt 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 @@ -141,7 +142,7 @@ private class ActiveNotificationsStoreBuilder( private fun NotificationEntry.toModel(): ActiveNotificationModel { val promotedContent = if (PromotedNotificationContentModel.featureFlagEnabled()) { - promotedNotificationContentModel + promotedNotificationContentModels } else { null } @@ -199,7 +200,7 @@ private fun ActiveNotificationsStore.createOrReuse( isGroupSummary: Boolean, bucket: Int, callType: CallType, - promotedContent: PromotedNotificationContentModel?, + promotedContent: PromotedNotificationContentModels?, ): ActiveNotificationModel { return individuals[key]?.takeIf { it.isCurrent( @@ -281,7 +282,7 @@ private fun ActiveNotificationModel.isCurrent( isGroupSummary: Boolean, bucket: Int, callType: CallType, - promotedContent: PromotedNotificationContentModel?, + promotedContent: PromotedNotificationContentModels?, ): Boolean { return when { key != this.key -> 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 96d41f1b3aaf..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 @@ -25,8 +25,8 @@ import com.android.systemui.statusbar.chips.notification.domain.interactor.Statu 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 @@ -201,13 +201,13 @@ constructor( * 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, @@ -229,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 } } /** @@ -251,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 @@ -269,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 dfadf74c71b1..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 @@ -2686,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); } } } 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/HybridConversationNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java index ba80f016cad4..4c7c46dfae94 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java @@ -51,7 +51,7 @@ import java.util.Objects; */ public class HybridConversationNotificationView extends HybridNotificationView { - private static final int MAX_SUMMARIZATION_LINES = 2; + private static final int MAX_SUMMARIZATION_LINES = 1; private ImageView mConversationIconView; private TextView mConversationSenderName; private ViewStub mConversationFacePileStub; @@ -295,7 +295,6 @@ public class HybridConversationNotificationView extends HybridNotificationView { if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return; if (!TextUtils.isEmpty(summarization)) { mConversationSenderName.setVisibility(GONE); - titleText = null; contentText = summarization; mTextView.setSingleLine(false); mTextView.setMaxLines(MAX_SUMMARIZATION_LINES); 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 487cbceb13a4..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 /** @@ -88,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/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..fe367a3927ed 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -90,7 +90,6 @@ import androidx.annotation.NonNull; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleRegistry; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.colorextraction.ColorExtractor; import com.android.internal.logging.MetricsLogger; @@ -233,6 +232,7 @@ import com.android.systemui.util.WallpaperController; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.concurrency.MessageRouter; import com.android.systemui.util.kotlin.JavaAdapter; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import com.android.systemui.volume.VolumeComponent; import com.android.systemui.wallet.controller.QuickAccessWalletController; import com.android.wm.shell.bubbles.Bubbles; @@ -597,7 +597,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private final EmergencyGestureIntentFactory mEmergencyGestureIntentFactory; - private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private final QuickAccessWalletController mWalletController; /** @@ -713,8 +712,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { BrightnessMirrorShowingRepository brightnessMirrorShowingRepository, GlanceableHubContainerController glanceableHubContainerController, EmergencyGestureIntentFactory emergencyGestureIntentFactory, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, - QuickAccessWalletController walletController + QuickAccessWalletController walletController, + WindowManager windowManager, + WindowManagerProvider windowManagerProvider ) { mContext = context; mNotificationsController = notificationsController; @@ -852,7 +852,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mLightRevealScrim = lightRevealScrim; - mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager; + mWindowManager = windowManager; + mWindowManagerProvider = windowManagerProvider; } private void initBubbles(Bubbles bubbles) { @@ -880,8 +881,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mColorExtractor.addOnColorsChangedListener(mOnColorsChangedListener); - mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); - mDisplay = mContext.getDisplay(); mDisplayId = mDisplay.getDisplayId(); updateDisplaySize(); @@ -1716,7 +1715,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mNotificationShadeWindowController.setRequestTopUi(false, TAG); } }, /* isDozing= */ false, RippleShape.CIRCLE, - sUiEventLogger, mViewCaptureAwareWindowManager).show(animationDelay); + sUiEventLogger, mWindowManager, mWindowManagerProvider).show(animationDelay); } @Override @@ -2407,11 +2406,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); } } } @@ -2875,6 +2875,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { protected WindowManager mWindowManager; protected IWindowManager mWindowManagerService; private final IDreamManager mDreamManager; + private final WindowManagerProvider mWindowManagerProvider; protected Display mDisplay; private int mDisplayId; @@ -2911,9 +2912,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { protected void toggleKeyboardShortcuts(int deviceId) { if (shouldUseTabletKeyboardShortcuts()) { - KeyboardShortcutListSearch.toggle(mContext, deviceId); + KeyboardShortcutListSearch.toggle(mContext, deviceId, mWindowManagerProvider); } else { - KeyboardShortcuts.toggle(mContext, deviceId); + KeyboardShortcuts.toggle(mContext, deviceId, mWindowManagerProvider); } } @@ -2927,7 +2928,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private boolean shouldUseTabletKeyboardShortcuts() { return mFeatureFlags.isEnabled(SHORTCUT_LIST_SEARCH_LAYOUT) - && Utilities.isLargeScreen(mContext); + && Utilities.isLargeScreen(mWindowManager, mContext.getResources()); } private void clearNotificationEffects() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt index caf8a43b2aaf..67a7eee2977a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt @@ -104,6 +104,7 @@ private constructor( // intercepted. See [View.OnTouchEvent] if (event.source == InputDevice.SOURCE_MOUSE) { if (event.action == MotionEvent.ACTION_UP) { + dispatchEventToShadeDisplayPolicy(event) v.performClick() shadeController.animateExpandShade() } @@ -113,6 +114,15 @@ private constructor( } } + private fun dispatchEventToShadeDisplayPolicy(event: MotionEvent) { + if (ShadeWindowGoesAround.isEnabled) { + // Notify the shade display policy that the status bar was touched. This may cause + // the shade to change display if the touch was in a display different than the shade + // one. + lazyStatusBarShadeDisplayPolicy.get().onStatusBarTouched(event, mView.width) + } + } + private val configurationListener = object : ConfigurationController.ConfigurationListener { override fun onDensityOrFontScaleChanged() { @@ -232,9 +242,6 @@ private constructor( !upOrCancel || shadeController.isExpandedVisible, ) } - if (ShadeWindowGoesAround.isEnabled && event.action == MotionEvent.ACTION_DOWN) { - lazyStatusBarShadeDisplayPolicy.get().onStatusBarTouched(event, mView.width) - } } private fun addDarkReceivers() { @@ -249,6 +256,9 @@ private constructor( inner class PhoneStatusBarViewTouchHandler : Gefingerpoken { override fun onInterceptTouchEvent(event: MotionEvent): Boolean { + if (event.action == MotionEvent.ACTION_DOWN) { + dispatchEventToShadeDisplayPolicy(event) + } return if (Flags.statusBarSwipeOverChip()) { shadeViewController.handleExternalInterceptTouch(event) } else { 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/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/statusbar/policy/DeviceStateRotationLockSettingController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java index fa022b4768fc..0f8d534df659 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java @@ -28,7 +28,6 @@ import android.util.IndentingPrintWriter; import androidx.annotation.NonNull; import com.android.settingslib.devicestate.DeviceStateAutoRotateSettingManager; -import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager; import com.android.systemui.Dumpable; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; @@ -49,7 +48,7 @@ public final class DeviceStateRotationLockSettingController private final RotationPolicyWrapper mRotationPolicyWrapper; private final DeviceStateManager mDeviceStateManager; private final Executor mMainExecutor; - private final DeviceStateRotationLockSettingsManager mDeviceStateRotationLockSettingsManager; + private final DeviceStateAutoRotateSettingManager mDeviceStateAutoRotateSettingManager; private final DeviceStateRotationLockSettingControllerLogger mLogger; // On registration for DeviceStateCallback, we will receive a callback with the current state @@ -65,13 +64,13 @@ public final class DeviceStateRotationLockSettingController RotationPolicyWrapper rotationPolicyWrapper, DeviceStateManager deviceStateManager, @Main Executor executor, - DeviceStateRotationLockSettingsManager deviceStateRotationLockSettingsManager, + DeviceStateAutoRotateSettingManager deviceStateAutoRotateSettingManager, DeviceStateRotationLockSettingControllerLogger logger, DumpManager dumpManager) { mRotationPolicyWrapper = rotationPolicyWrapper; mDeviceStateManager = deviceStateManager; mMainExecutor = executor; - mDeviceStateRotationLockSettingsManager = deviceStateRotationLockSettingsManager; + mDeviceStateAutoRotateSettingManager = deviceStateAutoRotateSettingManager; mLogger = logger; dumpManager.registerDumpable(this); } @@ -86,14 +85,14 @@ public final class DeviceStateRotationLockSettingController mDeviceStateManager.registerCallback(mMainExecutor, mDeviceStateCallback); mDeviceStateAutoRotateSettingListener = () -> readPersistedSetting("deviceStateRotationLockChange", mDeviceState); - mDeviceStateRotationLockSettingsManager.registerListener( + mDeviceStateAutoRotateSettingManager.registerListener( mDeviceStateAutoRotateSettingListener); } else { if (mDeviceStateCallback != null) { mDeviceStateManager.unregisterCallback(mDeviceStateCallback); } if (mDeviceStateAutoRotateSettingListener != null) { - mDeviceStateRotationLockSettingsManager.unregisterListener( + mDeviceStateAutoRotateSettingManager.unregisterListener( mDeviceStateAutoRotateSettingListener); } } @@ -102,7 +101,7 @@ public final class DeviceStateRotationLockSettingController @Override public void onRotationLockStateChanged(boolean newRotationLocked, boolean affordanceVisible) { int deviceState = mDeviceState; - boolean currentRotationLocked = mDeviceStateRotationLockSettingsManager + boolean currentRotationLocked = mDeviceStateAutoRotateSettingManager .isRotationLocked(deviceState); mLogger.logRotationLockStateChanged(deviceState, newRotationLocked, currentRotationLocked); if (deviceState == -1) { @@ -117,7 +116,7 @@ public final class DeviceStateRotationLockSettingController private void saveNewRotationLockSetting(boolean isRotationLocked) { int deviceState = mDeviceState; mLogger.logSaveNewRotationLockSetting(isRotationLocked, deviceState); - mDeviceStateRotationLockSettingsManager.updateSetting(deviceState, isRotationLocked); + mDeviceStateAutoRotateSettingManager.updateSetting(deviceState, isRotationLocked); } private void updateDeviceState(@NonNull DeviceState state) { @@ -139,7 +138,7 @@ public final class DeviceStateRotationLockSettingController private void readPersistedSetting(String caller, int state) { int rotationLockSetting = - mDeviceStateRotationLockSettingsManager.getRotationLockSetting(state); + mDeviceStateAutoRotateSettingManager.getRotationLockSetting(state); boolean shouldBeLocked = rotationLockSetting == DEVICE_STATE_ROTATION_LOCK_LOCKED; boolean isLocked = mRotationPolicyWrapper.isRotationLocked(); @@ -167,7 +166,7 @@ public final class DeviceStateRotationLockSettingController @Override public void dump(@NonNull PrintWriter printWriter, @NonNull String[] args) { IndentingPrintWriter pw = new IndentingPrintWriter(printWriter); - mDeviceStateRotationLockSettingsManager.dump(pw); + mDeviceStateAutoRotateSettingManager.dump(printWriter, null); pw.println("DeviceStateRotationLockSettingController"); pw.increaseIndent(); pw.println("mDeviceState: " + mDeviceState); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java index d1e807f18196..e2a6c195af38 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java @@ -18,13 +18,20 @@ package com.android.systemui.statusbar.policy.dagger; import android.content.Context; import android.content.res.Resources; +import android.hardware.devicestate.DeviceStateManager; +import android.os.Handler; import android.os.UserManager; import com.android.internal.R; -import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager; +import com.android.settingslib.devicestate.AndroidSecureSettings; +import com.android.settingslib.devicestate.DeviceStateAutoRotateSettingManager; +import com.android.settingslib.devicestate.DeviceStateAutoRotateSettingManagerProvider; +import com.android.settingslib.devicestate.PosturesHelper; +import com.android.settingslib.devicestate.SecureSettings; import com.android.settingslib.notification.modes.ZenIconLoader; 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.dagger.qualifiers.UiBackground; import com.android.systemui.log.LogBuffer; @@ -222,12 +229,34 @@ public interface StatusBarPolicyModule { return controller; } - /** Returns a singleton instance of DeviceStateRotationLockSettingsManager */ + /** */ + @SysUISingleton + @Provides + static SecureSettings provideAndroidSecureSettings(Context context) { + return new AndroidSecureSettings(context.getContentResolver()); + } + + /** */ @SysUISingleton @Provides - static DeviceStateRotationLockSettingsManager provideAutoRotateSettingsManager( - Context context) { - return DeviceStateRotationLockSettingsManager.getInstance(context); + static PosturesHelper providePosturesHelper(Context context, + DeviceStateManager deviceStateManager) { + return new PosturesHelper(context, deviceStateManager); + } + + /** Returns a singleton instance of DeviceStateAutoRotateSettingManager based on auto-rotate + * refactor flag. */ + @SysUISingleton + @Provides + static DeviceStateAutoRotateSettingManager provideAutoRotateSettingsManager( + Context context, + @Background Executor bgExecutor, + SecureSettings secureSettings, + @Main Handler mainHandler, + PosturesHelper posturesHelper + ) { + return DeviceStateAutoRotateSettingManagerProvider.createInstance(context, bgExecutor, + secureSettings, mainHandler, posturesHelper); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt index ecfcb29a9944..eae310ed169b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt @@ -19,7 +19,7 @@ package com.android.systemui.statusbar.window import android.content.Context import android.view.View import android.view.ViewGroup -import com.android.app.viewcapture.ViewCaptureAwareWindowManager +import android.view.WindowManager import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.fragments.FragmentHostManager import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController @@ -84,7 +84,7 @@ interface StatusBarWindowController { fun interface Factory { fun create( context: Context, - viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, + windowManager: WindowManager, statusBarConfigurationController: StatusBarConfigurationController, contentInsetsProvider: StatusBarContentInsetsProvider, ): StatusBarWindowController diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java index 25972ac2bedf..77cd58111b94 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java @@ -47,7 +47,6 @@ import android.view.WindowManager; import androidx.annotation.NonNull; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.policy.SystemBarUtils; import com.android.systemui.animation.ActivityTransitionAnimator; import com.android.systemui.animation.DelegateTransitionAnimatorController; @@ -78,7 +77,7 @@ public class StatusBarWindowControllerImpl implements StatusBarWindowController private static final boolean DEBUG = false; private final Context mContext; - private final ViewCaptureAwareWindowManager mWindowManager; + private final WindowManager mWindowManager; private final StatusBarConfigurationController mStatusBarConfigurationController; private final IWindowManager mIWindowManager; private final StatusBarContentInsetsProvider mContentInsetsProvider; @@ -100,7 +99,7 @@ public class StatusBarWindowControllerImpl implements StatusBarWindowController public StatusBarWindowControllerImpl( @Assisted Context context, @InternalWindowViewInflater StatusBarWindowViewInflater statusBarWindowViewInflater, - @Assisted ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, + @Assisted WindowManager windowManager, @Assisted StatusBarConfigurationController statusBarConfigurationController, IWindowManager iWindowManager, @Assisted StatusBarContentInsetsProvider contentInsetsProvider, @@ -108,7 +107,7 @@ public class StatusBarWindowControllerImpl implements StatusBarWindowController Optional<UnfoldTransitionProgressProvider> unfoldTransitionProgressProvider, @Main Executor mainExecutor) { mContext = context; - mWindowManager = viewCaptureAwareWindowManager; + mWindowManager = windowManager; mStatusBarConfigurationController = statusBarConfigurationController; mIWindowManager = iWindowManager; mContentInsetsProvider = contentInsetsProvider; @@ -406,7 +405,7 @@ public class StatusBarWindowControllerImpl implements StatusBarWindowController @Override StatusBarWindowControllerImpl create( @NonNull Context context, - @NonNull ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, + @NonNull WindowManager windowManager, @NonNull StatusBarConfigurationController statusBarConfigurationController, @NonNull StatusBarContentInsetsProvider contentInsetsProvider); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt index 39afc38dad11..74c028a8c7ae 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.window import android.content.Context import android.view.WindowManager -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DisplayRepository @@ -42,7 +41,6 @@ constructor( @Background backgroundApplicationScope: CoroutineScope, private val controllerFactory: StatusBarWindowController.Factory, private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository, - private val viewCaptureAwareWindowManagerFactory: ViewCaptureAwareWindowManager.Factory, private val statusBarConfigurationControllerStore: StatusBarConfigurationControllerStore, private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore, displayRepository: DisplayRepository, @@ -67,11 +65,9 @@ constructor( statusBarConfigurationControllerStore.forDisplay(displayId) ?: return null val contentInsetsProvider = statusBarContentInsetsProviderStore.forDisplay(displayId) ?: return null - val viewCaptureAwareWindowManager = - viewCaptureAwareWindowManagerFactory.create(statusBarDisplayContext.windowManager) return controllerFactory.create( statusBarDisplayContext.context, - viewCaptureAwareWindowManager, + statusBarDisplayContext.windowManager, statusBarConfigurationController, contentInsetsProvider, ) @@ -89,7 +85,7 @@ class SingleDisplayStatusBarWindowControllerStore @Inject constructor( context: Context, - viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, + windowManager: WindowManager, factory: StatusBarWindowControllerImpl.Factory, statusBarConfigurationControllerStore: StatusBarConfigurationControllerStore, statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore, @@ -98,7 +94,7 @@ constructor( PerDisplayStore<StatusBarWindowController> by SingleDisplayStore( factory.create( context, - viewCaptureAwareWindowManager, + windowManager, statusBarConfigurationControllerStore.defaultDisplay, statusBarContentInsetsProviderStore.defaultDisplay, ) diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt index 635576743462..ea5f422f977d 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt @@ -32,7 +32,6 @@ import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT import androidx.annotation.CallSuper import androidx.annotation.VisibleForTesting -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.systemui.CoreStartable import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager @@ -70,7 +69,7 @@ import java.io.PrintWriter abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : TemporaryViewLogger<T>>( internal val context: Context, internal val logger: U, - internal val windowManager: ViewCaptureAwareWindowManager, + internal val windowManager: WindowManager, @Main private val mainExecutor: DelayableExecutor, private val accessibilityManager: AccessibilityManager, private val configurationController: ConfigurationController, diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt index 9b9cba9186f4..b6f54331b29f 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt @@ -29,6 +29,7 @@ import android.view.View import android.view.View.ACCESSIBILITY_LIVE_REGION_ASSERTIVE import android.view.View.ACCESSIBILITY_LIVE_REGION_NONE import android.view.ViewGroup +import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityNodeInfo import android.widget.ImageView @@ -37,7 +38,6 @@ import androidx.annotation.DimenRes import androidx.annotation.IdRes import androidx.annotation.VisibleForTesting import com.android.app.animation.Interpolators -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.internal.widget.CachingIconView import com.android.systemui.Gefingerpoken import com.android.systemui.classifier.FalsingCollector @@ -81,7 +81,7 @@ open class ChipbarCoordinator constructor( context: Context, logger: ChipbarLogger, - viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, + windowManager: WindowManager, @Main mainExecutor: DelayableExecutor, accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, @@ -100,7 +100,7 @@ constructor( TemporaryViewDisplayController<ChipbarInfo, ChipbarLogger>( context, logger, - viewCaptureAwareWindowManager, + windowManager, mainExecutor, accessibilityManager, configurationController, diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt index c11e4c507914..00c6ffd861da 100644 --- a/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt +++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt @@ -21,7 +21,6 @@ import android.graphics.PixelFormat import android.view.Gravity import android.view.WindowInsets import android.view.WindowManager -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -38,7 +37,7 @@ import javax.inject.Inject class TopLevelWindowEffects @Inject constructor( @Application private val context: Context, @Application private val applicationScope: CoroutineScope, - private val windowManager: ViewCaptureAwareWindowManager, + private val windowManager: WindowManager, private val squeezeEffectInteractor: SqueezeEffectInteractor, private val keyEventInteractor: KeyEventInteractor, private val viewModelFactory: SqueezeEffectViewModel.Factory 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/ConvenienceExtensions.kt b/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt index 70fd5ab767d0..f1a556353273 100644 --- a/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt +++ b/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt @@ -20,7 +20,6 @@ import android.graphics.Rect import android.util.IndentingPrintWriter import android.view.View import android.view.ViewGroup -import dagger.Lazy import java.io.PrintWriter /** [Sequence] that yields all of the direct children of this [ViewGroup] */ @@ -56,11 +55,6 @@ val View.boundsOnScreen: Rect return bounds } -/** Extension method to convert [dagger.Lazy] to [kotlin.Lazy] for object of any class [T]. */ -fun <T> Lazy<T>.toKotlinLazy(): kotlin.Lazy<T> { - return lazy { this.get() } -} - /** * Returns whether this [Collection] contains exactly all [elements]. * diff --git a/packages/SystemUI/src/com/android/systemui/util/display/DisplayHelper.java b/packages/SystemUI/src/com/android/systemui/util/display/DisplayHelper.java index 8acd6535e751..757b2d973312 100644 --- a/packages/SystemUI/src/com/android/systemui/util/display/DisplayHelper.java +++ b/packages/SystemUI/src/com/android/systemui/util/display/DisplayHelper.java @@ -21,6 +21,8 @@ import android.hardware.display.DisplayManager; import android.view.Display; import android.view.WindowManager; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; + import javax.inject.Inject; /** @@ -29,14 +31,17 @@ import javax.inject.Inject; public class DisplayHelper { private final Context mContext; private final DisplayManager mDisplayManager; + private final WindowManagerProvider mWindowManagerProvider; /** * Default constructor. */ @Inject - public DisplayHelper(Context context, DisplayManager displayManager) { + public DisplayHelper(Context context, DisplayManager displayManager, + WindowManagerProvider windowManagerProvider) { mContext = context; mDisplayManager = displayManager; + mWindowManagerProvider = windowManagerProvider; } @@ -45,9 +50,8 @@ public class DisplayHelper { */ public Rect getMaxBounds(int displayId, int windowContextType) { final Display display = mDisplayManager.getDisplay(displayId); - WindowManager windowManager = mContext.createDisplayContext(display) - .createWindowContext(windowContextType, null) - .getSystemService(WindowManager.class); + WindowManager windowManager = mWindowManagerProvider.getWindowManager(mContext + .createDisplayContext(display).createWindowContext(windowContextType, null)); return windowManager.getMaximumWindowMetrics().getBounds(); } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt index 3ac6c7bc0c6b..457b6ac42f8b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt @@ -18,6 +18,7 @@ package com.android.systemui.volume.dialog import android.content.Context import android.os.Bundle +import android.view.Gravity import android.view.MotionEvent import android.view.View import android.view.ViewGroup @@ -57,8 +58,10 @@ constructor( attributes.apply { title = "VolumeDialog" // Not the same as Window#setTitle } - setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT) + setGravity(Gravity.END) } + setCancelable(false) setCanceledOnTouchOutside(true) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt index 938e313771ad..b06a3b7784f8 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt @@ -68,6 +68,7 @@ constructor( viewModel.isShowingSafetyWarning .mapLatest { isShowingSafetyWarning -> if (isShowingSafetyWarning) { + viewModel.onSafetyWarningDialogShown() showSafetyWarningVisibility { viewModel.onSafetyWarningDismissed() } } } @@ -76,6 +77,7 @@ constructor( viewModel.csdWarning .mapLatest { csdWarning -> if (csdWarning != null) { + viewModel.onCsdWarningDialogShown() showCsdWarningDialog(csdWarning, viewModel.csdWarningConfigModel.actions) { viewModel.onCsdWarningDismissed() } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt index bd23e8cce82d..4ed25859fb86 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt @@ -108,6 +108,7 @@ constructor( /** Resets current dialog timeout. */ fun resetDismissTimeout() { + controller.userActivity() mutableDismissDialogEvents.tryEmit(Unit) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/util/RingerDrawerConstraintsUtils.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/util/RingerDrawerConstraintsUtils.kt index fb9884cf4341..ad24f08a7214 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/util/RingerDrawerConstraintsUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/util/RingerDrawerConstraintsUtils.kt @@ -111,7 +111,7 @@ private fun ConstraintSet.adjustOpenConstraintsForDrawer( view.id, ConstraintSet.START, motionLayout.context.resources.getDimensionPixelSize( - R.dimen.volume_dialog_ringer_drawer_margin + R.dimen.volume_dialog_background_margin ), ) } @@ -121,7 +121,7 @@ private fun ConstraintSet.adjustOpenConstraintsForDrawer( view.id, ConstraintSet.END, motionLayout.context.resources.getDimensionPixelSize( - R.dimen.volume_dialog_components_spacing + R.dimen.volume_dialog_ringer_drawer_buttons_spacing ), ) } else { @@ -140,7 +140,7 @@ private fun ConstraintSet.adjustOpenConstraintsForDrawer( view.id, ConstraintSet.BOTTOM, motionLayout.context.resources.getDimensionPixelSize( - R.dimen.volume_dialog_components_spacing + R.dimen.volume_dialog_ringer_drawer_buttons_spacing ), ) } else { @@ -158,10 +158,10 @@ private fun ConstraintSet.adjustOpenConstraintsForDrawer( R.dimen.volume_dialog_ringer_drawer_button_size ) * (motionLayout.childCount - 1)) + (motionLayout.context.resources.getDimensionPixelSize( - R.dimen.volume_dialog_ringer_drawer_margin + R.dimen.volume_dialog_background_margin ) * 2) + (motionLayout.context.resources.getDimensionPixelSize( - R.dimen.volume_dialog_components_spacing + R.dimen.volume_dialog_ringer_drawer_buttons_spacing ) * (motionLayout.childCount - 2)) ORIENTATION_PORTRAIT -> diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt index db4b8ef5aef7..dc41e90faeaf 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt @@ -92,6 +92,8 @@ private fun VolumeDialogSlider( SliderDefaults.colors( activeTickColor = MaterialTheme.colorScheme.surfaceContainerHighest, inactiveTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest, + disabledActiveTickColor = MaterialTheme.colorScheme.surfaceContainerHighest, + disabledInactiveTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest, ) val collectedSliderStateModel by viewModel.state.collectAsStateWithLifecycle(null) val sliderStateModel = collectedSliderStateModel ?: return diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt index 6d16300bf56e..87cc1830af28 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt @@ -70,10 +70,18 @@ constructor( val isShowingSafetyWarning: Flow<Boolean> = dialogSafetyWarningInteractor.isShowingSafetyWarning val csdWarning: Flow<Int?> = dialogCsdWarningInteractor.csdWarning + fun onSafetyWarningDialogShown() { + dialogVisibilityInteractor.resetDismissTimeout() + } + fun onSafetyWarningDismissed() { dialogSafetyWarningInteractor.onSafetyWarningDismissed() } + fun onCsdWarningDialogShown() { + dialogVisibilityInteractor.resetDismissTimeout() + } + fun onCsdWarningDismissed() { dialogCsdWarningInteractor.onCsdWarningDismissed() } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java index b1c6455a6e57..107f670eae8d 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java @@ -42,7 +42,6 @@ import android.service.wallpaper.WallpaperService; import android.util.Log; import android.view.Surface; import android.view.SurfaceHolder; -import android.view.WindowManager; import androidx.annotation.NonNull; @@ -50,6 +49,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dagger.qualifiers.LongRunning; import com.android.systemui.settings.UserTracker; import com.android.systemui.util.concurrency.DelayableExecutor; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -71,6 +71,7 @@ public class ImageWallpaper extends WallpaperService { private boolean mPagesComputed = false; private final UserTracker mUserTracker; + private final WindowManagerProvider mWindowManagerProvider; // used to handle WallpaperService messages (e.g. DO_ATTACH, MSG_UPDATE_SURFACE) // and to receive WallpaperService callbacks (e.g. onCreateEngine, onSurfaceRedrawNeeded) @@ -84,10 +85,12 @@ public class ImageWallpaper extends WallpaperService { private static final int DELAY_UNLOAD_BITMAP = 2000; @Inject - public ImageWallpaper(@LongRunning DelayableExecutor longExecutor, UserTracker userTracker) { + public ImageWallpaper(@LongRunning DelayableExecutor longExecutor, UserTracker userTracker, + WindowManagerProvider windowManagerProvider) { super(); mLongExecutor = longExecutor; mUserTracker = userTracker; + mWindowManagerProvider = windowManagerProvider; } @Override @@ -552,8 +555,7 @@ public class ImageWallpaper extends WallpaperService { } private void getDisplaySizeAndUpdateColorExtractor() { - Rect window = getDisplayContext() - .getSystemService(WindowManager.class) + Rect window = mWindowManagerProvider.getWindowManager(getDisplayContext()) .getCurrentWindowMetrics() .getBounds(); mWallpaperLocalColorExtractor.setDisplayDimensions(window.width(), window.height()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index 0769ada805a2..645e306eed07 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -80,8 +80,6 @@ import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCapture; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.biometrics.AuthController; import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository; @@ -104,7 +102,6 @@ import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.commandline.CommandRegistry; import com.android.systemui.statusbar.events.PrivacyDotViewController; -import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.concurrency.FakeThreadFactory; import com.android.systemui.util.kotlin.JavaAdapter; @@ -112,8 +109,6 @@ import com.android.systemui.util.settings.FakeSettings; import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.time.FakeSystemClock; -import kotlin.Lazy; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -132,7 +127,6 @@ public class ScreenDecorationsTest extends SysuiTestCase { private ScreenDecorations mScreenDecorations; private WindowManager mWindowManager; - private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private DisplayManager mDisplayManager; private SecureSettings mSecureSettings; private FakeExecutor mExecutor; @@ -179,8 +173,6 @@ public class ScreenDecorationsTest extends SysuiTestCase { private CutoutDecorProviderFactory mCutoutFactory; @Mock private JavaAdapter mJavaAdapter; - @Mock - private Lazy<ViewCapture> mLazyViewCapture; private FakeFacePropertyRepository mFakeFacePropertyRepository = new FakeFacePropertyRepository(); @@ -254,14 +246,12 @@ public class ScreenDecorationsTest extends SysuiTestCase { new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), mFakeFacePropertyRepository)); - mViewCaptureAwareWindowManager = new ViewCaptureAwareWindowManager(mWindowManager, - mLazyViewCapture, false); mScreenDecorations = spy(new ScreenDecorations(mContext, mSecureSettings, mCommandRegistry, mUserTracker, mDisplayTracker, mDotViewController, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory, new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), mFakeFacePropertyRepository, mJavaAdapter, mCameraProtectionLoader, - mViewCaptureAwareWindowManager, mMainHandler, mExecutor) { + mWindowManager, mMainHandler, mExecutor) { @Override public void start() { super.start(); @@ -1276,7 +1266,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory, new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), mFakeFacePropertyRepository, mJavaAdapter, mCameraProtectionLoader, - mViewCaptureAwareWindowManager, mMainHandler, mExecutor); + mWindowManager, mMainHandler, mExecutor); screenDecorations.start(); when(mContext.getDisplay()).thenReturn(mDisplay); when(mDisplay.getDisplayInfo(any())).thenAnswer(new Answer<Boolean>() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java index 6d75c4ca3a38..d3d4e24001cb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java @@ -43,13 +43,13 @@ import android.view.accessibility.IRemoteMagnificationAnimationCallback; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.SysuiTestCase; import com.android.systemui.model.SysUiState; import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import org.junit.Before; import org.junit.Test; @@ -94,7 +94,7 @@ public class IMagnificationConnectionTest extends SysuiTestCase { @Mock private IWindowManager mIWindowManager; @Mock - private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; + private WindowManagerProvider mWindowManagerProvider; private IMagnificationConnection mIMagnificationConnection; private MagnificationImpl mMagnification; @@ -116,8 +116,7 @@ public class IMagnificationConnectionTest extends SysuiTestCase { mTestableLooper.getLooper(), mContext.getMainExecutor(), mCommandQueue, mModeSwitchesController, mSysUiState, mLauncherProxyService, mSecureSettings, mDisplayTracker, getContext().getSystemService(DisplayManager.class), - mA11yLogger, mIWindowManager, mAccessibilityManager, - mViewCaptureAwareWindowManager); + mA11yLogger, mIWindowManager, mAccessibilityManager, mWindowManagerProvider); mMagnification.mWindowMagnificationControllerSupplier = new FakeWindowMagnificationControllerSupplier( mContext.getSystemService(DisplayManager.class)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java index 8bfd2545ff2b..ae96e8fe7b8b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java @@ -50,13 +50,13 @@ import android.view.accessibility.IMagnificationConnectionCallback; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.SysuiTestCase; import com.android.systemui.model.SysUiState; import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import org.junit.Before; import org.junit.Test; @@ -98,7 +98,7 @@ public class MagnificationTest extends SysuiTestCase { @Mock private IWindowManager mIWindowManager; @Mock - private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; + private WindowManagerProvider mWindowManagerProvider; @Before public void setUp() throws Exception { @@ -132,8 +132,7 @@ public class MagnificationTest extends SysuiTestCase { mCommandQueue, mModeSwitchesController, mSysUiState, mLauncherProxyService, mSecureSettings, mDisplayTracker, getContext().getSystemService(DisplayManager.class), mA11yLogger, mIWindowManager, - getContext().getSystemService(AccessibilityManager.class), - mViewCaptureAwareWindowManager); + getContext().getSystemService(AccessibilityManager.class), mWindowManagerProvider); mMagnification.mWindowMagnificationControllerSupplier = new FakeControllerSupplier( mContext.getSystemService(DisplayManager.class), mWindowMagnificationController); mMagnification.mMagnificationSettingsSupplier = new FakeSettingsSupplier( diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java index a0f5b2214f80..ac0378b093c5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java @@ -51,8 +51,6 @@ import android.view.animation.AccelerateInterpolator; import android.window.InputTransferToken; import androidx.test.filters.LargeTest; - -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; @@ -112,8 +110,6 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { SysUiState mSysUiState; @Mock SecureSettings mSecureSettings; - @Mock - ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private SpyWindowMagnificationController mController; private WindowMagnificationController mSpyController; private WindowMagnificationAnimationController mWindowMagnificationAnimationController; @@ -167,7 +163,7 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { mSecureSettings, scvhSupplier, mSfVsyncFrameProvider, - mViewCaptureAwareWindowManager); + mWindowManager); mSpyController = mController.getSpyController(); } @@ -1023,7 +1019,7 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { SecureSettings secureSettings, Supplier<SurfaceControlViewHost> scvhSupplier, SfVsyncFrameCallbackProvider sfVsyncFrameProvider, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + WindowManager windowManager) { super( context, handler, @@ -1033,7 +1029,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { callback, sysUiState, secureSettings, - scvhSupplier); + scvhSupplier, + windowManager); mSpyController = Mockito.mock(WindowMagnificationController.class); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java index 5b32b922d377..c8e0b14152fd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java @@ -238,7 +238,8 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { mWindowMagnifierCallback, mSysUiState, mSecureSettings, - scvhSupplier); + scvhSupplier, + mWindowManager); verify(mMirrorWindowControl).setWindowDelegate( any(MirrorWindowControl.MirrorWindowDelegate.class)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java index 45b9f4ad2322..dfc3ff2791b2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java @@ -65,8 +65,6 @@ import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCapture; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.SysuiTestCase; import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView; @@ -74,8 +72,6 @@ import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView.OnSeekBarW import com.android.systemui.res.R; import com.android.systemui.util.settings.SecureSettings; -import kotlin.Lazy; - import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -104,8 +100,6 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { private SecureSettings mSecureSettings; @Mock private WindowMagnificationSettingsCallback mWindowMagnificationSettingsCallback; - @Mock - private Lazy<ViewCapture> mLazyViewCapture; private TestableWindowManager mWindowManager; private WindowMagnificationSettings mWindowMagnificationSettings; private MotionEventHelper mMotionEventHelper = new MotionEventHelper(); @@ -123,6 +117,7 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { final WindowManager wm = mContext.getSystemService(WindowManager.class); mWindowManager = spy(new TestableWindowManager(wm)); mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager); + mContext.addMockSystemService(Context.ACCESSIBILITY_SERVICE, mAccessibilityManager); when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).then( @@ -130,11 +125,9 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { when(mSecureSettings.getFloatForUser(anyString(), anyFloat(), anyInt())).then( returnsSecondArg()); - ViewCaptureAwareWindowManager vwm = new ViewCaptureAwareWindowManager(mWindowManager, - mLazyViewCapture, /* isViewCaptureEnabled= */ false); mWindowMagnificationSettings = new WindowMagnificationSettings(mContext, mWindowMagnificationSettingsCallback, mSfVsyncFrameProvider, - mSecureSettings, vwm); + mSecureSettings, mWindowManager); mSettingView = mWindowMagnificationSettings.getSettingView(); mZoomSeekbar = mSettingView.findViewById(R.id.magnifier_zoom_slider); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java index bc9d4c7fa0e6..b81cbe48b4ff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java @@ -38,8 +38,6 @@ import android.view.accessibility.AccessibilityManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCapture; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.settingslib.bluetooth.HearingAidDeviceManager; @@ -51,8 +49,6 @@ import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.util.settings.SecureSettings; -import kotlin.Lazy; - import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -78,7 +74,6 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { private Context mContextWrapper; private WindowManager mWindowManager; - private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private AccessibilityManager mAccessibilityManager; private KeyguardUpdateMonitor mKeyguardUpdateMonitor; private AccessibilityFloatingMenuController mController; @@ -93,8 +88,6 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { @Mock private SecureSettings mSecureSettings; @Mock - private Lazy<ViewCapture> mLazyViewCapture; - @Mock private NavigationModeController mNavigationModeController; @Mock private HearingAidDeviceManager mHearingAidDeviceManager; @@ -110,8 +103,6 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { }; mWindowManager = mContext.getSystemService(WindowManager.class); - mViewCaptureAwareWindowManager = new ViewCaptureAwareWindowManager(mWindowManager, - mLazyViewCapture, /* isViewCaptureEnabled= */ false); mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); mTestableLooper = TestableLooper.get(this); @@ -172,8 +163,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { enableAccessibilityFloatingMenuConfig(); mController = setUpController(); mController.mFloatingMenu = new MenuViewLayerController(mContextWrapper, mWindowManager, - mViewCaptureAwareWindowManager, mAccessibilityManager, mSecureSettings, - mNavigationModeController, mHearingAidDeviceManager); + mAccessibilityManager, mSecureSettings, mNavigationModeController, + mHearingAidDeviceManager); captureKeyguardUpdateMonitorCallback(); mKeyguardCallback.onUserUnlocked(); @@ -200,8 +191,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { enableAccessibilityFloatingMenuConfig(); mController = setUpController(); mController.mFloatingMenu = new MenuViewLayerController(mContextWrapper, mWindowManager, - mViewCaptureAwareWindowManager, mAccessibilityManager, mSecureSettings, - mNavigationModeController, mHearingAidDeviceManager); + mAccessibilityManager, mSecureSettings, mNavigationModeController, + mHearingAidDeviceManager); captureKeyguardUpdateMonitorCallback(); mKeyguardCallback.onUserSwitching(fakeUserId); @@ -215,8 +206,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { enableAccessibilityFloatingMenuConfig(); mController = setUpController(); mController.mFloatingMenu = new MenuViewLayerController(mContextWrapper, mWindowManager, - mViewCaptureAwareWindowManager, mAccessibilityManager, mSecureSettings, - mNavigationModeController, mHearingAidDeviceManager); + mAccessibilityManager, mSecureSettings, mNavigationModeController, + mHearingAidDeviceManager); captureKeyguardUpdateMonitorCallback(); mKeyguardCallback.onUserUnlocked(); mKeyguardCallback.onKeyguardVisibilityChanged(true); @@ -362,18 +353,15 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { private AccessibilityFloatingMenuController setUpController() { final WindowManager windowManager = mContext.getSystemService(WindowManager.class); - final ViewCaptureAwareWindowManager viewCaptureAwareWindowManager = - new ViewCaptureAwareWindowManager(windowManager, mLazyViewCapture, - /* isViewCaptureEnabled= */ false); final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); final FakeDisplayTracker displayTracker = new FakeDisplayTracker(mContext); mKeyguardUpdateMonitor = Dependency.get(KeyguardUpdateMonitor.class); final AccessibilityFloatingMenuController controller = new AccessibilityFloatingMenuController(mContextWrapper, windowManager, - viewCaptureAwareWindowManager, displayManager, mAccessibilityManager, - mTargetsObserver, mModeObserver, mHearingAidDeviceManager, - mKeyguardUpdateMonitor, mSecureSettings, displayTracker, - mNavigationModeController, new Handler(mTestableLooper.getLooper())); + displayManager, mAccessibilityManager, mTargetsObserver, mModeObserver, + mHearingAidDeviceManager, mKeyguardUpdateMonitor, mSecureSettings, + displayTracker, mNavigationModeController, + new Handler(mTestableLooper.getLooper())); controller.init(); return controller; 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..dfb6afe8bd12 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()) } } @@ -350,6 +352,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { val controller = createController() val runner = underTest.createEphemeralRunner(controller) runner.onAnimationCancelled() + waitForIdleSync() runner.onAnimationStart( TRANSIT_NONE, emptyArray(), @@ -357,12 +360,13 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { emptyArray(), iCallback, ) - waitForIdleSync() + verify(controller).onTransitionAnimationCancelled() verify(controller, never()).onTransitionAnimationStart(anyBoolean()) verify(listener).onTransitionAnimationCancelled() verify(listener, never()).onTransitionAnimationStart() + verify(iCallback).onAnimationFinished() assertNull(runner.delegate) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index a1d038ad8554..a9e6a3ebdb30 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -28,8 +28,6 @@ import android.view.WindowManager import android.view.accessibility.AccessibilityManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.app.viewcapture.ViewCapture -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityTransitionAnimator @@ -62,7 +60,6 @@ import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.testKosmos import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.google.common.truth.Truth.assertThat -import dagger.Lazy import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -98,7 +95,6 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { @Mock private lateinit var inflater: LayoutInflater @Mock private lateinit var windowManager: WindowManager - @Mock private lateinit var lazyViewCapture: kotlin.Lazy<ViewCapture> @Mock private lateinit var accessibilityManager: AccessibilityManager @Mock private lateinit var statusBarStateController: StatusBarStateController @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager @@ -160,11 +156,7 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { UdfpsControllerOverlay( context, inflater, - ViewCaptureAwareWindowManager( - windowManager, - lazyViewCapture, - isViewCaptureEnabled = false, - ), + windowManager, accessibilityManager, statusBarStateController, statusBarKeyguardViewManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 2c70249bcb06..bf79d11b2fb8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -1472,7 +1472,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa whenever(kosmos.udfpsUtils.onTouchOutsideOfSensorArea(any(), any(), any(), any(), any())) .thenReturn("Direction") - kosmos.promptViewModel.onAnnounceAccessibilityHint( + kosmos.promptViewModel.onUpdateAccessibilityHint( obtainMotionEvent(MotionEvent.ACTION_HOVER_ENTER), true, ) @@ -1497,7 +1497,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa whenever(kosmos.udfpsUtils.onTouchOutsideOfSensorArea(any(), any(), any(), any(), any())) .thenReturn("Direction") - kosmos.promptViewModel.onAnnounceAccessibilityHint( + kosmos.promptViewModel.onUpdateAccessibilityHint( obtainMotionEvent(MotionEvent.ACTION_HOVER_ENTER), true, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt index 57b397cfca7e..034bab855faf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt @@ -23,13 +23,11 @@ import android.view.WindowManager import android.view.WindowMetrics import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.app.viewcapture.ViewCapture -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.internal.logging.UiEventLogger +import com.android.systemui.res.R import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import com.android.systemui.res.R import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController @@ -42,12 +40,12 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers import org.mockito.Mock +import org.mockito.Mockito.`when` import org.mockito.Mockito.any import org.mockito.Mockito.eq import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @SmallTest @@ -62,7 +60,6 @@ class WiredChargingRippleControllerTest : SysuiTestCase() { @Mock private lateinit var windowManager: WindowManager @Mock private lateinit var uiEventLogger: UiEventLogger @Mock private lateinit var windowMetrics: WindowMetrics - @Mock private lateinit var lazyViewCapture: Lazy<ViewCapture> private val systemClock = FakeSystemClock() @Before @@ -71,9 +68,7 @@ class WiredChargingRippleControllerTest : SysuiTestCase() { `when`(featureFlags.isEnabled(Flags.CHARGING_RIPPLE)).thenReturn(true) controller = WiredChargingRippleController( commandRegistry, batteryController, configurationController, - featureFlags, context, windowManager, - ViewCaptureAwareWindowManager(windowManager, - lazyViewCapture, isViewCaptureEnabled = false), systemClock, uiEventLogger) + featureFlags, context, windowManager, systemClock, uiEventLogger) rippleView.setupShader() controller.rippleView = rippleView // Replace the real ripple view with a mock instance controller.registerCallbacks() 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..41fdaa74e57b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -73,10 +73,10 @@ import android.view.IRemoteAnimationFinishedCallback; import android.view.RemoteAnimationTarget; import android.view.View; import android.view.ViewRootImpl; +import android.view.WindowManager; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.foldables.FoldGracePeriodProvider; import com.android.internal.logging.InstanceId; import com.android.internal.logging.UiEventLogger; @@ -175,7 +175,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private @Mock BroadcastDispatcher mBroadcastDispatcher; private @Mock DismissCallbackRegistry mDismissCallbackRegistry; private @Mock DumpManager mDumpManager; - private @Mock ViewCaptureAwareWindowManager mWindowManager; + private @Mock WindowManager mWindowManager; private @Mock IActivityManager mActivityManager; private @Mock ConfigurationController mConfigurationController; private @Mock PowerManager mPowerManager; @@ -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/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt index c7beb158c2de..add87686bc9f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt @@ -20,8 +20,8 @@ import android.content.Context import android.os.Handler import android.os.PowerManager import android.view.ViewGroup +import android.view.WindowManager import android.view.accessibility.AccessibilityManager -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.systemui.dump.DumpManager import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.ConfigurationController @@ -35,7 +35,7 @@ class FakeMediaTttChipControllerReceiver( commandQueue: CommandQueue, context: Context, logger: MediaTttReceiverLogger, - viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, + windowManager: WindowManager, mainExecutor: DelayableExecutor, accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, @@ -53,7 +53,7 @@ class FakeMediaTttChipControllerReceiver( commandQueue, context, logger, - viewCaptureAwareWindowManager, + windowManager, mainExecutor, accessibilityManager, configurationController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt index 378dd452d030..1aa6ac67ec27 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt @@ -31,8 +31,6 @@ import android.view.accessibility.AccessibilityManager import android.widget.ImageView import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.app.viewcapture.ViewCapture -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.internal.logging.InstanceId import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.SysuiTestCase @@ -75,8 +73,6 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { @Mock private lateinit var windowManager: WindowManager @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var rippleController: MediaTttReceiverRippleController - @Mock private lateinit var lazyViewCapture: Lazy<ViewCapture> - private lateinit var viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager private lateinit var commandQueueCallback: CommandQueue.Callbacks private lateinit var fakeAppIconDrawable: Drawable private lateinit var uiEventLoggerFake: UiEventLoggerFake @@ -114,18 +110,12 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { fakeWakeLockBuilder = WakeLockFake.Builder(context) fakeWakeLockBuilder.setWakeLock(fakeWakeLock) - viewCaptureAwareWindowManager = - ViewCaptureAwareWindowManager( - windowManager, - lazyViewCapture, - isViewCaptureEnabled = false, - ) controllerReceiver = FakeMediaTttChipControllerReceiver( commandQueue, context, logger, - viewCaptureAwareWindowManager, + windowManager, fakeExecutor, accessibilityManager, configurationController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt index c90ac5993c31..18de32e80da6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt @@ -34,7 +34,6 @@ import android.widget.TextView import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.app.viewcapture.ViewCapture -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.SysuiTestCase @@ -100,7 +99,6 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { @Mock private lateinit var windowManager: WindowManager @Mock private lateinit var vibratorHelper: VibratorHelper @Mock private lateinit var swipeHandler: SwipeChipbarAwayGestureHandler - @Mock private lateinit var lazyViewCapture: Lazy<ViewCapture> private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder private lateinit var fakeWakeLock: WakeLockFake private lateinit var chipbarCoordinator: ChipbarCoordinator @@ -145,11 +143,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { ChipbarCoordinator( context, chipbarLogger, - ViewCaptureAwareWindowManager( - windowManager, - lazyViewCapture, - isViewCaptureEnabled = false, - ), + windowManager, fakeExecutor, accessibilityManager, configurationController, @@ -1476,7 +1470,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.end_button) private fun ChipStateSender.getExpectedStateText( - otherDeviceName: String = OTHER_DEVICE_NAME + otherDeviceName: String = OTHER_DEVICE_NAME, ): String? { return this.getChipTextString(context, otherDeviceName).loadText(context) } @@ -1487,7 +1481,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo, - null, + null ) } @@ -1497,7 +1491,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo, - null, + null ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt index b4db6da2000a..b169cc12f08a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt @@ -27,7 +27,6 @@ import android.view.ViewConfiguration import android.view.WindowManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.internal.jank.Cuj import com.android.internal.util.LatencyTracker import com.android.systemui.SysuiTestCase @@ -64,7 +63,7 @@ class BackPanelControllerTest : SysuiTestCase() { private var triggerThreshold: Float = 0.0f private val touchSlop = ViewConfiguration.get(context).scaledEdgeSlop @Mock private lateinit var vibratorHelper: VibratorHelper - @Mock private lateinit var viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager + @Mock private lateinit var windowManager: WindowManager @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var latencyTracker: LatencyTracker private val interactionJankMonitor by lazy { kosmos.interactionJankMonitor } @@ -79,7 +78,7 @@ class BackPanelControllerTest : SysuiTestCase() { mBackPanelController = BackPanelController( context, - viewCaptureAwareWindowManager, + windowManager, ViewConfiguration.get(context), Handler.createAsync(testableLooper.looper), systemClock, 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/qs/tiles/dialog/InternetDetailsContentControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentControllerTest.java index e4a4953063bb..e949c8a10c9c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentControllerTest.java @@ -62,7 +62,6 @@ import android.view.WindowManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.logging.UiEventLogger; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.settingslib.wifi.WifiUtils; @@ -162,7 +161,7 @@ public class InternetDetailsContentControllerTest extends SysuiTestCase { @Mock InternetDetailsContentController.InternetDialogCallback mInternetDialogCallback; @Mock - private ViewCaptureAwareWindowManager mWindowManager; + private WindowManager mWindowManager; @Mock private ToastFactory mToastFactory; @Mock @@ -234,9 +233,8 @@ public class InternetDetailsContentControllerTest extends SysuiTestCase { mSubscriptionManager, mTelephonyManager, mWifiManager, mConnectivityManager, mHandler, mExecutor, mBroadcastDispatcher, mock(KeyguardUpdateMonitor.class), mGlobalSettings, mKeyguardStateController, - mWindowManager, mToastFactory, mWorkerHandler, - mCarrierConfigTracker, mLocationController, mDialogTransitionAnimator, - mWifiStateWorker, mFlags); + mWindowManager, mToastFactory, mWorkerHandler, mCarrierConfigTracker, + mLocationController, mDialogTransitionAnimator, mWifiStateWorker, mFlags); mSubscriptionManager.addOnSubscriptionsChangedListener(mExecutor, mInternetDetailsContentController.mOnSubscriptionsChangedListener); mInternetDetailsContentController.onStart(mInternetDialogCallback, true); 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/KeyboardShortcutListSearchTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java index 8045a13ff9be..07204ee814d2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java @@ -34,6 +34,7 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.systemui.SysuiTestCase; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import com.google.android.material.bottomsheet.BottomSheetDialog; @@ -61,6 +62,7 @@ public class KeyboardShortcutListSearchTest extends SysuiTestCase { @Mock private BottomSheetDialog mBottomSheetDialog; @Mock WindowManager mWindowManager; @Mock Handler mHandler; + @Mock WindowManagerProvider mWindowManagerProvider; @Before public void setUp() { @@ -77,7 +79,7 @@ public class KeyboardShortcutListSearchTest extends SysuiTestCase { public void toggle_isShowingTrue_instanceShouldBeNull() { when(mBottomSheetDialog.isShowing()).thenReturn(true); - mKeyboardShortcutListSearch.toggle(mContext, DEVICE_ID); + mKeyboardShortcutListSearch.toggle(mContext, DEVICE_ID, mWindowManagerProvider); assertThat(mKeyboardShortcutListSearch.sInstance).isNull(); } @@ -86,7 +88,7 @@ public class KeyboardShortcutListSearchTest extends SysuiTestCase { public void toggle_isShowingFalse_showKeyboardShortcuts() { when(mBottomSheetDialog.isShowing()).thenReturn(false); - mKeyboardShortcutListSearch.toggle(mContext, DEVICE_ID); + mKeyboardShortcutListSearch.toggle(mContext, DEVICE_ID, mWindowManagerProvider); verify(mWindowManager).requestAppKeyboardShortcuts(any(), anyInt()); verify(mWindowManager).requestImeKeyboardShortcuts(any(), anyInt()); @@ -96,7 +98,7 @@ public class KeyboardShortcutListSearchTest extends SysuiTestCase { public void requestAppKeyboardShortcuts_callback_sanitisesIcons() { KeyboardShortcutGroup group = createKeyboardShortcutGroupForIconTests(); - mKeyboardShortcutListSearch.toggle(mContext, DEVICE_ID); + mKeyboardShortcutListSearch.toggle(mContext, DEVICE_ID, mWindowManagerProvider); ArgumentCaptor<WindowManager.KeyboardShortcutsReceiver> callbackCaptor = ArgumentCaptor.forClass(WindowManager.KeyboardShortcutsReceiver.class); @@ -114,7 +116,7 @@ public class KeyboardShortcutListSearchTest extends SysuiTestCase { public void requestImeKeyboardShortcuts_callback_sanitisesIcons() { KeyboardShortcutGroup group = createKeyboardShortcutGroupForIconTests(); - mKeyboardShortcutListSearch.toggle(mContext, DEVICE_ID); + mKeyboardShortcutListSearch.toggle(mContext, DEVICE_ID, mWindowManagerProvider); ArgumentCaptor<WindowManager.KeyboardShortcutsReceiver> callbackCaptor = ArgumentCaptor.forClass(WindowManager.KeyboardShortcutsReceiver.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsReceiverTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsReceiverTest.java index 2cb9791cc159..0bd9b29a1a37 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsReceiverTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsReceiverTest.java @@ -36,6 +36,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.shared.recents.utilities.Utilities; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import org.junit.After; import org.junit.Before; @@ -59,6 +60,7 @@ public class KeyboardShortcutsReceiverTest extends SysuiTestCase { @Mock private KeyboardShortcuts mKeyboardShortcuts; @Mock private KeyboardShortcutListSearch mKeyboardShortcutListSearch; + @Mock private WindowManagerProvider mWindowManagerProvider; @Before public void setUp() { @@ -69,7 +71,8 @@ public class KeyboardShortcutsReceiverTest extends SysuiTestCase { KeyboardShortcuts.sInstance = mKeyboardShortcuts; KeyboardShortcutListSearch.sInstance = mKeyboardShortcutListSearch; - mKeyboardShortcutsReceiver = spy(new KeyboardShortcutsReceiver(mFeatureFlags)); + mKeyboardShortcutsReceiver = spy(new KeyboardShortcutsReceiver(mFeatureFlags, + mWindowManagerProvider)); } @Before diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java index 20ecaf75c625..939f2b899dbe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java @@ -39,6 +39,7 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.systemui.SysuiTestCase; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import org.junit.Before; import org.junit.Rule; @@ -67,6 +68,7 @@ public class KeyboardShortcutsTest extends SysuiTestCase { @Mock private Dialog mDialog; @Mock WindowManager mWindowManager; @Mock Handler mHandler; + @Mock WindowManagerProvider mWindowManagerProvider; @Before public void setUp() { @@ -92,7 +94,7 @@ public class KeyboardShortcutsTest extends SysuiTestCase { public void toggle_isShowingTrue_instanceShouldBeNull() { when(mDialog.isShowing()).thenReturn(true); - KeyboardShortcuts.toggle(mContext, DEVICE_ID); + KeyboardShortcuts.toggle(mContext, DEVICE_ID, mWindowManagerProvider); assertThat(KeyboardShortcuts.sInstance).isNull(); } @@ -101,7 +103,7 @@ public class KeyboardShortcutsTest extends SysuiTestCase { public void toggle_isShowingFalse_showKeyboardShortcuts() { when(mDialog.isShowing()).thenReturn(false); - KeyboardShortcuts.toggle(mContext, DEVICE_ID); + KeyboardShortcuts.toggle(mContext, DEVICE_ID, mWindowManagerProvider); verify(mWindowManager).requestAppKeyboardShortcuts(any(), anyInt()); verify(mWindowManager).requestImeKeyboardShortcuts(any(), anyInt()); @@ -131,7 +133,7 @@ public class KeyboardShortcutsTest extends SysuiTestCase { @Test public void requestAppKeyboardShortcuts_callback_sanitisesIcons() { KeyboardShortcutGroup group = createKeyboardShortcutGroupForIconTests(); - KeyboardShortcuts.toggle(mContext, DEVICE_ID); + KeyboardShortcuts.toggle(mContext, DEVICE_ID, mWindowManagerProvider); emitAppShortcuts(singletonList(group), DEVICE_ID); @@ -142,7 +144,7 @@ public class KeyboardShortcutsTest extends SysuiTestCase { @Test public void requestImeKeyboardShortcuts_callback_sanitisesIcons() { KeyboardShortcutGroup group = createKeyboardShortcutGroupForIconTests(); - KeyboardShortcuts.toggle(mContext, DEVICE_ID); + KeyboardShortcuts.toggle(mContext, DEVICE_ID, mWindowManagerProvider); emitImeShortcuts(singletonList(group), DEVICE_ID); @@ -153,7 +155,7 @@ public class KeyboardShortcutsTest extends SysuiTestCase { @Test public void onImeAndAppShortcutsReceived_appShortcutsNull_doesNotCrash() { KeyboardShortcutGroup group = createKeyboardShortcutGroupForIconTests(); - KeyboardShortcuts.toggle(mContext, DEVICE_ID); + KeyboardShortcuts.toggle(mContext, DEVICE_ID, mWindowManagerProvider); emitImeShortcuts(singletonList(group), DEVICE_ID); emitAppShortcuts(/* groups= */ null, DEVICE_ID); @@ -162,7 +164,7 @@ public class KeyboardShortcutsTest extends SysuiTestCase { @Test public void onImeAndAppShortcutsReceived_imeShortcutsNull_doesNotCrash() { KeyboardShortcutGroup group = createKeyboardShortcutGroupForIconTests(); - KeyboardShortcuts.toggle(mContext, DEVICE_ID); + KeyboardShortcuts.toggle(mContext, DEVICE_ID, mWindowManagerProvider); emitAppShortcuts(singletonList(group), DEVICE_ID); emitImeShortcuts(/* groups= */ null, DEVICE_ID); @@ -170,7 +172,7 @@ public class KeyboardShortcutsTest extends SysuiTestCase { @Test public void onImeAndAppShortcutsReceived_bothNull_doesNotCrash() { - KeyboardShortcuts.toggle(mContext, DEVICE_ID); + KeyboardShortcuts.toggle(mContext, DEVICE_ID, mWindowManagerProvider); emitImeShortcuts(/* groups= */ null, DEVICE_ID); emitAppShortcuts(/* groups= */ null, DEVICE_ID); 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/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index a7f3fdcb517e..0c5cbc299aee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -93,7 +93,6 @@ import android.view.WindowMetrics; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.compose.animation.scene.ObservableTransitionState; import com.android.internal.colorextraction.ColorExtractor; import com.android.internal.logging.UiEventLogger; @@ -214,6 +213,7 @@ import com.android.systemui.util.settings.FakeGlobalSettings; import com.android.systemui.util.settings.FakeSettings; import com.android.systemui.util.settings.SystemSettings; import com.android.systemui.util.time.FakeSystemClock; +import com.android.systemui.utils.windowmanager.WindowManagerProvider; import com.android.systemui.volume.VolumeComponent; import com.android.systemui.wallet.controller.QuickAccessWalletController; import com.android.wm.shell.bubbles.Bubbles; @@ -372,9 +372,10 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private GlanceableHubContainerController mGlanceableHubContainerController; @Mock private EmergencyGestureIntentFactory mEmergencyGestureIntentFactory; @Mock private NotificationSettingsInteractor mNotificationSettingsInteractor; - @Mock private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; @Mock private StatusBarLongPressGestureDetector mStatusBarLongPressGestureDetector; @Mock private QuickAccessWalletController mQuickAccessWalletController; + @Mock private WindowManager mWindowManager; + @Mock private WindowManagerProvider mWindowManagerProvider; private ShadeController mShadeController; private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); private final FakeGlobalSettings mFakeGlobalSettings = new FakeGlobalSettings(); @@ -642,8 +643,9 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mBrightnessMirrorShowingRepository, mGlanceableHubContainerController, mEmergencyGestureIntentFactory, - mViewCaptureAwareWindowManager, - mQuickAccessWalletController + mQuickAccessWalletController, + mWindowManager, + mWindowManagerProvider ); mScreenLifecycle.addObserver(mCentralSurfaces.mScreenObserver); mCentralSurfaces.initShadeVisibilityListener(); @@ -1363,15 +1365,13 @@ public class CentralSurfacesImplTest extends SysuiTestCase { private void switchToScreenSize(int widthDp, int heightDp) { WindowMetrics windowMetrics = Mockito.mock(WindowMetrics.class); - WindowManager windowManager = Mockito.mock(WindowManager.class); Configuration configuration = new Configuration(); configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT; mContext.getOrCreateTestableResources().overrideConfiguration(configuration); when(windowMetrics.getBounds()).thenReturn(new Rect(0, 0, widthDp, heightDp)); - when(windowManager.getCurrentWindowMetrics()).thenReturn(windowMetrics); - mContext.addMockSystemService(WindowManager.class, windowManager); + when(mWindowManager.getCurrentWindowMetrics()).thenReturn(windowMetrics); } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt index 68f66611c981..574b2c010a37 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt @@ -471,6 +471,51 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { } @Test + @EnableFlags(ShadeWindowGoesAround.FLAG_NAME) + fun onTouch_withMouseOnEndSideIcons_flagOn_propagatedToShadeDisplayPolicy() { + val view = createViewMock() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + controller = createAndInitController(view) + } + val event = getActionUpEventFromSource(InputDevice.SOURCE_MOUSE) + + val statusContainer = view.requireViewById<View>(R.id.system_icons) + statusContainer.dispatchTouchEvent(event) + + verify(statusBarTouchShadeDisplayPolicy).onStatusBarTouched(eq(event), any()) + } + + @Test + @EnableFlags(ShadeWindowGoesAround.FLAG_NAME) + fun onTouch_withMouseOnStartSideIcons_flagOn_propagatedToShadeDisplayPolicy() { + val view = createViewMock() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + controller = createAndInitController(view) + } + val event = getActionUpEventFromSource(InputDevice.SOURCE_MOUSE) + + val statusContainer = view.requireViewById<View>(R.id.status_bar_start_side_content) + statusContainer.dispatchTouchEvent(event) + + verify(statusBarTouchShadeDisplayPolicy).onStatusBarTouched(eq(event), any()) + } + + @Test + @DisableFlags(ShadeWindowGoesAround.FLAG_NAME) + fun onTouch_withMouseOnSystemIcons_flagOff_notPropagatedToShadeDisplayPolicy() { + val view = createViewMock() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + controller = createAndInitController(view) + } + val event = getActionUpEventFromSource(InputDevice.SOURCE_MOUSE) + + val statusContainer = view.requireViewById<View>(R.id.system_icons) + statusContainer.dispatchTouchEvent(event) + + verify(statusBarTouchShadeDisplayPolicy, never()).onStatusBarTouched(eq(event), any()) + } + + @Test fun shadeIsExpandedOnStatusIconMouseClick() { val view = createViewMock() InstrumentationRegistry.getInstrumentation().runOnMainSync { 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/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionKairosAdapterTelephonySmokeTests.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionKairosAdapterTelephonySmokeTests.kt new file mode 100644 index 000000000000..5695df5c307d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionKairosAdapterTelephonySmokeTests.kt @@ -0,0 +1,69 @@ +/* + * 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.data.repository.prod + +import androidx.test.filters.SmallTest +import com.android.systemui.activated +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.launchKairosNetwork +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import org.mockito.Mockito + +@OptIn(ExperimentalKairosApi::class, ExperimentalCoroutinesApi::class) +@SmallTest +class MobileConnectionKairosAdapterTelephonySmokeTests : MobileConnectionTelephonySmokeTestsBase() { + + var job: Job? = null + val kairosNetwork = testScope.backgroundScope.launchKairosNetwork() + + override fun recreateRepo(): MobileConnectionRepository { + lateinit var adapter: MobileConnectionRepositoryKairosAdapter + job?.cancel() + Mockito.clearInvocations(telephonyManager) + job = + testScope.backgroundScope.launch { + kairosNetwork.activateSpec { + val repo = activated { + MobileConnectionRepositoryKairosImpl( + MobileConnectionRepositoryTest.SUB_1_ID, + context, + subscriptionModel.toState(), + MobileConnectionRepositoryTest.DEFAULT_NAME_MODEL, + MobileConnectionRepositoryTest.SEP, + connectivityManager, + telephonyManager, + systemUiCarrierConfig, + fakeBroadcastDispatcher, + mobileMappings, + testDispatcher, + logger, + tableLogger, + flags, + ) + } + adapter = MobileConnectionRepositoryKairosAdapter(repo, systemUiCarrierConfig) + Unit + } + } + testScope.runCurrent() + return adapter + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryKairosAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryKairosAdapterTest.kt new file mode 100644 index 000000000000..0cb7c1eea268 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryKairosAdapterTest.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.activated +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.launchKairosNetwork +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import org.junit.runner.RunWith +import org.mockito.Mockito + +@OptIn(ExperimentalKairosApi::class, ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class MobileConnectionRepositoryKairosAdapterTest : MobileConnectionRepositoryTest() { + + var job: Job? = null + val kairosNetwork = testScope.backgroundScope.launchKairosNetwork() + + override fun recreateRepo(): MobileConnectionRepository { + lateinit var adapter: MobileConnectionRepositoryKairosAdapter + job?.cancel() + Mockito.clearInvocations(telephonyManager) + job = + testScope.backgroundScope.launch { + kairosNetwork.activateSpec { + val repo = activated { + MobileConnectionRepositoryKairosImpl( + SUB_1_ID, + context, + subscriptionModel.toState(), + DEFAULT_NAME_MODEL, + SEP, + connectivityManager, + telephonyManager, + systemUiCarrierConfig, + fakeBroadcastDispatcher, + mobileMappings, + testDispatcher, + logger, + tableLogger, + flags, + ) + } + adapter = MobileConnectionRepositoryKairosAdapter(repo, systemUiCarrierConfig) + Unit + } + } + testScope.runCurrent() // ensure the lateinit is set + return adapter + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt index ed8be9b253ab..2636195c0021 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt @@ -88,6 +88,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionMod import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig import com.android.systemui.statusbar.pipeline.mobile.data.model.testCarrierConfig import com.android.systemui.statusbar.pipeline.mobile.data.model.testCarrierConfigWithOverride +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.signalStrength import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.telephonyDisplayInfo @@ -116,25 +117,49 @@ import org.mockito.kotlin.argumentCaptor @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @SmallTest @RunWith(AndroidJUnit4::class) -class MobileConnectionRepositoryTest : SysuiTestCase() { - private lateinit var underTest: MobileConnectionRepositoryImpl +class MobileConnectionRepositoryImplTest : MobileConnectionRepositoryTest() { + override fun recreateRepo(): MobileConnectionRepository = + MobileConnectionRepositoryImpl( + SUB_1_ID, + context, + subscriptionModel, + DEFAULT_NAME_MODEL, + SEP, + connectivityManager, + telephonyManager, + systemUiCarrierConfig, + fakeBroadcastDispatcher, + mobileMappings, + testDispatcher, + logger, + tableLogger, + flags, + testScope.backgroundScope, + ) +} + +abstract class MobileConnectionRepositoryTest : SysuiTestCase() { + + abstract fun recreateRepo(): MobileConnectionRepository + + lateinit var underTest: MobileConnectionRepository - private val flags = + protected val flags = FakeFeatureFlagsClassic().also { it.set(ROAMING_INDICATOR_VIA_DISPLAY_INFO, true) } - @Mock private lateinit var connectivityManager: ConnectivityManager - @Mock private lateinit var telephonyManager: TelephonyManager - @Mock private lateinit var logger: MobileInputLogger - @Mock private lateinit var tableLogger: TableLogBuffer - @Mock private lateinit var context: Context + @Mock protected lateinit var connectivityManager: ConnectivityManager + @Mock protected lateinit var telephonyManager: TelephonyManager + @Mock protected lateinit var logger: MobileInputLogger + @Mock protected lateinit var tableLogger: TableLogBuffer + @Mock protected lateinit var context: Context - private val mobileMappings = FakeMobileMappingsProxy() - private val systemUiCarrierConfig = SystemUiCarrierConfig(SUB_1_ID, testCarrierConfig()) + protected val mobileMappings = FakeMobileMappingsProxy() + protected val systemUiCarrierConfig = SystemUiCarrierConfig(SUB_1_ID, testCarrierConfig()) - private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(testDispatcher) + protected val testDispatcher = UnconfinedTestDispatcher() + protected val testScope = TestScope(testDispatcher) - private val subscriptionModel: MutableStateFlow<SubscriptionModel?> = + protected val subscriptionModel: MutableStateFlow<SubscriptionModel?> = MutableStateFlow( SubscriptionModel( subscriptionId = SUB_1_ID, @@ -144,28 +169,11 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { ) @Before - fun setUp() { + fun setUpBase() { MockitoAnnotations.initMocks(this) whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID) - underTest = - MobileConnectionRepositoryImpl( - SUB_1_ID, - context, - subscriptionModel, - DEFAULT_NAME_MODEL, - SEP, - connectivityManager, - telephonyManager, - systemUiCarrierConfig, - fakeBroadcastDispatcher, - mobileMappings, - testDispatcher, - logger, - tableLogger, - flags, - testScope.backgroundScope, - ) + underTest = recreateRepo() } @Test @@ -400,6 +408,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { fun carrierId_initialValueCaptured() = testScope.runTest { whenever(telephonyManager.simCarrierId).thenReturn(1234) + underTest = recreateRepo() var latest: Int? = null val job = underTest.carrierId.onEach { latest = it }.launchIn(this) @@ -430,6 +439,8 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { @Test fun carrierNetworkChange() = testScope.runTest { + underTest = recreateRepo() + var latest: Boolean? = null val job = underTest.carrierNetworkChangeActive.onEach { latest = it }.launchIn(this) @@ -622,24 +633,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { flags.set(ROAMING_INDICATOR_VIA_DISPLAY_INFO, true) // Re-create the repository, because the flag is read at init - underTest = - MobileConnectionRepositoryImpl( - SUB_1_ID, - context, - subscriptionModel, - DEFAULT_NAME_MODEL, - SEP, - connectivityManager, - telephonyManager, - systemUiCarrierConfig, - fakeBroadcastDispatcher, - mobileMappings, - testDispatcher, - logger, - tableLogger, - flags, - testScope.backgroundScope, - ) + underTest = recreateRepo() var latest: Boolean? = null val job = underTest.isRoaming.onEach { latest = it }.launchIn(this) @@ -671,24 +665,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { flags.set(ROAMING_INDICATOR_VIA_DISPLAY_INFO, false) // Re-create the repository, because the flag is read at init - underTest = - MobileConnectionRepositoryImpl( - SUB_1_ID, - context, - subscriptionModel, - DEFAULT_NAME_MODEL, - SEP, - connectivityManager, - telephonyManager, - systemUiCarrierConfig, - fakeBroadcastDispatcher, - mobileMappings, - testDispatcher, - logger, - tableLogger, - flags, - testScope.backgroundScope, - ) + underTest = recreateRepo() var latest: Boolean? = null val job = underTest.isRoaming.onEach { latest = it }.launchIn(this) @@ -1441,14 +1418,14 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { } companion object { - private const val SUB_1_ID = 1 + const val SUB_1_ID = 1 - private const val DEFAULT_NAME = "Fake Mobile Network" - private val DEFAULT_NAME_MODEL = NetworkNameModel.Default(DEFAULT_NAME) - private const val SEP = "-" + const val DEFAULT_NAME = "Fake Mobile Network" + val DEFAULT_NAME_MODEL = NetworkNameModel.Default(DEFAULT_NAME) + const val SEP = "-" - private const val SPN = "testSpn" - private const val DATA_SPN = "testDataSpn" - private const val PLMN = "testPlmn" + const val SPN = "testSpn" + const val DATA_SPN = "testDataSpn" + const val PLMN = "testPlmn" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt index 6f21e795532b..caa6e21fd883 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt @@ -42,6 +42,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetwork import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig import com.android.systemui.statusbar.pipeline.mobile.data.model.testCarrierConfig +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.getTelephonyCallbackForType import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.signalStrength import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy @@ -92,47 +93,53 @@ import org.mockito.MockitoAnnotations */ @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @SmallTest -class MobileConnectionTelephonySmokeTests : SysuiTestCase() { - private lateinit var underTest: MobileConnectionRepositoryImpl +class MobileConnectionTelephonySmokeTests : MobileConnectionTelephonySmokeTestsBase() { + override fun recreateRepo(): MobileConnectionRepository = + MobileConnectionRepositoryImpl( + SUB_1_ID, + context, + subscriptionModel, + DEFAULT_NAME, + SEP, + connectivityManager, + telephonyManager, + systemUiCarrierConfig, + fakeBroadcastDispatcher, + mobileMappings, + testDispatcher, + logger, + tableLogger, + flags, + testScope.backgroundScope, + ) +} + +abstract class MobileConnectionTelephonySmokeTestsBase : SysuiTestCase() { + protected lateinit var underTest: MobileConnectionRepository - private val flags = + protected val flags = FakeFeatureFlagsClassic().also { it.set(Flags.ROAMING_INDICATOR_VIA_DISPLAY_INFO, true) } - @Mock private lateinit var connectivityManager: ConnectivityManager - @Mock private lateinit var telephonyManager: TelephonyManager - @Mock private lateinit var logger: MobileInputLogger - @Mock private lateinit var tableLogger: TableLogBuffer - @Mock private lateinit var subscriptionModel: StateFlow<SubscriptionModel?> + @Mock protected lateinit var connectivityManager: ConnectivityManager + @Mock protected lateinit var telephonyManager: TelephonyManager + @Mock protected lateinit var logger: MobileInputLogger + @Mock protected lateinit var tableLogger: TableLogBuffer + @Mock protected lateinit var subscriptionModel: StateFlow<SubscriptionModel?> - private val mobileMappings = FakeMobileMappingsProxy() - private val systemUiCarrierConfig = SystemUiCarrierConfig(SUB_1_ID, testCarrierConfig()) + protected val mobileMappings = FakeMobileMappingsProxy() + protected val systemUiCarrierConfig = SystemUiCarrierConfig(SUB_1_ID, testCarrierConfig()) - private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(testDispatcher) + protected val testDispatcher = UnconfinedTestDispatcher() + protected val testScope = TestScope(testDispatcher) + + abstract fun recreateRepo(): MobileConnectionRepository @Before fun setUp() { MockitoAnnotations.initMocks(this) whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID) - underTest = - MobileConnectionRepositoryImpl( - SUB_1_ID, - context, - subscriptionModel, - DEFAULT_NAME, - SEP, - connectivityManager, - telephonyManager, - systemUiCarrierConfig, - fakeBroadcastDispatcher, - mobileMappings, - testDispatcher, - logger, - tableLogger, - flags, - testScope.backgroundScope, - ) + underTest = recreateRepo() } @Test @@ -329,9 +336,9 @@ class MobileConnectionTelephonySmokeTests : SysuiTestCase() { } companion object { - private const val SUB_1_ID = 1 + const val SUB_1_ID = 1 - private val DEFAULT_NAME = NetworkNameModel.Default("default name") - private const val SEP = "-" + val DEFAULT_NAME = NetworkNameModel.Default("default name") + const val SEP = "-" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryKairosAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryKairosAdapterTest.kt new file mode 100644 index 000000000000..65849ae103c4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryKairosAdapterTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod + +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.launchKairosNetwork +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeCarrierConfigRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryKairosAdapter +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import org.mockito.Mockito +import org.mockito.kotlin.mock + +@OptIn(ExperimentalKairosApi::class, ExperimentalCoroutinesApi::class) +@SmallTest +// This is required because our [SubscriptionManager.OnSubscriptionsChangedListener] uses a looper +// to run the callback and this makes the looper place nicely with TestScope etc. +@TestableLooper.RunWithLooper +class MobileConnectionsRepositoryKairosAdapterTest : + MobileConnectionsRepositoryTest<MobileConnectionsRepositoryKairosAdapter>() { + + var job: Job? = null + val kairosNetwork = testScope.backgroundScope.launchKairosNetwork() + + override fun recreateRepo(): MobileConnectionsRepositoryKairosAdapter { + val carrierConfigRepo = FakeCarrierConfigRepository() + lateinit var connectionsRepo: MobileConnectionsRepositoryKairosImpl + connectionsRepo = + MobileConnectionsRepositoryKairosImpl( + connectivityRepository = connectivityRepository, + subscriptionManager = subscriptionManager, + subscriptionManagerProxy = subscriptionManagerProxy, + telephonyManager = telephonyManager, + logger = logger, + tableLogger = summaryLogger, + mobileMappingsProxy = mobileMappings, + broadcastDispatcher = fakeBroadcastDispatcher, + context = context, + bgDispatcher = testDispatcher, + mainDispatcher = testDispatcher, + airplaneModeRepository = airplaneModeRepository, + wifiRepository = wifiRepository, + keyguardUpdateMonitor = updateMonitor, + dumpManager = mock(), + mobileRepoFactory = { + MobileConnectionRepositoryKairosFactoryImpl( + context = context, + connectionsRepo = connectionsRepo, + logFactory = logBufferFactory, + carrierConfigRepo = carrierConfigRepo, + telephonyManager = telephonyManager, + mobileRepoFactory = { + subId, + mobileLogger, + subscriptionModel, + defaultNetworkName, + networkNameSeparator, + systemUiCarrierConfig, + telephonyManager -> + MobileConnectionRepositoryKairosImpl( + subId = subId, + context = context, + subscriptionModel = subscriptionModel, + defaultNetworkName = defaultNetworkName, + networkNameSeparator = networkNameSeparator, + connectivityManager = connectivityManager, + telephonyManager = telephonyManager, + systemUiCarrierConfig = systemUiCarrierConfig, + broadcastDispatcher = fakeBroadcastDispatcher, + mobileMappingsProxy = mobileMappings, + bgDispatcher = testDispatcher, + logger = logger, + tableLogBuffer = mobileLogger, + flags = flags, + ) + }, + mergedRepoFactory = + CarrierMergedConnectionRepositoryKairos.Factory( + telephonyManager, + wifiRepository, + ), + ) + }, + ) + + val adapter = + MobileConnectionsRepositoryKairosAdapter( + kairosRepo = connectionsRepo, + kairosNetwork = kairosNetwork, + scope = testScope.backgroundScope, + connectivityRepository = connectivityRepository, + context = context, + carrierConfigRepo = carrierConfigRepo, + ) + + job?.cancel() + Mockito.clearInvocations(telephonyManager) + job = + testScope.backgroundScope.launch { + kairosNetwork.activateSpec { + connectionsRepo.run { activate() } + adapter.run { activate() } + } + } + testScope.runCurrent() // ensure everything is activated + return adapter + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt index d1d6e27332b0..c3662880c5cf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -37,6 +37,7 @@ import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET import android.telephony.TelephonyCallback import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener +import android.telephony.TelephonyCallback.EmergencyCallbackModeListener import android.telephony.TelephonyManager import android.testing.TestableLooper import androidx.test.filters.SmallTest @@ -58,6 +59,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.carrierConfigRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.tableBufferLogName import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy @@ -78,6 +80,7 @@ import com.android.wifitrackerlib.MergedCarrierEntry import com.android.wifitrackerlib.WifiEntry import com.android.wifitrackerlib.WifiPickerTracker import com.google.common.truth.Truth.assertThat +import java.time.Duration import java.util.UUID import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn @@ -93,6 +96,7 @@ import org.junit.Test import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock +import org.mockito.Mockito.atLeast import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations import org.mockito.kotlin.any @@ -104,34 +108,311 @@ import org.mockito.kotlin.whenever // This is required because our [SubscriptionManager.OnSubscriptionsChangedListener] uses a looper // to run the callback and this makes the looper place nicely with TestScope etc. @TestableLooper.RunWithLooper -class MobileConnectionsRepositoryTest : SysuiTestCase() { +class MobileConnectionsRepositoryImplTest : + MobileConnectionsRepositoryTest<MobileConnectionsRepositoryImpl>() { + override fun recreateRepo() = + MobileConnectionsRepositoryImpl( + connectivityRepository = connectivityRepository, + subscriptionManager = subscriptionManager, + subscriptionManagerProxy = subscriptionManagerProxy, + telephonyManager = telephonyManager, + logger = logger, + tableLogger = summaryLogger, + mobileMappingsProxy = mobileMappings, + broadcastDispatcher = fakeBroadcastDispatcher, + context = context, + bgDispatcher = testDispatcher, + scope = testScope.backgroundScope, + mainDispatcher = testDispatcher, + airplaneModeRepository = airplaneModeRepository, + wifiRepository = wifiRepository, + fullMobileRepoFactory = fullConnectionFactory, + keyguardUpdateMonitor = updateMonitor, + dumpManager = mock(), + ) + + @Test + fun activeDataSentBeforeSubscriptionList_subscriptionReusesActiveDataRepo() = + testScope.runTest { + val activeRepo by collectLastValue(underTest.activeMobileDataRepository) + collectLastValue(underTest.subscriptions) + + // GIVEN active repo is updated before the subscription list updates + getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() + .onActiveDataSubscriptionIdChanged(SUB_2_ID) + + assertThat(activeRepo).isNotNull() + + // GIVEN the subscription list is then updated which includes the active data sub id + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + // WHEN requesting a connection repository for the subscription + val newRepo = underTest.getRepoForSubId(SUB_2_ID) + + // THEN the newly request repo has been cached and reused + assertThat(activeRepo).isSameInstanceAs(newRepo) + } + + @Test + fun testConnectionRepository_invalidSubId_doesNotThrow() = + testScope.runTest { + underTest.getRepoForSubId(SUB_1_ID) + // No exception + } + + @Test + fun testConnectionRepository_carrierMergedAndMobileSubs_usesCorrectRepos() = + testScope.runTest { + collectLastValue(underTest.subscriptions) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) + setWifiState(isCarrierMerged = true) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + val mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + } + + @Test + fun testSubscriptions_subNoLongerCarrierMerged_repoUpdates() = + testScope.runTest { + collectLastValue(underTest.subscriptions) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) + setWifiState(isCarrierMerged = true) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + var mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + + // WHEN the wifi network updates to be not carrier merged + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_ACTIVE) + setWifiState(isCarrierMerged = false) + runCurrent() + + // THEN the repos update + val noLongerCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(noLongerCarrierMergedRepo.getIsCarrierMerged()).isFalse() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + } + + @Test + fun testSubscriptions_subBecomesCarrierMerged_repoUpdates() = + testScope.runTest { + collectLastValue(underTest.subscriptions) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_ACTIVE) + setWifiState(isCarrierMerged = false) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + runCurrent() + + val notYetCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + var mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(notYetCarrierMergedRepo.getIsCarrierMerged()).isFalse() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + + // WHEN the wifi network updates to be carrier merged + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) + setWifiState(isCarrierMerged = true) + runCurrent() + + // THEN the repos update + val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + } + + @Test + @Ignore("b/333912012") + fun testConnectionCache_clearsInvalidSubscriptions() = + testScope.runTest { + collectLastValue(underTest.subscriptions) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + // Get repos to trigger caching + val repo1 = underTest.getRepoForSubId(SUB_1_ID) + val repo2 = underTest.getRepoForSubId(SUB_2_ID) + + assertThat(underTest.getSubIdRepoCache()) + .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2) + + // SUB_2 disappears + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1) + } + + @Test + @Ignore("b/333912012") + fun testConnectionCache_clearsInvalidSubscriptions_includingCarrierMerged() = + testScope.runTest { + collectLastValue(underTest.subscriptions) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) + setWifiState(isCarrierMerged = true) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + // Get repos to trigger caching + val repo1 = underTest.getRepoForSubId(SUB_1_ID) + val repo2 = underTest.getRepoForSubId(SUB_2_ID) + val repoCarrierMerged = underTest.getRepoForSubId(SUB_CM_ID) + + assertThat(underTest.getSubIdRepoCache()) + .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2, SUB_CM_ID, repoCarrierMerged) + + // SUB_2 and SUB_CM disappear + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1) + } + + /** Regression test for b/261706421 */ + @Test + @Ignore("b/333912012") + fun testConnectionsCache_clearMultipleSubscriptionsAtOnce_doesNotThrow() = + testScope.runTest { + collectLastValue(underTest.subscriptions) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + // Get repos to trigger caching + val repo1 = underTest.getRepoForSubId(SUB_1_ID) + val repo2 = underTest.getRepoForSubId(SUB_2_ID) + + assertThat(underTest.getSubIdRepoCache()) + .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2) + + // All subscriptions disappear + whenever(subscriptionManager.completeActiveSubscriptionInfoList).thenReturn(listOf()) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(underTest.getSubIdRepoCache()).isEmpty() + } + + @Test + fun getRepoForSubId_activeDataSubIdIsRequestedBeforeSubscriptionsUpdate() = + testScope.runTest { + var latestActiveRepo: MobileConnectionRepository? = null + collectLastValue( + underTest.activeMobileDataSubscriptionId.filterNotNull().onEach { + latestActiveRepo = underTest.getRepoForSubId(it) + } + ) + + val latestSubscriptions by collectLastValue(underTest.subscriptions) + + // Active data subscription id is sent, but no subscription change has been posted yet + getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() + .onActiveDataSubscriptionIdChanged(SUB_2_ID) + + // Subscriptions list is empty + assertThat(latestSubscriptions).isEmpty() + // getRepoForSubId does not throw + assertThat(latestActiveRepo).isNotNull() + } + + @Test + fun testConnectionsCache_keepsReposCached() = + testScope.runTest { + // Collect subscriptions to start the job + collectLastValue(underTest.subscriptions) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1)) + getSubscriptionCallback().onSubscriptionsChanged() + + val repo1_1 = underTest.getRepoForSubId(SUB_1_ID) + + // All subscriptions disappear + whenever(subscriptionManager.completeActiveSubscriptionInfoList).thenReturn(listOf()) + getSubscriptionCallback().onSubscriptionsChanged() + + // Sub1 comes back + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1)) + getSubscriptionCallback().onSubscriptionsChanged() + + val repo1_2 = underTest.getRepoForSubId(SUB_1_ID) + + assertThat(repo1_1).isSameInstanceAs(repo1_2) + } + + @Test + fun testConnectionsCache_doesNotDropReferencesThatHaveBeenRealized() = + testScope.runTest { + // Collect subscriptions to start the job + collectLastValue(underTest.subscriptions) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1)) + getSubscriptionCallback().onSubscriptionsChanged() + + // Client grabs a reference to a repository, but doesn't keep it around + underTest.getRepoForSubId(SUB_1_ID) + + // All subscriptions disappear + whenever(subscriptionManager.completeActiveSubscriptionInfoList).thenReturn(listOf()) + getSubscriptionCallback().onSubscriptionsChanged() + + val repo1 = underTest.getRepoForSubId(SUB_1_ID) + + assertThat(repo1).isNotNull() + } +} + +abstract class MobileConnectionsRepositoryTest<T : MobileConnectionsRepository> : SysuiTestCase() { private val kosmos = testKosmos() - private val flags = + protected val flags = FakeFeatureFlagsClassic().also { it.set(Flags.ROAMING_INDICATOR_VIA_DISPLAY_INFO, true) } private lateinit var connectionFactory: MobileConnectionRepositoryImpl.Factory private lateinit var carrierMergedFactory: CarrierMergedConnectionRepository.Factory - private lateinit var fullConnectionFactory: FullMobileConnectionRepository.Factory - private lateinit var connectivityRepository: ConnectivityRepository - private lateinit var airplaneModeRepository: FakeAirplaneModeRepository - private lateinit var wifiRepository: WifiRepository + protected lateinit var fullConnectionFactory: FullMobileConnectionRepository.Factory + protected lateinit var connectivityRepository: ConnectivityRepository + protected lateinit var airplaneModeRepository: FakeAirplaneModeRepository + protected lateinit var wifiRepository: WifiRepository private lateinit var carrierConfigRepository: CarrierConfigRepository - @Mock private lateinit var connectivityManager: ConnectivityManager - @Mock private lateinit var subscriptionManager: SubscriptionManager - @Mock private lateinit var telephonyManager: TelephonyManager - @Mock private lateinit var logger: MobileInputLogger - private val summaryLogger = logcatTableLogBuffer(kosmos, "summaryLogger") - @Mock private lateinit var logBufferFactory: TableLogBufferFactory - @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor + @Mock protected lateinit var connectivityManager: ConnectivityManager + @Mock protected lateinit var subscriptionManager: SubscriptionManager + @Mock protected lateinit var telephonyManager: TelephonyManager + @Mock protected lateinit var logger: MobileInputLogger + protected val summaryLogger = logcatTableLogBuffer(kosmos, "summaryLogger") + @Mock protected lateinit var logBufferFactory: TableLogBufferFactory + @Mock protected lateinit var updateMonitor: KeyguardUpdateMonitor @Mock private lateinit var wifiManager: WifiManager @Mock private lateinit var wifiPickerTrackerFactory: WifiPickerTrackerFactory @Mock private lateinit var wifiPickerTracker: WifiPickerTracker private val wifiTableLogBuffer = logcatTableLogBuffer(kosmos, "wifiTableLog") - private val mobileMappings = FakeMobileMappingsProxy() - private val subscriptionManagerProxy = FakeSubscriptionManagerProxy() + protected val mobileMappings = FakeMobileMappingsProxy() + protected val subscriptionManagerProxy = FakeSubscriptionManagerProxy() private val mainExecutor = FakeExecutor(FakeSystemClock()) private val wifiLogBuffer = LogBuffer("wifi", maxSize = 100, logcatEchoTracker = mock()) private val wifiPickerTrackerCallback = @@ -139,10 +420,10 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { private val vcnTransportInfo = VcnTransportInfo.Builder().build() private val userRepository = kosmos.fakeUserRepository - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) + protected val testDispatcher = StandardTestDispatcher() + protected val testScope = TestScope(testDispatcher) - private lateinit var underTest: MobileConnectionsRepositoryImpl + protected lateinit var underTest: T @Before fun setUp() { @@ -237,30 +518,13 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { carrierMergedRepoFactory = carrierMergedFactory, ) - underTest = - MobileConnectionsRepositoryImpl( - connectivityRepository, - subscriptionManager, - subscriptionManagerProxy, - telephonyManager, - logger, - summaryLogger, - mobileMappings, - fakeBroadcastDispatcher, - context, - /* bgDispatcher = */ testDispatcher, - testScope.backgroundScope, - /* mainDispatcher = */ testDispatcher, - airplaneModeRepository, - wifiRepository, - fullConnectionFactory, - updateMonitor, - mock(), - ) + underTest = recreateRepo() testScope.runCurrent() } + abstract fun recreateRepo(): T + @Test fun testSubscriptions_initiallyEmpty() = testScope.runTest { @@ -410,9 +674,17 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { fun activeRepo_updatesWithActiveDataId() = testScope.runTest { val latest by collectLastValue(underTest.activeMobileDataRepository) + runCurrent() - getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() - .onActiveDataSubscriptionIdChanged(SUB_2_ID) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_2)) + getSubscriptionCallbacks().forEach { it.onSubscriptionsChanged() } + runCurrent() + + getTelephonyCallbacksForType<ActiveDataSubscriptionIdListener>().forEach { + it.onActiveDataSubscriptionIdChanged(SUB_2_ID) + } + runCurrent() assertThat(latest?.subId).isEqualTo(SUB_2_ID) } @@ -422,6 +694,10 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.activeMobileDataRepository) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_2)) + getSubscriptionCallbacks().forEach { it.onSubscriptionsChanged() } + getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() .onActiveDataSubscriptionIdChanged(SUB_2_ID) @@ -437,60 +713,15 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { /** Regression test for b/268146648. */ fun activeSubIdIsSetBeforeSubscriptionsAreUpdated_doesNotThrow() = testScope.runTest { - val activeRepo by collectLastValue(underTest.activeMobileDataRepository) + val activeRepo = collectLastValue(underTest.activeMobileDataRepository) val subscriptions by collectLastValue(underTest.subscriptions) - getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() - .onActiveDataSubscriptionIdChanged(SUB_2_ID) + getTelephonyCallbacksForType<ActiveDataSubscriptionIdListener>().forEach { + it.onActiveDataSubscriptionIdChanged(SUB_2_ID) + } assertThat(subscriptions).isEmpty() - assertThat(activeRepo).isNotNull() - } - - @Test - fun getRepoForSubId_activeDataSubIdIsRequestedBeforeSubscriptionsUpdate() = - testScope.runTest { - var latestActiveRepo: MobileConnectionRepository? = null - collectLastValue( - underTest.activeMobileDataSubscriptionId.filterNotNull().onEach { - latestActiveRepo = underTest.getRepoForSubId(it) - } - ) - - val latestSubscriptions by collectLastValue(underTest.subscriptions) - - // Active data subscription id is sent, but no subscription change has been posted yet - getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() - .onActiveDataSubscriptionIdChanged(SUB_2_ID) - - // Subscriptions list is empty - assertThat(latestSubscriptions).isEmpty() - // getRepoForSubId does not throw - assertThat(latestActiveRepo).isNotNull() - } - - @Test - fun activeDataSentBeforeSubscriptionList_subscriptionReusesActiveDataRepo() = - testScope.runTest { - val activeRepo by collectLastValue(underTest.activeMobileDataRepository) - collectLastValue(underTest.subscriptions) - - // GIVEN active repo is updated before the subscription list updates - getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() - .onActiveDataSubscriptionIdChanged(SUB_2_ID) - - assertThat(activeRepo).isNotNull() - - // GIVEN the subscription list is then updated which includes the active data sub id - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_2)) - getSubscriptionCallback().onSubscriptionsChanged() - - // WHEN requesting a connection repository for the subscription - val newRepo = underTest.getRepoForSubId(SUB_2_ID) - - // THEN the newly request repo has been cached and reused - assertThat(activeRepo).isSameInstanceAs(newRepo) + activeRepo.invoke() // does not throw } @Test @@ -501,6 +732,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { whenever(subscriptionManager.completeActiveSubscriptionInfoList) .thenReturn(listOf(SUB_1)) getSubscriptionCallback().onSubscriptionsChanged() + runCurrent() val repo1 = underTest.getRepoForSubId(SUB_1_ID) val repo2 = underTest.getRepoForSubId(SUB_1_ID) @@ -525,80 +757,6 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { assertThat(repo1).isSameInstanceAs(repo2) } - @Test - fun testConnectionRepository_carrierMergedAndMobileSubs_usesCorrectRepos() = - testScope.runTest { - collectLastValue(underTest.subscriptions) - - getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) - setWifiState(isCarrierMerged = true) - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1, SUB_CM)) - getSubscriptionCallback().onSubscriptionsChanged() - - val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) - val mobileRepo = underTest.getRepoForSubId(SUB_1_ID) - assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue() - assertThat(mobileRepo.getIsCarrierMerged()).isFalse() - } - - @Test - fun testSubscriptions_subNoLongerCarrierMerged_repoUpdates() = - testScope.runTest { - collectLastValue(underTest.subscriptions) - - getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) - setWifiState(isCarrierMerged = true) - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1, SUB_CM)) - getSubscriptionCallback().onSubscriptionsChanged() - - val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) - var mobileRepo = underTest.getRepoForSubId(SUB_1_ID) - assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue() - assertThat(mobileRepo.getIsCarrierMerged()).isFalse() - - // WHEN the wifi network updates to be not carrier merged - getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_ACTIVE) - setWifiState(isCarrierMerged = false) - runCurrent() - - // THEN the repos update - val noLongerCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) - mobileRepo = underTest.getRepoForSubId(SUB_1_ID) - assertThat(noLongerCarrierMergedRepo.getIsCarrierMerged()).isFalse() - assertThat(mobileRepo.getIsCarrierMerged()).isFalse() - } - - @Test - fun testSubscriptions_subBecomesCarrierMerged_repoUpdates() = - testScope.runTest { - collectLastValue(underTest.subscriptions) - - getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_ACTIVE) - setWifiState(isCarrierMerged = false) - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1, SUB_CM)) - getSubscriptionCallback().onSubscriptionsChanged() - runCurrent() - - val notYetCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) - var mobileRepo = underTest.getRepoForSubId(SUB_1_ID) - assertThat(notYetCarrierMergedRepo.getIsCarrierMerged()).isFalse() - assertThat(mobileRepo.getIsCarrierMerged()).isFalse() - - // WHEN the wifi network updates to be carrier merged - getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) - setWifiState(isCarrierMerged = true) - runCurrent() - - // THEN the repos update - val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) - mobileRepo = underTest.getRepoForSubId(SUB_1_ID) - assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue() - assertThat(mobileRepo.getIsCarrierMerged()).isFalse() - } - @SuppressLint("UnspecifiedRegisterReceiverFlag") @Test fun testDeviceEmergencyCallState_eagerlyChecksState() = @@ -674,139 +832,6 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @Test - @Ignore("b/333912012") - fun testConnectionCache_clearsInvalidSubscriptions() = - testScope.runTest { - collectLastValue(underTest.subscriptions) - - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1, SUB_2)) - getSubscriptionCallback().onSubscriptionsChanged() - - // Get repos to trigger caching - val repo1 = underTest.getRepoForSubId(SUB_1_ID) - val repo2 = underTest.getRepoForSubId(SUB_2_ID) - - assertThat(underTest.getSubIdRepoCache()) - .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2) - - // SUB_2 disappears - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1)) - getSubscriptionCallback().onSubscriptionsChanged() - - assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1) - } - - @Test - @Ignore("b/333912012") - fun testConnectionCache_clearsInvalidSubscriptions_includingCarrierMerged() = - testScope.runTest { - collectLastValue(underTest.subscriptions) - - getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) - setWifiState(isCarrierMerged = true) - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1, SUB_2, SUB_CM)) - getSubscriptionCallback().onSubscriptionsChanged() - - // Get repos to trigger caching - val repo1 = underTest.getRepoForSubId(SUB_1_ID) - val repo2 = underTest.getRepoForSubId(SUB_2_ID) - val repoCarrierMerged = underTest.getRepoForSubId(SUB_CM_ID) - - assertThat(underTest.getSubIdRepoCache()) - .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2, SUB_CM_ID, repoCarrierMerged) - - // SUB_2 and SUB_CM disappear - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1)) - getSubscriptionCallback().onSubscriptionsChanged() - - assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1) - } - - /** Regression test for b/261706421 */ - @Test - @Ignore("b/333912012") - fun testConnectionsCache_clearMultipleSubscriptionsAtOnce_doesNotThrow() = - testScope.runTest { - collectLastValue(underTest.subscriptions) - - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1, SUB_2)) - getSubscriptionCallback().onSubscriptionsChanged() - - // Get repos to trigger caching - val repo1 = underTest.getRepoForSubId(SUB_1_ID) - val repo2 = underTest.getRepoForSubId(SUB_2_ID) - - assertThat(underTest.getSubIdRepoCache()) - .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2) - - // All subscriptions disappear - whenever(subscriptionManager.completeActiveSubscriptionInfoList).thenReturn(listOf()) - getSubscriptionCallback().onSubscriptionsChanged() - - assertThat(underTest.getSubIdRepoCache()).isEmpty() - } - - @Test - fun testConnectionsCache_keepsReposCached() = - testScope.runTest { - // Collect subscriptions to start the job - collectLastValue(underTest.subscriptions) - - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1)) - getSubscriptionCallback().onSubscriptionsChanged() - - val repo1_1 = underTest.getRepoForSubId(SUB_1_ID) - - // All subscriptions disappear - whenever(subscriptionManager.completeActiveSubscriptionInfoList).thenReturn(listOf()) - getSubscriptionCallback().onSubscriptionsChanged() - - // Sub1 comes back - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1)) - getSubscriptionCallback().onSubscriptionsChanged() - - val repo1_2 = underTest.getRepoForSubId(SUB_1_ID) - - assertThat(repo1_1).isSameInstanceAs(repo1_2) - } - - @Test - fun testConnectionsCache_doesNotDropReferencesThatHaveBeenRealized() = - testScope.runTest { - // Collect subscriptions to start the job - collectLastValue(underTest.subscriptions) - - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1)) - getSubscriptionCallback().onSubscriptionsChanged() - - // Client grabs a reference to a repository, but doesn't keep it around - underTest.getRepoForSubId(SUB_1_ID) - - // All subscriptions disappear - whenever(subscriptionManager.completeActiveSubscriptionInfoList).thenReturn(listOf()) - getSubscriptionCallback().onSubscriptionsChanged() - - val repo1 = underTest.getRepoForSubId(SUB_1_ID) - - assertThat(repo1).isNotNull() - } - - @Test - fun testConnectionRepository_invalidSubId_doesNotThrow() = - testScope.runTest { - underTest.getRepoForSubId(SUB_1_ID) - // No exception - } - - @Test fun connectionRepository_logBufferContainsSubIdInItsName() = testScope.runTest { collectLastValue(underTest.subscriptions) @@ -814,6 +839,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { whenever(subscriptionManager.completeActiveSubscriptionInfoList) .thenReturn(listOf(SUB_1, SUB_2)) getSubscriptionCallback().onSubscriptionsChanged() + runCurrent() // Get repos to trigger creation underTest.getRepoForSubId(SUB_1_ID) @@ -848,15 +874,27 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { fun defaultDataSubId_fetchesInitialValueOnStart() = testScope.runTest { subscriptionManagerProxy.defaultDataSubId = 2 + underTest = recreateRepo() + val latest by collectLastValue(underTest.defaultDataSubId) assertThat(latest).isEqualTo(2) } + private fun setDefaultDataSubId(subId: Int) { + subscriptionManagerProxy.defaultDataSubId = subId + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED).apply { + putExtra(PhoneConstants.SUBSCRIPTION_KEY, subId) + }, + ) + } + @Test fun defaultDataSubId_filtersOutInvalidSubIds() = testScope.runTest { - subscriptionManagerProxy.defaultDataSubId = INVALID_SUBSCRIPTION_ID + setDefaultDataSubId(INVALID_SUBSCRIPTION_ID) val latest by collectLastValue(underTest.defaultDataSubId) assertThat(latest).isNull() @@ -865,15 +903,12 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { @Test fun defaultDataSubId_filtersOutInvalidSubIds_fromValidToInvalid() = testScope.runTest { - subscriptionManagerProxy.defaultDataSubId = 2 + setDefaultDataSubId(2) val latest by collectLastValue(underTest.defaultDataSubId) assertThat(latest).isEqualTo(2) - val intent = - Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) - .putExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID) - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent) + setDefaultDataSubId(INVALID_SUBSCRIPTION_ID) assertThat(latest).isNull() } @@ -881,7 +916,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { @Test fun defaultDataSubId_fetchesCurrentOnRestart() = testScope.runTest { - subscriptionManagerProxy.defaultDataSubId = 2 + setDefaultDataSubId(2) var latest: Int? = null var job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this) runCurrent() @@ -894,7 +929,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { latest = null - subscriptionManagerProxy.defaultDataSubId = 1 + setDefaultDataSubId(1) job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this) runCurrent() @@ -1281,26 +1316,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { // The initial value will be fetched when the repo is created, so we need to override // the resources and then re-create the repo. - underTest = - MobileConnectionsRepositoryImpl( - connectivityRepository, - subscriptionManager, - subscriptionManagerProxy, - telephonyManager, - logger, - summaryLogger, - mobileMappings, - fakeBroadcastDispatcher, - context, - testDispatcher, - testScope.backgroundScope, - testDispatcher, - airplaneModeRepository, - wifiRepository, - fullConnectionFactory, - updateMonitor, - mock(), - ) + underTest = recreateRepo() val latest by collectLastValue(underTest.defaultDataSubRatConfig) @@ -1428,6 +1444,12 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { assertThat(underTest.getIsAnySimSecure()).isFalse() whenever(updateMonitor.isSimPinSecure).thenReturn(true) + org.mockito.kotlin + .argumentCaptor<KeyguardUpdateMonitorCallback>() + .apply { verify(updateMonitor, atLeast(0)).registerCallback(capture()) } + .allValues + .forEach { it.onSimStateChanged(0, 0, 0) } + runCurrent() assertThat(underTest.getIsAnySimSecure()).isTrue() } @@ -1447,19 +1469,26 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { testScope.runTest { whenever(telephonyManager.emergencyCallbackMode).thenReturn(true) + getTelephonyCallbacksForType<EmergencyCallbackModeListener>().forEach { + it.onCallbackModeStarted( + TelephonyManager.EMERGENCY_CALLBACK_MODE_SMS, + Duration.ZERO, + 0, + ) + } runCurrent() assertThat(underTest.isInEcmMode()).isTrue() } - private fun TestScope.getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback { + protected fun TestScope.getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback { runCurrent() val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>() verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture()) return callbackCaptor.value!! } - private fun setWifiState(isCarrierMerged: Boolean) { + protected fun setWifiState(isCarrierMerged: Boolean) { if (isCarrierMerged) { val mergedEntry = mock<MergedCarrierEntry>().apply { @@ -1481,7 +1510,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { wifiPickerTrackerCallback.value.onWifiEntriesChanged() } - private fun TestScope.getSubscriptionCallback(): + protected fun TestScope.getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener { runCurrent() val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>() @@ -1490,25 +1519,39 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { return callbackCaptor.value!! } - private fun TestScope.getTelephonyCallbacks(): List<TelephonyCallback> { + protected fun TestScope.getSubscriptionCallbacks(): + List<SubscriptionManager.OnSubscriptionsChangedListener> { + runCurrent() + val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>() + verify(subscriptionManager, atLeast(0)) + .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture()) + return callbackCaptor.allValues + } + + fun TestScope.getTelephonyCallbacks(): List<TelephonyCallback> { runCurrent() val callbackCaptor = argumentCaptor<TelephonyCallback>() - verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) + verify(telephonyManager, atLeast(0)) + .registerTelephonyCallback(any(), callbackCaptor.capture()) return callbackCaptor.allValues } - private inline fun <reified T> TestScope.getTelephonyCallbackForType(): T { - val cbs = this.getTelephonyCallbacks().filterIsInstance<T>() + inline fun <reified T> TestScope.getTelephonyCallbackForType(): T { + val cbs = getTelephonyCallbacksForType<T>() assertThat(cbs.size).isEqualTo(1) return cbs[0] } + inline fun <reified T> TestScope.getTelephonyCallbacksForType(): List<T> { + return getTelephonyCallbacks().filterIsInstance<T>() + } + companion object { // Subscription 1 - private const val SUB_1_ID = 1 + const val SUB_1_ID = 1 private const val SUB_1_NAME = "Carrier $SUB_1_ID" private val GROUP_1 = ParcelUuid(UUID.randomUUID()) - private val SUB_1 = + val SUB_1 = mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) whenever(it.groupUuid).thenReturn(GROUP_1) @@ -1524,10 +1567,10 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { ) // Subscription 2 - private const val SUB_2_ID = 2 + const val SUB_2_ID = 2 private const val SUB_2_NAME = "Carrier $SUB_2_ID" private val GROUP_2 = ParcelUuid(UUID.randomUUID()) - private val SUB_2 = + val SUB_2 = mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) whenever(it.groupUuid).thenReturn(GROUP_2) @@ -1552,6 +1595,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { whenever(it.subscriptionId).thenReturn(SUB_3_ID_GROUPED) whenever(it.groupUuid).thenReturn(GROUP_ID_3_4) whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET) + whenever(it.carrierName).thenReturn("") } // Subscription 4 @@ -1561,17 +1605,18 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { whenever(it.subscriptionId).thenReturn(SUB_4_ID_GROUPED) whenever(it.groupUuid).thenReturn(GROUP_ID_3_4) whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET) + whenever(it.carrierName).thenReturn("") } // Subs 3 and 4 are considered to be in the same group ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ private const val NET_ID = 123 - private val NETWORK = mock<Network>().apply { whenever(getNetId()).thenReturn(NET_ID) } + val NETWORK = mock<Network>().apply { whenever(getNetId()).thenReturn(NET_ID) } // Carrier merged subscription - private const val SUB_CM_ID = 5 + const val SUB_CM_ID = 5 private const val SUB_CM_NAME = "Carrier $SUB_CM_ID" - private val SUB_CM = + val SUB_CM = mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_CM_ID) whenever(it.carrierName).thenReturn(SUB_CM_NAME) @@ -1590,7 +1635,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { whenever(this.isCarrierMerged).thenReturn(true) whenever(this.subscriptionId).thenReturn(SUB_CM_ID) } - private val WIFI_NETWORK_CAPS_CM = + val WIFI_NETWORK_CAPS_CM = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) whenever(it.transportInfo).thenReturn(WIFI_INFO_CM) @@ -1602,7 +1647,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { whenever(this.isPrimary).thenReturn(true) whenever(this.isCarrierMerged).thenReturn(false) } - private val WIFI_NETWORK_CAPS_ACTIVE = + val WIFI_NETWORK_CAPS_ACTIVE = mock<NetworkCapabilities>().also { whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) whenever(it.transportInfo).thenReturn(WIFI_INFO_ACTIVE) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java index 3247a1ab6eb0..8ff8fe6cc3d6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java @@ -44,6 +44,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.R; import com.android.internal.view.RotationPolicy; +import com.android.settingslib.devicestate.AndroidSecureSettings; import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; @@ -117,7 +118,8 @@ public class DeviceStateRotationLockSettingControllerTest extends SysuiTestCase ArgumentCaptor.forClass(DeviceStateManager.DeviceStateCallback.class); mContentResolver = mContext.getContentResolver(); - mSettingsManager = DeviceStateRotationLockSettingsManager.getInstance(mContext); + mSettingsManager = new DeviceStateRotationLockSettingsManager(mContext, + new AndroidSecureSettings(mContentResolver)); mDeviceStateRotationLockSettingController = new DeviceStateRotationLockSettingController( mFakeRotationPolicy, diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt index 54df9e99baa5..bb6ba46f1a0b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt @@ -25,13 +25,12 @@ import android.view.WindowManager import android.view.accessibility.AccessibilityManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.internal.logging.InstanceId import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.systemui.res.R import com.android.systemui.SysuiTestCase import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager -import com.android.systemui.res.R import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener import com.android.systemui.util.concurrency.DelayableExecutor @@ -78,7 +77,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { @Mock private lateinit var dumpManager: DumpManager @Mock - private lateinit var windowManager: ViewCaptureAwareWindowManager + private lateinit var windowManager: WindowManager @Mock private lateinit var powerManager: PowerManager @@ -1143,7 +1142,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { inner class TestController( context: Context, logger: TemporaryViewLogger<ViewInfo>, - viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, + windowManager: WindowManager, @Main mainExecutor: DelayableExecutor, accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, @@ -1155,7 +1154,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { ) : TemporaryViewDisplayController<ViewInfo, TemporaryViewLogger<ViewInfo>>( context, logger, - viewCaptureAwareWindowManager, + windowManager, mainExecutor, accessibilityManager, configurationController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt index 4260b6558950..664f2df62782 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt @@ -30,8 +30,6 @@ import android.widget.TextView import androidx.core.animation.doOnCancel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.app.viewcapture.ViewCapture -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.internal.logging.InstanceId import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.SysuiTestCase @@ -86,7 +84,6 @@ class ChipbarCoordinatorTest : SysuiTestCase() { @Mock private lateinit var viewUtil: ViewUtil @Mock private lateinit var vibratorHelper: VibratorHelper @Mock private lateinit var swipeGestureHandler: SwipeChipbarAwayGestureHandler - @Mock private lateinit var lazyViewCapture: Lazy<ViewCapture> private lateinit var chipbarAnimator: TestChipbarAnimator private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder private lateinit var fakeWakeLock: WakeLockFake @@ -115,8 +112,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { ChipbarCoordinator( context, logger, - ViewCaptureAwareWindowManager(windowManager, lazyViewCapture, - isViewCaptureEnabled = false), + windowManager, fakeExecutor, accessibilityManager, configurationController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/WindowManagerProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/utils/WindowManagerProviderImplTest.kt new file mode 100644 index 000000000000..7b52237f0a01 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/utils/WindowManagerProviderImplTest.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.utils + +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.view.WindowManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.utils.windowmanager.WindowManagerProviderImpl +import com.google.common.truth.Truth.assertThat +import org.junit.runner.RunWith +import org.junit.Test + +@RunWith(AndroidJUnit4::class) +@SmallTest +class WindowManagerProviderImplTest : SysuiTestCase() { + + private val windowManagerProvider = WindowManagerProviderImpl() + private val windowManagerFromSystemService = mContext.getSystemService(WindowManager::class.java) + + @Test + @EnableFlags(Flags.FLAG_ENABLE_VIEW_CAPTURE_TRACING) + fun viewCaptureTracingEnabled_verifyWMInstanceDoesNotMatchContextOne() { + val windowManagerFromProvider = windowManagerProvider.getWindowManager(mContext) + assertThat(windowManagerFromProvider).isNotEqualTo(windowManagerFromSystemService) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_VIEW_CAPTURE_TRACING) + fun viewCaptureTracingDisabled_verifyWMInstanceMatchesContextOne() { + mContext.addMockSystemService(WindowManager::class.java, windowManagerFromSystemService) + + val windowManagerFromProvider = windowManagerProvider.getWindowManager(mContext) + assertThat(windowManagerFromProvider).isEqualTo(windowManagerFromSystemService) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 0d7ce5353cd4..f89571f2db2d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -101,8 +101,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; -import com.android.app.viewcapture.ViewCapture; -import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.colorextraction.ColorExtractor; import com.android.internal.logging.UiEventLogger; import com.android.internal.protolog.ProtoLog; @@ -358,8 +356,6 @@ public class BubblesTest extends SysuiTestCase { @Mock private Display mDefaultDisplay; @Mock - private Lazy<ViewCapture> mLazyViewCapture; - @Mock private SyncTransactionQueue mSyncQueue; private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); @@ -429,8 +425,7 @@ public class BubblesTest extends SysuiTestCase { mNotificationShadeWindowController = new NotificationShadeWindowControllerImpl( mContext, new FakeWindowRootViewComponent.Factory(mNotificationShadeWindowView), - new ViewCaptureAwareWindowManager(mWindowManager, mLazyViewCapture, - /* isViewCaptureEnabled= */ false), + mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController, 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/communal/posturing/data/repository/FakePosturingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/posturing/data/repository/FakePosturingRepository.kt index 8a597a61ee78..47b1bf52d8e2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/posturing/data/repository/FakePosturingRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/posturing/data/repository/FakePosturingRepository.kt @@ -16,18 +16,18 @@ package com.android.systemui.communal.posturing.data.repository -import com.android.systemui.communal.posturing.shared.model.PosturedState -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import com.android.systemui.communal.posturing.data.model.PositionState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow class FakePosturingRepository : PosturingRepository { - private val _postured = MutableStateFlow<PosturedState>(PosturedState.Unknown) + private val _postured = MutableSharedFlow<PositionState>() - override val posturedState: StateFlow<PosturedState> = _postured.asStateFlow() + override val positionState: Flow<PositionState> = _postured.asSharedFlow() - fun setPosturedState(state: PosturedState) { - _postured.value = state + suspend fun emitPositionState(state: PositionState) { + _postured.emit(state) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorKosmos.kt index 53c9c6440c69..792346ebce59 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractorKosmos.kt @@ -18,6 +18,27 @@ package com.android.systemui.communal.posturing.domain.interactor import com.android.systemui.communal.posturing.data.repository.posturingRepository import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.advanceTimeBy +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.util.sensors.asyncSensorManager +import com.android.systemui.util.time.systemClock val Kosmos.posturingInteractor by - Kosmos.Fixture<PosturingInteractor> { PosturingInteractor(repository = posturingRepository) } + Kosmos.Fixture<PosturingInteractor> { + PosturingInteractor( + repository = posturingRepository, + asyncSensorManager = asyncSensorManager, + applicationScope = applicationCoroutineScope, + bgDispatcher = testDispatcher, + logBuffer = logcatLogBuffer("PosturingInteractor"), + clock = systemClock, + ) + } + +fun Kosmos.advanceTimeBySlidingWindowAndRun() { + advanceTimeBy(PosturingInteractor.SLIDING_WINDOW_DURATION) + runCurrent() +} 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/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt index 044332981bf8..ae28022e096e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt @@ -25,6 +25,7 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.kosmos.Kosmos.Fixture import kotlin.coroutines.CoroutineContext +import kotlin.time.Duration import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -32,6 +33,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import org.mockito.kotlin.verify @@ -72,6 +74,8 @@ fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) = let { kosmos -> fun Kosmos.runCurrent() = testScope.runCurrent() +fun Kosmos.advanceTimeBy(duration: Duration) = testScope.advanceTimeBy(duration) + fun <T> Kosmos.collectLastValue(flow: Flow<T>) = testScope.collectLastValue(flow) fun <T> Kosmos.collectValues(flow: Flow<T>): FlowValue<List<T>> = testScope.collectValues(flow) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelKosmos.kt index 2f3d3c3e0489..ba1c598a79d7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.media.controls.ui.viewmodel import android.content.applicationContext +import com.android.internal.logging.InstanceId import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher @@ -31,5 +32,9 @@ val Kosmos.mediaControlViewModel by backgroundExecutor = fakeExecutor, interactor = mediaControlInteractor, logger = mediaUiEventLogger, + instanceId = InstanceId.fakeInstanceId(-1), + onAdded = {}, + onRemoved = {}, + onUpdated = {}, ) } 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/statusbar/data/repository/PrivacyDotWindowControllerStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/PrivacyDotWindowControllerStoreKosmos.kt index aae32cfaafa6..f83fcb32aafe 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/PrivacyDotWindowControllerStoreKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/PrivacyDotWindowControllerStoreKosmos.kt @@ -16,8 +16,6 @@ package com.android.systemui.statusbar.data.repository -import android.view.WindowManager -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.systemui.display.data.repository.displayRepository import com.android.systemui.display.data.repository.displayWindowPropertiesRepository import com.android.systemui.kosmos.Kosmos @@ -35,14 +33,6 @@ val Kosmos.privacyDotWindowControllerStoreImpl by windowControllerFactory = { _, _, _, _ -> mock() }, displayWindowPropertiesRepository = displayWindowPropertiesRepository, privacyDotViewControllerStore = privacyDotViewControllerStore, - viewCaptureAwareWindowManagerFactory = - object : ViewCaptureAwareWindowManager.Factory { - override fun create( - windowManager: WindowManager - ): ViewCaptureAwareWindowManager { - return mock() - } - }, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/events/PrivacyDotWindowControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/events/PrivacyDotWindowControllerKosmos.kt index c73838708a7a..da4027e46783 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/events/PrivacyDotWindowControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/events/PrivacyDotWindowControllerKosmos.kt @@ -17,8 +17,8 @@ package com.android.systemui.statusbar.events import android.content.testableContext +import android.view.fakeWindowManager import android.view.layoutInflater -import com.android.app.viewcapture.realCaptureAwareWindowManager import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.decor.privacyDotDecorProviderFactory import com.android.systemui.kosmos.Kosmos @@ -28,7 +28,7 @@ var Kosmos.privacyDotWindowController by PrivacyDotWindowController( testableContext.displayId, privacyDotViewController, - realCaptureAwareWindowManager, + fakeWindowManager, layoutInflater, fakeExecutor, privacyDotDecorProviderFactory, 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 165f85d2cbba..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 @@ -49,7 +49,7 @@ fun activeNotificationModel( contentIntent: PendingIntent? = null, bucket: Int = BUCKET_UNKNOWN, callType: CallType = CallType.None, - promotedContent: PromotedNotificationContentModel? = null, + promotedContent: PromotedNotificationContentModels? = null, ) = ActiveNotificationModel( key = key, 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/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/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/window/FakeStatusBarWindowControllerFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt index 3a19547f0713..f20fb3bf1779 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt @@ -17,14 +17,14 @@ package com.android.systemui.statusbar.window import android.content.Context -import com.android.app.viewcapture.ViewCaptureAwareWindowManager +import android.view.WindowManager import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider class FakeStatusBarWindowControllerFactory : StatusBarWindowController.Factory { override fun create( context: Context, - viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, + windowManager: WindowManager, statusBarConfigurationController: StatusBarConfigurationController, contentInsetsProvider: StatusBarContentInsetsProvider, ) = FakeStatusBarWindowController() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt index f595aef41e2d..b0214769362f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt @@ -17,8 +17,8 @@ package com.android.systemui.statusbar.window import android.content.testableContext +import android.view.fakeWindowManager import android.view.windowManagerService -import com.android.app.viewcapture.realCaptureAwareWindowManager import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.fragments.fragmentService import com.android.systemui.kosmos.Kosmos @@ -33,7 +33,7 @@ val Kosmos.statusBarWindowControllerImpl by StatusBarWindowControllerImpl( testableContext, statusBarWindowViewInflater, - realCaptureAwareWindowManager, + fakeWindowManager, statusBarConfigurationController, windowManagerService, statusBarContentInsetsProvider, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStoreKosmos.kt index 4941ceb7991d..0f9310376b2a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStoreKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStoreKosmos.kt @@ -16,9 +16,6 @@ package com.android.systemui.statusbar.window -import android.view.WindowManager -import com.android.app.viewcapture.ViewCaptureAwareWindowManager -import com.android.app.viewcapture.realCaptureAwareWindowManager import com.android.systemui.display.data.repository.displayRepository import com.android.systemui.display.data.repository.displayWindowPropertiesRepository import com.android.systemui.kosmos.Kosmos @@ -33,14 +30,6 @@ val Kosmos.multiDisplayStatusBarWindowControllerStore by backgroundApplicationScope = applicationCoroutineScope, controllerFactory = { _, _, _, _ -> mock() }, displayWindowPropertiesRepository = displayWindowPropertiesRepository, - viewCaptureAwareWindowManagerFactory = - object : ViewCaptureAwareWindowManager.Factory { - override fun create( - windowManager: WindowManager - ): ViewCaptureAwareWindowManager { - return realCaptureAwareWindowManager - } - }, statusBarConfigurationControllerStore = statusBarConfigurationControllerStore, statusBarContentInsetsProviderStore = statusBarContentInsetsProviderStore, displayRepository = displayRepository, 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/utils/windowmanager/FakeWindowManagerProvider.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/windowmanager/FakeWindowManagerProvider.kt new file mode 100644 index 000000000000..5c8eae3183c7 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/windowmanager/FakeWindowManagerProvider.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.utils.windowmanager + +import android.content.Context +import android.view.WindowManager + +/** Fake implementation of [WindowManagerProvider], to be used in tests only. */ +class FakeWindowManagerProvider(private val windowManager: WindowManager) : WindowManagerProvider { + + override fun getWindowManager(context: Context): WindowManager { + return windowManager + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/utils/src/com/android/app/viewcapture/ViewCaptureAwareWindowManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCsdWarningInteractorKosmos.kt index 021c7bbb44cd..fc7fc0f4e5b4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/app/viewcapture/ViewCaptureAwareWindowManagerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCsdWarningInteractorKosmos.kt @@ -14,23 +14,13 @@ * limitations under the License. */ -package com.android.app.viewcapture +package com.android.systemui.volume.dialog.domain.interactor -import android.view.fakeWindowManager import com.android.systemui.kosmos.Kosmos -import org.mockito.kotlin.mock +import com.android.systemui.volume.dialog.shared.model.CsdWarningConfigModel -val Kosmos.mockViewCaptureAwareWindowManager by - Kosmos.Fixture { mock<ViewCaptureAwareWindowManager>() } +val Kosmos.volumeDialogCsdWarningInteractor: VolumeDialogCsdWarningInteractor by + Kosmos.Fixture { VolumeDialogCsdWarningInteractor(volumeDialogStateInteractor) } -val Kosmos.realCaptureAwareWindowManager by - Kosmos.Fixture { - ViewCaptureAwareWindowManager( - fakeWindowManager, - lazyViewCapture = lazy { mock<ViewCapture>() }, - isViewCaptureEnabled = false, - ) - } - -var Kosmos.viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager by - Kosmos.Fixture { mockViewCaptureAwareWindowManager } +val Kosmos.csdWarningConfigModel: CsdWarningConfigModel by + Kosmos.Fixture { CsdWarningConfigModel(emptyList()) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModelKosmos.kt new file mode 100644 index 000000000000..5a58599eaade --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModelKosmos.kt @@ -0,0 +1,41 @@ +/* + * 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.volume.dialog.ui.viewmodel + +import com.android.internal.logging.uiEventLogger +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.backgroundScope +import com.android.systemui.volume.dialog.domain.interactor.csdWarningConfigModel +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogCsdWarningInteractor +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogSafetyWarningInteractor +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor +import com.android.systemui.volume.dialog.shared.volumeDialogLogger +import com.android.systemui.volume.dialog.volumeDialog + +val Kosmos.volumeDialogPluginViewModel: VolumeDialogPluginViewModel by + Kosmos.Fixture { + VolumeDialogPluginViewModel( + backgroundScope, + volumeDialogVisibilityInteractor, + volumeDialogSafetyWarningInteractor, + volumeDialogCsdWarningInteractor, + { volumeDialog }, + volumeDialogLogger, + csdWarningConfigModel, + uiEventLogger, + ) + } diff --git a/packages/SystemUI/utils/Android.bp b/packages/SystemUI/utils/Android.bp index 1efb11b436ff..8b63c07b270f 100644 --- a/packages/SystemUI/utils/Android.bp +++ b/packages/SystemUI/utils/Android.bp @@ -26,6 +26,8 @@ java_library { "src/**/*.kt", ], static_libs: [ + "//frameworks/libs/systemui:view_capture", + "com_android_systemui_flags_lib", "kotlin-stdlib", "kotlinx_coroutines", ], diff --git a/packages/SystemUI/utils/src/com/android/systemui/utils/windowmanager/WindowManagerProvider.kt b/packages/SystemUI/utils/src/com/android/systemui/utils/windowmanager/WindowManagerProvider.kt new file mode 100644 index 000000000000..4e6eacbc8808 --- /dev/null +++ b/packages/SystemUI/utils/src/com/android/systemui/utils/windowmanager/WindowManagerProvider.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.utils.windowmanager + +import android.content.Context +import android.view.WindowManager + +/** + * Provider for [WindowManager] in SystemUI. + * + * Use this class over [WindowManagerUtils] in cases where + * a [WindowManager] is needed for a context created inside the class. [WindowManagerUtils] should + * only be used in a class where the [WindowManager] is needed for a custom context inside the + * class, and the class is not part of the dagger graph. Example usage: + * ```kotlin + * class Sample { + * private final WindowManager mWindowManager; + * + * @Inject + * public Sample(WindowManagerProvider windowManagerProvider) { + * Context context = getCustomContext(); + * mWindowManager = windowManagerProvider.getWindowManager(context); + * } + * // use mWindowManager + * } + * + * class SampleTest { + * + * @Mock + * WindowManager mWindowManager; + * + * FakeWindowManagerProvider fakeProvider = new FakeWindowManagerProvider(mWindowManager); + * + * // define the behaviour of mWindowManager to get required WindowManager instance in tests. + * } + * ``` + */ +interface WindowManagerProvider { + + /** Method to return the required [WindowManager]. */ + fun getWindowManager(context: Context): WindowManager +} diff --git a/packages/SystemUI/utils/src/com/android/systemui/utils/windowmanager/WindowManagerProviderImpl.kt b/packages/SystemUI/utils/src/com/android/systemui/utils/windowmanager/WindowManagerProviderImpl.kt new file mode 100644 index 000000000000..5e965ed47403 --- /dev/null +++ b/packages/SystemUI/utils/src/com/android/systemui/utils/windowmanager/WindowManagerProviderImpl.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.utils.windowmanager + +import android.content.Context +import android.view.WindowManager + +/** Implementation of [WindowManagerProvider]. */ +class WindowManagerProviderImpl : WindowManagerProvider { + + override fun getWindowManager(context: Context): WindowManager { + return WindowManagerUtils.getWindowManager(context) + } +} diff --git a/packages/SystemUI/utils/src/com/android/systemui/utils/windowmanager/WindowManagerUtils.kt b/packages/SystemUI/utils/src/com/android/systemui/utils/windowmanager/WindowManagerUtils.kt new file mode 100644 index 000000000000..643e93422294 --- /dev/null +++ b/packages/SystemUI/utils/src/com/android/systemui/utils/windowmanager/WindowManagerUtils.kt @@ -0,0 +1,49 @@ +/* + * 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.utils.windowmanager + +import android.content.Context +import android.view.WindowManager +import com.android.app.viewcapture.ViewCaptureAwareWindowManagerFactory +import com.android.systemui.Flags.enableViewCaptureTracing + +/** + * Provides [WindowManager] in SystemUI. Use [WindowManagerProvider] unless [WindowManager] instance + * needs to be created in a class that is not part of the dagger dependency graph. + */ +object WindowManagerUtils { + + /** Method to return the required [WindowManager]. */ + @JvmStatic + fun getWindowManager(context: Context): WindowManager { + return if (!enableViewCaptureTracing()) { + context.getSystemService(WindowManager::class.java) + } else { + /** + * We use this token to supply windowContextToken to [WindowManager] for + * [WindowContext]. + */ + val windowContextToken = context.windowContextToken + + ViewCaptureAwareWindowManagerFactory.getInstance( + context, + parent = null, + windowContextToken, + ) + } + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index 47aa8f5736bf..aae8879e9199 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -1942,14 +1942,9 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ } public void notifyGesture(AccessibilityGestureEvent gestureEvent) { - if (android.view.accessibility.Flags.copyEventsForGestureDetection()) { - // We will use this event async, so copy it because it contains MotionEvents. - mInvocationHandler.obtainMessage(InvocationHandler.MSG_ON_GESTURE, - gestureEvent.copyForAsync()).sendToTarget(); - } else { - mInvocationHandler.obtainMessage(InvocationHandler.MSG_ON_GESTURE, - gestureEvent).sendToTarget(); - } + // We will use this event async, so copy it because it contains MotionEvents. + mInvocationHandler.obtainMessage(InvocationHandler.MSG_ON_GESTURE, + gestureEvent.copyForAsync()).sendToTarget(); } public void notifySystemActionsChangedLocked() { @@ -2426,9 +2421,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ case MSG_ON_GESTURE: { if (message.obj instanceof AccessibilityGestureEvent gesture) { notifyGestureInternal(gesture); - if (android.view.accessibility.Flags.copyEventsForGestureDetection()) { - gesture.recycle(); - } + gesture.recycle(); } } break; case MSG_CLEAR_ACCESSIBILITY_CACHE: { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 69ea12d86d47..39c1fa73b7ce 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -529,14 +529,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } 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( @@ -652,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) { @@ -701,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; @@ -718,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) @@ -730,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. @@ -740,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( @@ -763,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 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/accessibility/java/com/android/server/accessibility/gestures/GestureMatcher.java b/services/accessibility/java/com/android/server/accessibility/gestures/GestureMatcher.java index 3668eefe293d..62b6b85afa58 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/GestureMatcher.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/GestureMatcher.java @@ -336,13 +336,8 @@ public abstract class GestureMatcher { // Recycle the old event first if necessary, to handle duplicate calls to post. recycleEvent(); mTargetState = state; - if (android.view.accessibility.Flags.copyEventsForGestureDetection()) { - mEvent = event.copy(); - mRawEvent = rawEvent.copy(); - } else { - mEvent = event; - mRawEvent = rawEvent; - } + mEvent = event.copy(); + mRawEvent = rawEvent.copy(); mPolicyFlags = policyFlags; mHandler.postDelayed(this, delay); if (DEBUG) { @@ -379,15 +374,13 @@ public abstract class GestureMatcher { } private void recycleEvent() { - if (android.view.accessibility.Flags.copyEventsForGestureDetection()) { - if (mEvent == null || mRawEvent == null) { - return; - } - mEvent.recycle(); - mRawEvent.recycle(); - mEvent = null; - mRawEvent = null; + if (mEvent == null || mRawEvent == null) { + return; } + mEvent.recycle(); + mRawEvent.recycle(); + mEvent = null; + mRawEvent = null; } } diff --git a/services/core/Android.bp b/services/core/Android.bp index decac40d20f8..cf85dd957b3f 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -258,7 +258,6 @@ java_library_static { "dreams_flags_lib", "aconfig_new_storage_flags_lib", "powerstats_flags_lib", - "locksettings_flags_lib", "MmdProperties", "mmd_flags_lib", "profiling_flags_lib", 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/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 6b3661a2a004..797ee002affb 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 @@ -4188,7 +4190,13 @@ public class AudioService extends IAudioService.Stub Log.d(TAG, "adjustStreamVolume postSetHearingAidVolumeIndex index=" + newIndex + " stream=" + streamType); } - mDeviceBroker.postSetHearingAidVolumeIndex(newIndex, streamType); + int haIndex; + final VolumeStreamState vss = getVssForStreamOrDefault(streamType); + synchronized (mVolumeStateLock) { + haIndex = (int) (vss.getMinIndex() + (newIndex - vss.getMinIndex()) + / vss.getIndexStepFactor()); + } + mDeviceBroker.postSetHearingAidVolumeIndex(haIndex, streamType); } } @@ -4452,6 +4460,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 }) @@ -5126,7 +5155,13 @@ public class AudioService extends IAudioService.Stub mDeviceBroker.postSetLeAudioVolumeIndex(index * 10, getVssForStreamOrDefault(streamType).getMaxIndex(), streamType); } else if (device == AudioSystem.DEVICE_OUT_HEARING_AID) { - mDeviceBroker.postSetHearingAidVolumeIndex(index * 10, streamType); + int haIndex = index * 10; + final VolumeStreamState vss = getVssForStreamOrDefault(streamType); + synchronized (mVolumeStateLock) { + haIndex = (int) (vss.getMinIndex() + + (haIndex - vss.getMinIndex()) / vss.getIndexStepFactor()); + } + mDeviceBroker.postSetHearingAidVolumeIndex(haIndex, streamType); } else if (AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)) { mDeviceBroker.postSetAvrcpAbsoluteVolumeIndex(index); } else { @@ -5257,7 +5292,13 @@ public class AudioService extends IAudioService.Stub && streamType == getBluetoothContextualVolumeStream()) { Log.i(TAG, "setStreamVolume postSetHearingAidVolumeIndex index=" + index + " stream=" + streamType); - mDeviceBroker.postSetHearingAidVolumeIndex(index, streamType); + int haIndex; + final VolumeStreamState vss = getVssForStreamOrDefault(streamType); + synchronized (mVolumeStateLock) { + haIndex = (int) (vss.getMinIndex() + + (index - vss.getMinIndex()) / vss.getIndexStepFactor()); + } + mDeviceBroker.postSetHearingAidVolumeIndex(haIndex, streamType); } synchronized (mHdmiClientLock) { 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..6af55300d0b3 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -25,6 +25,7 @@ import static android.view.KeyEvent.KEYCODE_UNKNOWN; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static com.android.hardware.input.Flags.enableCustomizableInputGestures; +import static com.android.hardware.input.Flags.fixSearchModifierFallbacks; import static com.android.hardware.input.Flags.keyEventActivityDetection; import static com.android.hardware.input.Flags.touchpadVisualizer; import static com.android.hardware.input.Flags.useKeyGestureEventHandler; @@ -750,7 +751,8 @@ public class InputManagerService extends IInputManager.Stub * @return True if the lookup was successful, false otherwise. */ @Override // Binder call - public boolean hasKeys(int deviceId, int sourceMask, int[] keyCodes, boolean[] keyExists) { + public boolean hasKeys(int deviceId, int sourceMask, @NonNull int[] keyCodes, + @NonNull boolean[] keyExists) { Objects.requireNonNull(keyCodes, "keyCodes must not be null"); Objects.requireNonNull(keyExists, "keyExists must not be null"); if (keyExists.length < keyCodes.length) { @@ -791,7 +793,7 @@ public class InputManagerService extends IInputManager.Stub * @deprecated Use {@link #transferTouchGesture(IBinder, IBinder)} */ @Deprecated - public boolean transferTouch(IBinder destChannelToken, int displayId) { + public boolean transferTouch(@NonNull IBinder destChannelToken, int displayId) { // TODO(b/162194035): Replace this with a SPY window Objects.requireNonNull(destChannelToken, "destChannelToken must not be null"); return mNative.transferTouch(destChannelToken, displayId); @@ -803,7 +805,7 @@ public class InputManagerService extends IInputManager.Stub * @param displayId Target display id. * @return The input channel. */ - public InputChannel monitorInput(String inputChannelName, int displayId) { + public InputChannel monitorInput(@NonNull String inputChannelName, int displayId) { Objects.requireNonNull(inputChannelName, "inputChannelName not be null"); if (displayId < Display.DEFAULT_DISPLAY) { @@ -835,7 +837,7 @@ public class InputManagerService extends IInputManager.Stub return outInputChannel; } - private void removeSpyWindowGestureMonitor(IBinder inputChannelToken) { + private void removeSpyWindowGestureMonitor(@NonNull IBinder inputChannelToken) { final GestureMonitorSpyWindow monitor; synchronized (mInputMonitors) { monitor = mInputMonitors.remove(inputChannelToken); @@ -854,8 +856,8 @@ public class InputManagerService extends IInputManager.Stub * @return The input channel. */ @Override // Binder call - public InputMonitor monitorGestureInput(IBinder monitorToken, @NonNull String requestedName, - int displayId) { + public InputMonitor monitorGestureInput(@NonNull IBinder monitorToken, + @NonNull String requestedName, int displayId) { if (!checkCallingPermission(android.Manifest.permission.MONITOR_INPUT, "monitorGestureInput()")) { throw new SecurityException("Requires MONITOR_INPUT permission"); @@ -902,7 +904,7 @@ public class InputManagerService extends IInputManager.Stub * Removes an input channel. * @param connectionToken The input channel to unregister. */ - public void removeInputChannel(IBinder connectionToken) { + public void removeInputChannel(@NonNull IBinder connectionToken) { Objects.requireNonNull(connectionToken, "connectionToken must not be null"); mNative.removeInputChannel(connectionToken); } @@ -977,12 +979,12 @@ public class InputManagerService extends IInputManager.Stub } @Override // Binder call - public boolean injectInputEvent(InputEvent event, int mode) { + public boolean injectInputEvent(@NonNull InputEvent event, int mode) { return injectInputEventToTarget(event, mode, Process.INVALID_UID); } @Override // Binder call - public boolean injectInputEventToTarget(InputEvent event, int mode, int targetUid) { + public boolean injectInputEventToTarget(@NonNull InputEvent event, int mode, int targetUid) { if (!checkCallingPermission(android.Manifest.permission.INJECT_EVENTS, "injectInputEvent()", true /*checkInstrumentationSource*/)) { throw new SecurityException( @@ -1032,7 +1034,7 @@ public class InputManagerService extends IInputManager.Stub } @Override // Binder call - public VerifiedInputEvent verifyInputEvent(InputEvent event) { + public VerifiedInputEvent verifyInputEvent(@NonNull InputEvent event) { Objects.requireNonNull(event, "event must not be null"); return mNative.verifyInputEvent(event); } @@ -1106,7 +1108,8 @@ public class InputManagerService extends IInputManager.Stub } @Override // Binder call - public void registerInputDevicesChangedListener(IInputDevicesChangedListener listener) { + public void registerInputDevicesChangedListener( + @NonNull IInputDevicesChangedListener listener) { Objects.requireNonNull(listener, "listener must not be null"); synchronized (mInputDevicesLock) { @@ -1176,7 +1179,7 @@ public class InputManagerService extends IInputManager.Stub } @Override // Binder call & native callback - public TouchCalibration getTouchCalibrationForInputDevice(String inputDeviceDescriptor, + public TouchCalibration getTouchCalibrationForInputDevice(@NonNull String inputDeviceDescriptor, int surfaceRotation) { Objects.requireNonNull(inputDeviceDescriptor, "inputDeviceDescriptor must not be null"); @@ -1186,8 +1189,8 @@ public class InputManagerService extends IInputManager.Stub } @Override // Binder call - public void setTouchCalibrationForInputDevice(String inputDeviceDescriptor, int surfaceRotation, - TouchCalibration calibration) { + public void setTouchCalibrationForInputDevice(@NonNull String inputDeviceDescriptor, + int surfaceRotation, @NonNull TouchCalibration calibration) { if (!checkCallingPermission(android.Manifest.permission.SET_INPUT_CALIBRATION, "setTouchCalibrationForInputDevice()")) { throw new SecurityException("Requires SET_INPUT_CALIBRATION permission"); @@ -1225,7 +1228,7 @@ public class InputManagerService extends IInputManager.Stub } @Override // Binder call - public void registerTabletModeChangedListener(ITabletModeChangedListener listener) { + public void registerTabletModeChangedListener(@NonNull ITabletModeChangedListener listener) { if (!checkCallingPermission(android.Manifest.permission.TABLET_MODE, "registerTabletModeChangedListener()")) { throw new SecurityException("Requires TABLET_MODE_LISTENER permission"); @@ -1341,7 +1344,7 @@ public class InputManagerService extends IInputManager.Stub } @Override - public void requestPointerCapture(IBinder inputChannelToken, boolean enabled) { + public void requestPointerCapture(@NonNull IBinder inputChannelToken, boolean enabled) { Objects.requireNonNull(inputChannelToken, "inputChannelToken must not be null"); mNative.requestPointerCapture(inputChannelToken, enabled); @@ -1664,7 +1667,8 @@ public class InputManagerService extends IInputManager.Stub } @Override // Binder call - public boolean registerVibratorStateListener(int deviceId, IVibratorStateListener listener) { + public boolean registerVibratorStateListener(int deviceId, + @NonNull IVibratorStateListener listener) { Objects.requireNonNull(listener, "listener must not be null"); RemoteCallbackList<IVibratorStateListener> listeners; @@ -1717,8 +1721,8 @@ public class InputManagerService extends IInputManager.Stub // Binder call @Override - public boolean setPointerIcon(PointerIcon icon, int displayId, int deviceId, int pointerId, - IBinder inputToken) { + public boolean setPointerIcon(@NonNull PointerIcon icon, int displayId, int deviceId, + int pointerId, IBinder inputToken) { Objects.requireNonNull(icon); return mNative.setPointerIcon(icon, displayId, deviceId, pointerId, inputToken); } @@ -1896,7 +1900,7 @@ public class InputManagerService extends IInputManager.Stub } @Override // Binder call - public boolean registerSensorListener(IInputSensorEventListener listener) { + public boolean registerSensorListener(@NonNull IInputSensorEventListener listener) { if (DEBUG) { Slog.d(TAG, "registerSensorListener: listener=" + listener + " callingPid=" + Binder.getCallingPid()); @@ -1927,7 +1931,7 @@ public class InputManagerService extends IInputManager.Stub } @Override // Binder call - public void unregisterSensorListener(IInputSensorEventListener listener) { + public void unregisterSensorListener(@NonNull IInputSensorEventListener listener) { if (DEBUG) { Slog.d(TAG, "unregisterSensorListener: listener=" + listener + " callingPid=" + Binder.getCallingPid()); @@ -2016,7 +2020,8 @@ public class InputManagerService extends IInputManager.Stub /** * Set specified light state with for a specific input device. */ - private void setLightStateInternal(int deviceId, Light light, LightState lightState) { + private void setLightStateInternal(int deviceId, @NonNull Light light, + @NonNull LightState lightState) { Objects.requireNonNull(light, "light does not exist"); if (DEBUG) { Slog.d(TAG, "setLightStateInternal device " + deviceId + " light " + light @@ -2079,7 +2084,7 @@ public class InputManagerService extends IInputManager.Stub } @Override - public void openLightSession(int deviceId, String opPkg, IBinder token) { + public void openLightSession(int deviceId, String opPkg, @NonNull IBinder token) { Objects.requireNonNull(token); synchronized (mLightLock) { Preconditions.checkState(mLightSessions.get(token) == null, "already registered"); @@ -2098,7 +2103,7 @@ public class InputManagerService extends IInputManager.Stub } @Override - public void closeLightSession(int deviceId, IBinder token) { + public void closeLightSession(int deviceId, @NonNull IBinder token) { Objects.requireNonNull(token); synchronized (mLightLock) { LightSession lightSession = mLightSessions.get(token); @@ -2128,13 +2133,15 @@ public class InputManagerService extends IInputManager.Stub } @Override - public void registerBatteryListener(int deviceId, IInputDeviceBatteryListener listener) { + public void registerBatteryListener(int deviceId, + @NonNull IInputDeviceBatteryListener listener) { Objects.requireNonNull(listener); mBatteryController.registerBatteryListener(deviceId, listener, Binder.getCallingPid()); } @Override - public void unregisterBatteryListener(int deviceId, IInputDeviceBatteryListener listener) { + public void unregisterBatteryListener(int deviceId, + @NonNull IInputDeviceBatteryListener listener) { Objects.requireNonNull(listener); mBatteryController.unregisterBatteryListener(deviceId, listener, Binder.getCallingPid()); } @@ -2155,7 +2162,7 @@ public class InputManagerService extends IInputManager.Stub @EnforcePermission(Manifest.permission.MONITOR_INPUT) @Override - public void pilferPointers(IBinder inputChannelToken) { + public void pilferPointers(@NonNull IBinder inputChannelToken) { super.pilferPointers_enforcePermission(); Objects.requireNonNull(inputChannelToken); @@ -2164,7 +2171,7 @@ public class InputManagerService extends IInputManager.Stub @Override @EnforcePermission(Manifest.permission.MONITOR_KEYBOARD_BACKLIGHT) - public void registerKeyboardBacklightListener(IKeyboardBacklightListener listener) { + public void registerKeyboardBacklightListener(@NonNull IKeyboardBacklightListener listener) { super.registerKeyboardBacklightListener_enforcePermission(); Objects.requireNonNull(listener); mKeyboardBacklightController.registerKeyboardBacklightListener(listener, @@ -2173,7 +2180,7 @@ public class InputManagerService extends IInputManager.Stub @Override @EnforcePermission(Manifest.permission.MONITOR_KEYBOARD_BACKLIGHT) - public void unregisterKeyboardBacklightListener(IKeyboardBacklightListener listener) { + public void unregisterKeyboardBacklightListener(@NonNull IKeyboardBacklightListener listener) { super.unregisterKeyboardBacklightListener_enforcePermission(); Objects.requireNonNull(listener); mKeyboardBacklightController.unregisterKeyboardBacklightListener(listener, @@ -2653,6 +2660,8 @@ public class InputManagerService extends IInputManager.Stub @SuppressWarnings("unused") @VisibleForTesting long interceptKeyBeforeDispatching(IBinder focus, KeyEvent event, int policyFlags) { + final long keyNotConsumedGoFallback = -2; + final long keyConsumed = -1; final long keyNotConsumed = 0; long value = keyNotConsumed; // TODO(b/358569822) Remove below once we have nicer API for listening to shortcuts @@ -2667,6 +2676,16 @@ public class InputManagerService extends IInputManager.Stub value = mWindowManagerCallbacks.interceptKeyBeforeDispatching(focus, event, policyFlags); } + if (fixSearchModifierFallbacks() && value == keyNotConsumed && event.isMetaPressed()) { + // If the key has not been consumed and includes the meta key, do not send the event + // to the app and attempt to generate a fallback. + final KeyCharacterMap kcm = event.getKeyCharacterMap(); + final KeyCharacterMap.FallbackAction fallbackAction = + kcm.getFallbackAction(event.getKeyCode(), event.getMetaState()); + if (fallbackAction != null) { + return keyNotConsumedGoFallback; + } + } return value; } @@ -2751,18 +2770,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 +2795,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 +2816,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 +2823,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 +2830,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 +2838,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 +3167,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 @@ -3306,9 +3329,10 @@ public class InputManagerService extends IInputManager.Stub * @param token the window token that's about to receive this event * @param event the key event that's being dispatched * @param policyFlags the policy flags - * @return negative value if the key should be skipped (not sent to the app). 0 if the key - * should proceed getting dispatched to the app. positive value to indicate the additional - * time delay, in nanoseconds, to wait before sending this key to the app. + * @return -1 if the key should be skipped (not sent to the app). -2 if the key should not + * be sent to the app, but it should still generate a fallback. + * 0 if the key should proceed getting dispatched to the app. positive value to indicate the + * additional time delay, in nanoseconds, to wait before sending this key to the app. */ long interceptKeyBeforeDispatching(IBinder token, KeyEvent event, int policyFlags); @@ -3425,7 +3449,7 @@ public class InputManagerService extends IInputManager.Stub } @Override - public void sendInputEvent(InputEvent event, int policyFlags) { + public void sendInputEvent(@NonNull InputEvent event, int policyFlags) { if (!checkCallingPermission(android.Manifest.permission.INJECT_EVENTS, "sendInputEvent()")) { throw new SecurityException( @@ -3447,9 +3471,9 @@ public class InputManagerService extends IInputManager.Stub * Interface for the system to handle request from InputMonitors. */ private final class InputMonitorHost extends IInputMonitorHost.Stub { - private final IBinder mInputChannelToken; + private final @NonNull IBinder mInputChannelToken; - InputMonitorHost(IBinder inputChannelToken) { + InputMonitorHost(@NonNull IBinder inputChannelToken) { mInputChannelToken = inputChannelToken; } 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/locksettings/Android.bp b/services/core/java/com/android/server/locksettings/Android.bp deleted file mode 100644 index 53f1ac668e49..000000000000 --- a/services/core/java/com/android/server/locksettings/Android.bp +++ /dev/null @@ -1,11 +0,0 @@ -aconfig_declarations { - name: "locksettings_flags", - package: "com.android.server.locksettings", - container: "system", - srcs: ["*.aconfig"], -} - -java_aconfig_library { - name: "locksettings_flags_lib", - aconfig_declarations: "locksettings_flags", -} diff --git a/services/core/java/com/android/server/locksettings/RebootEscrowManager.java b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java index 820c0efcc1cf..4b704d01c3b9 100644 --- a/services/core/java/com/android/server/locksettings/RebootEscrowManager.java +++ b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java @@ -428,15 +428,11 @@ class RebootEscrowManager { /** Wrapper function to set error code serialized through handler, */ private void setLoadEscrowDataErrorCode(@RebootEscrowErrorCode int value, Handler handler) { - if (Flags.waitForInternetRor()) { - mInjector.post( - handler, - () -> { - mLoadEscrowDataErrorCode = value; - }); - } else { - mLoadEscrowDataErrorCode = value; - } + mInjector.post( + handler, + () -> { + mLoadEscrowDataErrorCode = value; + }); } /** Wrapper function to compare and set error code serialized through handler. */ @@ -511,23 +507,17 @@ class RebootEscrowManager { mWakeLock.acquire(mInjector.getWakeLockTimeoutMillis()); } - if (Flags.waitForInternetRor()) { - // Timeout to stop retrying same as the wake lock timeout. - mInjector.postDelayed( - retryHandler, - () -> { - mRebootEscrowTimedOut = true; - }, - mInjector.getLoadEscrowTimeoutMillis()); - - mInjector.post( - retryHandler, - () -> loadRebootEscrowDataOnInternet(retryHandler, users, rebootEscrowUsers)); - return; - } + // Timeout to stop retrying same as the wake lock timeout. + mInjector.postDelayed( + retryHandler, + () -> { + mRebootEscrowTimedOut = true; + }, + mInjector.getLoadEscrowTimeoutMillis()); - mInjector.post(retryHandler, () -> loadRebootEscrowDataWithRetry( - retryHandler, 0, users, rebootEscrowUsers)); + mInjector.post( + retryHandler, + () -> loadRebootEscrowDataOnInternet(retryHandler, users, rebootEscrowUsers)); } void scheduleLoadRebootEscrowDataOrFail( @@ -548,27 +538,13 @@ class RebootEscrowManager { return; } - if (Flags.waitForInternetRor()) { - if (mRebootEscrowTimedOut) { - Slog.w(TAG, "Failed to load reboot escrow data within timeout"); - compareAndSetLoadEscrowDataErrorCode( - ERROR_NONE, ERROR_TIMEOUT_EXHAUSTED, retryHandler); - } else { - Slog.w( - TAG, - "Failed to load reboot escrow data after " + attemptNumber + " attempts"); - compareAndSetLoadEscrowDataErrorCode( - ERROR_NONE, ERROR_RETRY_COUNT_EXHAUSTED, retryHandler); - } - onGetRebootEscrowKeyFailed(users, attemptNumber, retryHandler); - return; - } - - Slog.w(TAG, "Failed to load reboot escrow data after " + attemptNumber + " attempts"); - if (mInjector.serverBasedResumeOnReboot() && !mInjector.isNetworkConnected()) { - mLoadEscrowDataErrorCode = ERROR_NO_NETWORK; + if (mRebootEscrowTimedOut) { + Slog.w(TAG, "Failed to load reboot escrow data within timeout"); + compareAndSetLoadEscrowDataErrorCode(ERROR_NONE, ERROR_TIMEOUT_EXHAUSTED, retryHandler); } else { - mLoadEscrowDataErrorCode = ERROR_RETRY_COUNT_EXHAUSTED; + Slog.w(TAG, "Failed to load reboot escrow data after " + attemptNumber + " attempts"); + compareAndSetLoadEscrowDataErrorCode( + ERROR_NONE, ERROR_RETRY_COUNT_EXHAUSTED, retryHandler); } onGetRebootEscrowKeyFailed(users, attemptNumber, retryHandler); } diff --git a/services/core/java/com/android/server/locksettings/flags.aconfig b/services/core/java/com/android/server/locksettings/flags.aconfig deleted file mode 100644 index 6818de91c98e..000000000000 --- a/services/core/java/com/android/server/locksettings/flags.aconfig +++ /dev/null @@ -1,9 +0,0 @@ -package: "com.android.server.locksettings" -container: "system" - -flag { - name: "wait_for_internet_ror" - namespace: "sudo" - description: "Feature flag to wait for internet connectivity before calling resume on reboot server." - bug: "231660348" -}
\ No newline at end of file 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/projection/TEST_MAPPING b/services/core/java/com/android/server/media/projection/TEST_MAPPING index b33097c50002..7061044aaeee 100644 --- a/services/core/java/com/android/server/media/projection/TEST_MAPPING +++ b/services/core/java/com/android/server/media/projection/TEST_MAPPING @@ -2,6 +2,34 @@ "presubmit": [ { "name": "MediaProjectionTests" + }, + { + "name": "CtsMediaProjectionTestCases" + }, + { + "name": "CtsMediaProjectionSDK33TestCases" + }, + { + "name": "CtsMediaProjectionSDK34TestCases" + }, + { + "name": "CtsMediaAudioTestCases", + "options": [ + { + "include-filter": "android.media.audio.cts.RemoteSubmixTest" + }, + { + "include-filter": "android.media.audio.cts.AudioPlaybackCaptureTest" + } + ] + }, + { + "name": "CtsStatsdAtomHostTestCases", + "options": [ + { + "include-filter": "android.cts.statsdatom.media.projection.MediaProjectionAtomsTests" + } + ] } ] } 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/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java index 8d787fe99e3a..651111e431c3 100644 --- a/services/core/java/com/android/server/om/OverlayManagerService.java +++ b/services/core/java/com/android/server/om/OverlayManagerService.java @@ -985,8 +985,8 @@ public final class OverlayManagerService extends SystemService { final String pkgName = request.overlay.getPackageName(); if (callingUid != Process.ROOT_UID && !ArrayUtils.contains( mPackageManager.getPackagesForUid(callingUid), pkgName)) { - throw new IllegalArgumentException("UID " + callingUid + " does own package" - + "name " + pkgName); + throw new IllegalArgumentException("UID " + callingUid + " does not own " + + "packageName " + pkgName); } } else { // Enforce actor requirements for enabling, disabling, and reordering overlays. diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index b441e9dd561d..7e5ada54c953 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -1994,6 +1994,9 @@ public class ComputerEngine implements Computer { if (Process.isSdkSandboxUid(uid)) { uid = getBaseSdkSandboxUid(); } + if(isKnownIsolatedComputeApp(uid)) { + uid = getIsolatedOwner(uid); + } final int appId = UserHandle.getAppId(uid); return getPackagesForUidInternalBody(callingUid, userId, appId, isCallerInstantApp); } 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/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java index f88681dbcaeb..e98176b0e82b 100644 --- a/services/core/java/com/android/server/pm/UserManagerInternal.java +++ b/services/core/java/com/android/server/pm/UserManagerInternal.java @@ -18,6 +18,7 @@ package com.android.server.pm; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SpecialUsers.CanBeNULL; import android.annotation.UserIdInt; import android.content.Context; import android.content.pm.LauncherUserInfo; @@ -620,11 +621,17 @@ public abstract class UserManagerInternal { * Returns the user id of the communal profile, or {@link android.os.UserHandle#USER_NULL} * if there is no such user. */ - public abstract @UserIdInt int getCommunalProfileId(); + public abstract @CanBeNULL @UserIdInt int getCommunalProfileId(); /** - * Checks whether to show a notification for sounds (e.g., alarms, timers, etc.) from - * background users. + * Returns the user id of the supervising profile, or {@link android.os.UserHandle#USER_NULL} if + * there is no such user. + */ + public abstract @CanBeNULL @UserIdInt int getSupervisingProfileId(); + + /** + * Checks whether to show a notification for sounds (e.g., alarms, timers, etc.) from background + * users. */ public static boolean shouldShowNotificationForBackgroundUserSounds() { return Flags.addUiForSoundsFromBackgroundUsers() && Resources.getSystem().getBoolean( diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 33a7e7476cf6..053b4aae90dd 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1530,6 +1530,20 @@ public class UserManagerService extends IUserManager.Stub { return UserHandle.USER_NULL; } + /** Returns the currently-designated supervising profile, or USER_NULL if not present. */ + private @CanBeNULL @UserIdInt int getSupervisingProfileId() { + synchronized (mUsersLock) { + final int userSize = mUsers.size(); + for (int i = 0; i < userSize; i++) { + final UserInfo user = mUsers.valueAt(i).info; + if (user.isSupervisingProfile() && !mRemovingUserIds.get(user.id)) { + return user.id; + } + } + } + return UserHandle.USER_NULL; + } + public @NonNull List<UserInfo> getUsers(boolean excludeDying) { return getUsers(/*excludePartial= */ true, excludeDying, /* excludePreCreated= */ true); @@ -8348,10 +8362,14 @@ public class UserManagerService extends IUserManager.Stub { } @Override - public @UserIdInt int getCommunalProfileId() { + public @CanBeNULL @UserIdInt int getCommunalProfileId() { return getCommunalProfileIdUnchecked(); } + @Override + public @CanBeNULL @UserIdInt int getSupervisingProfileId() { + return UserManagerService.this.getSupervisingProfileId(); + } } // class LocalService diff --git a/services/core/java/com/android/server/pm/permission/AccessCheckDelegate.java b/services/core/java/com/android/server/pm/permission/AccessCheckDelegate.java index e989d6875d15..4c14e96e6c98 100644 --- a/services/core/java/com/android/server/pm/permission/AccessCheckDelegate.java +++ b/services/core/java/com/android/server/pm/permission/AccessCheckDelegate.java @@ -36,6 +36,7 @@ import android.text.TextUtils; import android.util.ArrayMap; import android.util.SparseArray; +import com.android.internal.annotations.GuardedBy; import com.android.internal.util.ArrayUtils; import com.android.internal.util.function.DodecFunction; import com.android.internal.util.function.HexConsumer; @@ -141,150 +142,191 @@ public interface AccessCheckDelegate extends CheckPermissionDelegate, CheckOpsDe class AccessCheckDelegateImpl implements AccessCheckDelegate { public static final String SHELL_PKG = "com.android.shell"; + + private final Object mLock = new Object(); + + @GuardedBy("mLock") private int mDelegateAndOwnerUid = INVALID_UID; @Nullable + @GuardedBy("mLock") private String mDelegatePackage; @Nullable + @GuardedBy("mLock") private String[] mDelegatePermissions; + @GuardedBy("mLock") boolean mDelegateAllPermissions; @Nullable + @GuardedBy("mLock") private SparseArray<ArrayMap<String, Integer>> mOverridePermissionStates; @Override public void setShellPermissionDelegate(int uid, @NonNull String packageName, @Nullable String[] permissions) { - mDelegateAndOwnerUid = uid; - mDelegatePackage = packageName; - mDelegatePermissions = permissions; - mDelegateAllPermissions = permissions == null; + synchronized (mLock) { + mDelegateAndOwnerUid = uid; + mDelegatePackage = packageName; + mDelegatePermissions = permissions; + mDelegateAllPermissions = permissions == null; + } PackageManager.invalidatePackageInfoCache(); } @Override public void removeShellPermissionDelegate() { - mDelegatePackage = null; - mDelegatePermissions = null; - mDelegateAllPermissions = false; + synchronized (mLock) { + mDelegatePackage = null; + mDelegatePermissions = null; + mDelegateAllPermissions = false; + } PackageManager.invalidatePackageInfoCache(); } @Override public void addOverridePermissionState(int ownerUid, int uid, @NonNull String permission, int state) { - if (mOverridePermissionStates == null) { - mDelegateAndOwnerUid = ownerUid; - mOverridePermissionStates = new SparseArray<>(); - } + synchronized (mLock) { + if (mOverridePermissionStates == null) { + mDelegateAndOwnerUid = ownerUid; + mOverridePermissionStates = new SparseArray<>(); + } - int uidIdx = mOverridePermissionStates.indexOfKey(uid); - ArrayMap<String, Integer> perUidOverrides; - if (uidIdx < 0) { - perUidOverrides = new ArrayMap<>(); - mOverridePermissionStates.put(uid, perUidOverrides); - } else { - perUidOverrides = mOverridePermissionStates.valueAt(uidIdx); - } + int uidIdx = mOverridePermissionStates.indexOfKey(uid); + ArrayMap<String, Integer> perUidOverrides; + if (uidIdx < 0) { + perUidOverrides = new ArrayMap<>(); + mOverridePermissionStates.put(uid, perUidOverrides); + } else { + perUidOverrides = mOverridePermissionStates.valueAt(uidIdx); + } - perUidOverrides.put(permission, state); + perUidOverrides.put(permission, state); + } PackageManager.invalidatePackageInfoCache(); } @Override public void removeOverridePermissionState(int uid, @NonNull String permission) { - if (mOverridePermissionStates == null) { - return; - } + synchronized (mLock) { + if (mOverridePermissionStates == null) { + return; + } - ArrayMap<String, Integer> perUidOverrides = mOverridePermissionStates.get(uid); + ArrayMap<String, Integer> perUidOverrides = mOverridePermissionStates.get(uid); - if (perUidOverrides == null) { - return; - } + if (perUidOverrides == null) { + return; + } - perUidOverrides.remove(permission); - PackageManager.invalidatePackageInfoCache(); + perUidOverrides.remove(permission); - if (perUidOverrides.isEmpty()) { - mOverridePermissionStates.remove(uid); - } - if (mOverridePermissionStates.size() == 0) { - mOverridePermissionStates = null; + if (perUidOverrides.isEmpty()) { + mOverridePermissionStates.remove(uid); + } + if (mOverridePermissionStates.size() == 0) { + mOverridePermissionStates = null; + } } + PackageManager.invalidatePackageInfoCache(); } @Override public void clearOverridePermissionStates(int uid) { - if (mOverridePermissionStates == null) { - return; - } + synchronized (mLock) { + if (mOverridePermissionStates == null) { + return; + } - mOverridePermissionStates.remove(uid); - PackageManager.invalidatePackageInfoCache(); + mOverridePermissionStates.remove(uid); - if (mOverridePermissionStates.size() == 0) { - mOverridePermissionStates = null; + if (mOverridePermissionStates.size() == 0) { + mOverridePermissionStates = null; + } } + PackageManager.invalidatePackageInfoCache(); } @Override public void clearAllOverridePermissionStates() { - mOverridePermissionStates = null; + synchronized (mLock) { + mOverridePermissionStates = null; + } PackageManager.invalidatePackageInfoCache(); } @Override public List<String> getDelegatedPermissionNames() { - return mDelegatePermissions == null ? null : List.of(mDelegatePermissions); + synchronized (mLock) { + return mDelegatePermissions == null ? null : List.of(mDelegatePermissions); + } } @Override public boolean hasShellPermissionDelegate() { - return mDelegateAllPermissions || mDelegatePermissions != null; + synchronized (mLock) { + return mDelegateAllPermissions || mDelegatePermissions != null; + } } @Override public boolean isDelegatePackage(int uid, @NonNull String packageName) { - return mDelegateAndOwnerUid == uid && TextUtils.equals(mDelegatePackage, packageName); + synchronized (mLock) { + return mDelegateAndOwnerUid == uid + && TextUtils.equals(mDelegatePackage, packageName); + } } @Override public boolean hasOverriddenPermissions() { - return mOverridePermissionStates != null; + synchronized (mLock) { + return mOverridePermissionStates != null; + } } @Override public boolean isDelegateAndOwnerUid(int uid) { - return uid == mDelegateAndOwnerUid; + synchronized (mLock) { + return uid == mDelegateAndOwnerUid; + } } @Override public boolean hasDelegateOrOverrides() { - return hasShellPermissionDelegate() || hasOverriddenPermissions(); + synchronized (mLock) { + return hasShellPermissionDelegate() || hasOverriddenPermissions(); + } } @Override public int checkPermission(@NonNull String packageName, @NonNull String permissionName, @NonNull String persistentDeviceId, @UserIdInt int userId, @NonNull QuadFunction<String, String, String, Integer, Integer> superImpl) { - if (TextUtils.equals(mDelegatePackage, packageName) && !SHELL_PKG.equals(packageName)) { - if (isDelegatePermission(permissionName)) { - final long identity = Binder.clearCallingIdentity(); - try { - return checkPermission(SHELL_PKG, permissionName, persistentDeviceId, - userId, superImpl); - } finally { - Binder.restoreCallingIdentity(identity); + boolean useShellDelegate; + + synchronized (mLock) { + useShellDelegate = !SHELL_PKG.equals(packageName) + && TextUtils.equals(mDelegatePackage, packageName) + && isDelegatePermission(permissionName); + + if (!useShellDelegate && mOverridePermissionStates != null) { + int uid = LocalServices.getService(PackageManagerInternal.class) + .getPackageUid(packageName, 0, userId); + if (uid >= 0) { + Map<String, Integer> permissionGrants = mOverridePermissionStates.get(uid); + if (permissionGrants != null + && permissionGrants.containsKey(permissionName)) { + return permissionGrants.get(permissionName); + } } } } - if (mOverridePermissionStates != null) { - int uid = LocalServices.getService(PackageManagerInternal.class) - .getPackageUid(packageName, 0, userId); - if (uid >= 0) { - Map<String, Integer> permissionGrants = mOverridePermissionStates.get(uid); - if (permissionGrants != null && permissionGrants.containsKey(permissionName)) { - return permissionGrants.get(permissionName); - } + + if (useShellDelegate) { + final long identity = Binder.clearCallingIdentity(); + try { + return checkPermission(SHELL_PKG, permissionName, persistentDeviceId, userId, + superImpl); + } finally { + Binder.restoreCallingIdentity(identity); } } return superImpl.apply(packageName, permissionName, persistentDeviceId, userId); @@ -294,21 +336,27 @@ public interface AccessCheckDelegate extends CheckPermissionDelegate, CheckOpsDe public int checkUidPermission(int uid, @NonNull String permissionName, @NonNull String persistentDeviceId, @NonNull TriFunction<Integer, String, String, Integer> superImpl) { - if (uid == mDelegateAndOwnerUid && uid != Process.SHELL_UID) { - if (isDelegatePermission(permissionName)) { - final long identity = Binder.clearCallingIdentity(); - try { - return checkUidPermission(Process.SHELL_UID, permissionName, - persistentDeviceId, superImpl); - } finally { - Binder.restoreCallingIdentity(identity); + boolean useShellDelegate; + + synchronized (mLock) { + useShellDelegate = uid != Process.SHELL_UID && uid == mDelegateAndOwnerUid + && isDelegatePermission(permissionName); + + if (!useShellDelegate && mOverridePermissionStates != null) { + Map<String, Integer> permissionGrants = mOverridePermissionStates.get(uid); + if (permissionGrants != null && permissionGrants.containsKey(permissionName)) { + return permissionGrants.get(permissionName); } } } - if (mOverridePermissionStates != null) { - Map<String, Integer> permissionGrants = mOverridePermissionStates.get(uid); - if (permissionGrants != null && permissionGrants.containsKey(permissionName)) { - return permissionGrants.get(permissionName); + + if (useShellDelegate) { + final long identity = Binder.clearCallingIdentity(); + try { + return checkUidPermission(Process.SHELL_UID, permissionName, persistentDeviceId, + superImpl); + } finally { + Binder.restoreCallingIdentity(identity); } } return superImpl.apply(uid, permissionName, persistentDeviceId); @@ -319,7 +367,13 @@ public interface AccessCheckDelegate extends CheckPermissionDelegate, CheckOpsDe @Nullable String attributionTag, int virtualDeviceId, boolean raw, @NonNull HexFunction<Integer, Integer, String, String, Integer, Boolean, Integer> superImpl) { - if (uid == mDelegateAndOwnerUid && isDelegateOp(code)) { + boolean useShellDelegate; + + synchronized (mLock) { + useShellDelegate = uid == mDelegateAndOwnerUid && isDelegateOp(code); + } + + if (useShellDelegate) { final int shellUid = UserHandle.getUid(UserHandle.getUserId(uid), Process.SHELL_UID); final long identity = Binder.clearCallingIdentity(); @@ -335,7 +389,13 @@ public interface AccessCheckDelegate extends CheckPermissionDelegate, CheckOpsDe @Override public int checkAudioOperation(int code, int usage, int uid, @Nullable String packageName, @NonNull QuadFunction<Integer, Integer, Integer, String, Integer> superImpl) { - if (uid == mDelegateAndOwnerUid && isDelegateOp(code)) { + boolean useShellDelegate; + + synchronized (mLock) { + useShellDelegate = uid == mDelegateAndOwnerUid && isDelegateOp(code); + } + + if (useShellDelegate) { final int shellUid = UserHandle.getUid(UserHandle.getUserId(uid), Process.SHELL_UID); final long identity = Binder.clearCallingIdentity(); @@ -354,7 +414,13 @@ public interface AccessCheckDelegate extends CheckPermissionDelegate, CheckOpsDe @Nullable String message, boolean shouldCollectMessage, int notedCount, @NonNull NonaFunction<Integer, Integer, String, String, Integer, Boolean, String, Boolean, Integer, SyncNotedAppOp> superImpl) { - if (uid == mDelegateAndOwnerUid && isDelegateOp(code)) { + boolean useShellDelegate; + + synchronized (mLock) { + useShellDelegate = uid == mDelegateAndOwnerUid && isDelegateOp(code); + } + + if (useShellDelegate) { final int shellUid = UserHandle.getUid(UserHandle.getUserId(uid), Process.SHELL_UID); final long identity = Binder.clearCallingIdentity(); @@ -375,21 +441,29 @@ public interface AccessCheckDelegate extends CheckPermissionDelegate, CheckOpsDe @Nullable String message, boolean shouldCollectMessage, boolean skiProxyOperation, @NonNull HexFunction<Integer, AttributionSource, Boolean, String, Boolean, Boolean, SyncNotedAppOp> superImpl) { - if (!isDelegateOp(code)) { - return superImpl.apply(code, attributionSource, shouldCollectAsyncNotedOp, - message, shouldCollectMessage, skiProxyOperation); + boolean isDelegateOp; + int delegateAndOwnerUid; + + synchronized (mLock) { + isDelegateOp = isDelegateOp(code); + delegateAndOwnerUid = mDelegateAndOwnerUid; + } + + if (!isDelegateOp) { + return superImpl.apply(code, attributionSource, shouldCollectAsyncNotedOp, message, + shouldCollectMessage, skiProxyOperation); } final int shellUid = UserHandle.getUid( UserHandle.getUserId(attributionSource.getUid()), Process.SHELL_UID); AttributionSource next = attributionSource.getNext(); - if (next != null && next.getUid() == mDelegateAndOwnerUid) { + if (next != null && next.getUid() == delegateAndOwnerUid) { next = new AttributionSource(shellUid, Process.INVALID_PID, SHELL_PKG, next.getAttributionTag(), next.getToken(), /*renouncedPermissions*/ null, next.getDeviceId(), next.getNext()); attributionSource = new AttributionSource(attributionSource, next); } - if (attributionSource.getUid() == mDelegateAndOwnerUid) { + if (attributionSource.getUid() == delegateAndOwnerUid) { attributionSource = new AttributionSource(shellUid, Process.INVALID_PID, SHELL_PKG, attributionSource.getAttributionTag(), attributionSource.getToken(), /*renouncedPermissions*/ null, @@ -397,9 +471,8 @@ public interface AccessCheckDelegate extends CheckPermissionDelegate, CheckOpsDe } final long identity = Binder.clearCallingIdentity(); try { - return superImpl.apply(code, attributionSource, - shouldCollectAsyncNotedOp, message, shouldCollectMessage, - skiProxyOperation); + return superImpl.apply(code, attributionSource, shouldCollectAsyncNotedOp, message, + shouldCollectMessage, skiProxyOperation); } finally { Binder.restoreCallingIdentity(identity); } @@ -413,7 +486,13 @@ public interface AccessCheckDelegate extends CheckPermissionDelegate, CheckOpsDe @AttributionFlags int attributionFlags, int attributionChainId, @NonNull DodecFunction<IBinder, Integer, Integer, String, String, Integer, Boolean, Boolean, String, Boolean, Integer, Integer, SyncNotedAppOp> superImpl) { - if (uid == mDelegateAndOwnerUid && isDelegateOp(code)) { + boolean useShellDelegate; + + synchronized (mLock) { + useShellDelegate = uid == mDelegateAndOwnerUid && isDelegateOp(code); + } + + if (useShellDelegate) { final int shellUid = UserHandle.getUid(UserHandle.getUserId(uid), Process.SHELL_UID); final long identity = Binder.clearCallingIdentity(); @@ -440,7 +519,14 @@ public interface AccessCheckDelegate extends CheckPermissionDelegate, CheckOpsDe @NonNull UndecFunction<IBinder, Integer, AttributionSource, Boolean, Boolean, String, Boolean, Boolean, Integer, Integer, Integer, SyncNotedAppOp> superImpl) { - if (attributionSource.getUid() == mDelegateAndOwnerUid && isDelegateOp(code)) { + boolean useShellDelegate; + + synchronized (mLock) { + useShellDelegate = attributionSource.getUid() == mDelegateAndOwnerUid + && isDelegateOp(code); + } + + if (useShellDelegate) { final int shellUid = UserHandle.getUid(UserHandle.getUserId( attributionSource.getUid()), Process.SHELL_UID); final long identity = Binder.clearCallingIdentity(); @@ -467,7 +553,14 @@ public interface AccessCheckDelegate extends CheckPermissionDelegate, CheckOpsDe @NonNull AttributionSource attributionSource, boolean skipProxyOperation, @NonNull QuadFunction<IBinder, Integer, AttributionSource, Boolean, Void> superImpl) { - if (attributionSource.getUid() == mDelegateAndOwnerUid && isDelegateOp(code)) { + boolean useShellDelegate; + + synchronized (mLock) { + useShellDelegate = attributionSource.getUid() == mDelegateAndOwnerUid + && isDelegateOp(code); + } + + if (useShellDelegate) { final int shellUid = UserHandle.getUid(UserHandle.getUserId( attributionSource.getUid()), Process.SHELL_UID); final long identity = Binder.clearCallingIdentity(); @@ -490,7 +583,13 @@ public interface AccessCheckDelegate extends CheckPermissionDelegate, CheckOpsDe public void finishOperation(IBinder clientId, int code, int uid, String packageName, String attributionTag, int virtualDeviceId, @NonNull HexConsumer<IBinder, Integer, Integer, String, String, Integer> superImpl) { - if (uid == mDelegateAndOwnerUid && isDelegateOp(code)) { + boolean useShellDelegate; + + synchronized (mLock) { + useShellDelegate = uid == mDelegateAndOwnerUid && isDelegateOp(code); + } + + if (useShellDelegate) { final int shellUid = UserHandle.getUid(UserHandle.getUserId(uid), Process.SHELL_UID); final long identity = Binder.clearCallingIdentity(); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 38d458767015..e9f522d08328 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -86,6 +86,7 @@ import static android.view.contentprotection.flags.Flags.createAccessibilityOver import static com.android.hardware.input.Flags.enableNew25q2Keycodes; import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures; import static com.android.hardware.input.Flags.enableVoiceAccessKeyGestures; +import static com.android.hardware.input.Flags.fixSearchModifierFallbacks; import static com.android.hardware.input.Flags.inputManagerLifecycleSupport; import static com.android.hardware.input.Flags.keyboardA11yShortcutControl; import static com.android.hardware.input.Flags.modifierShortcutDump; @@ -115,7 +116,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 +268,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 +312,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 +1236,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 +1273,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 +1332,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 +1351,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 +2343,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { return ActivityManager.getService(); } + LockPatternUtils getLockPatternUtils() { + return new LockPatternUtils(mContext); + } + ButtonOverridePermissionChecker getButtonOverridePermissionChecker() { return new ButtonOverridePermissionChecker(); } @@ -2360,7 +2395,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(); @@ -4147,6 +4182,13 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } + if (fixSearchModifierFallbacks()) { + // Pass event as unhandled to give other services, e.g. InputManagerService, the + // opportunity to determine if the event can be modified, e.g. generating a fallback for + // meta/search events. + return false; + } + // Reserve all the META modifier combos for system behavior return (metaState & KeyEvent.META_META_ON) != 0; } @@ -4240,19 +4282,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 +4336,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 +4354,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 +4362,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 +4399,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 +4413,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 +4422,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 +4447,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 +4482,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 +4499,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 +4535,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 +4544,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/AppCompatCameraOverrides.java b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java index cb82f480d73b..d152a1dbe17d 100644 --- a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java @@ -35,6 +35,7 @@ import static com.android.server.wm.AppCompatUtils.isChangeEnabled; import android.annotation.NonNull; import android.annotation.Nullable; +import android.window.DesktopModeFlags; import com.android.server.wm.utils.OptPropFactory; import com.android.window.flags.Flags; @@ -177,7 +178,7 @@ class AppCompatCameraOverrides { * </ul> */ boolean shouldApplyFreeformTreatmentForCameraCompat() { - return Flags.enableCameraCompatForDesktopWindowing() + return DesktopModeFlags.ENABLE_CAMERA_COMPAT_SIMULATE_REQUESTED_ORIENTATION.isTrue() && (shouldEnableCameraCompatFreeformTreatmentForApp() || shouldEnableCameraCompatFreeformTreatmentForAllApps()); } diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java index 276c7d2cbaa0..e7a0803df916 100644 --- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java @@ -26,9 +26,9 @@ import android.app.CameraCompatTaskInfo; import android.content.pm.ActivityInfo.ScreenOrientation; import android.content.res.Configuration; import android.widget.Toast; +import android.window.DesktopModeFlags; import com.android.internal.annotations.VisibleForTesting; -import com.android.window.flags.Flags; /** * Encapsulate policy logic related to app compat display rotation. @@ -53,7 +53,7 @@ class AppCompatCameraPolicy { final boolean needsDisplayRotationCompatPolicy = wmService.mAppCompatConfiguration.isCameraCompatTreatmentEnabledAtBuildTime(); final boolean needsCameraCompatFreeformPolicy = - Flags.enableCameraCompatForDesktopWindowing() + DesktopModeFlags.ENABLE_CAMERA_COMPAT_SIMULATE_REQUESTED_ORIENTATION.isTrue() && DesktopModeHelper.canEnterDesktopMode(wmService.mContext); if (needsDisplayRotationCompatPolicy || needsCameraCompatFreeformPolicy) { mCameraStateMonitor = new CameraStateMonitor(displayContent, wmService.mH); 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/DeviceStateAutoRotateSettingIssueLogger.java b/services/core/java/com/android/server/wm/DeviceStateAutoRotateSettingIssueLogger.java index 937039d838e5..a7d03485058f 100644 --- a/services/core/java/com/android/server/wm/DeviceStateAutoRotateSettingIssueLogger.java +++ b/services/core/java/com/android/server/wm/DeviceStateAutoRotateSettingIssueLogger.java @@ -29,13 +29,13 @@ import java.util.function.LongSupplier; /** * Logs potential race conditions that lead to incorrect auto-rotate setting. * - * Before go/auto-rotate-refactor, there is a race condition that happen during device state + * <p>Before go/auto-rotate-refactor, there is a race condition that happen during device state * changes, as a result, incorrect auto-rotate setting are written for a device state in * DEVICE_STATE_ROTATION_LOCK. Realistically, users shouldn’t be able to change * DEVICE_STATE_ROTATION_LOCK while the device folds/unfolds. * - * This class monitors the time between a device state change and a subsequent change to the device - * state based auto-rotate setting. If the duration is less than a threshold + * <p>This class monitors the time between a device state change and a subsequent change to the + * device state based auto-rotate setting. If the duration is less than a threshold * (DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_THRESHOLD), a potential issue is logged. The logging of * the atom is not expected to occur often, realistically estimated once a month on few devices. * But the number could be bigger, as that's what this metric is set to reveal. @@ -72,23 +72,33 @@ public class DeviceStateAutoRotateSettingIssueLogger { } private void onStateChange() { - // Only move forward if both of the events have occurred already - if (mLastDeviceStateChangeTime != TIME_NOT_SET - && mLastDeviceStateAutoRotateSettingChangeTime != TIME_NOT_SET) { - final long duration = - mLastDeviceStateAutoRotateSettingChangeTime - mLastDeviceStateChangeTime; - boolean isDeviceStateChangeFirst = duration > 0; + // Only move forward if both of the events have occurred already. + if (mLastDeviceStateChangeTime == TIME_NOT_SET + || mLastDeviceStateAutoRotateSettingChangeTime == TIME_NOT_SET) { + return; + } + final long duration = + mLastDeviceStateAutoRotateSettingChangeTime - mLastDeviceStateChangeTime; + boolean isDeviceStateChangeFirst = duration > 0; - if (abs(duration) - < DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_THRESHOLD_MILLIS) { - FrameworkStatsLog.write( - FrameworkStatsLog.DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_REPORTED, - (int) abs(duration), - isDeviceStateChangeFirst); - } + if (abs(duration) + < DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_THRESHOLD_MILLIS) { + FrameworkStatsLog.write( + FrameworkStatsLog.DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_REPORTED, + (int) abs(duration), + isDeviceStateChangeFirst); + // This pair is logged, reset both timestamps. mLastDeviceStateAutoRotateSettingChangeTime = TIME_NOT_SET; mLastDeviceStateChangeTime = TIME_NOT_SET; + } else { + // This pair was not logged, reset the earlier timestamp. + if (isDeviceStateChangeFirst) { + mLastDeviceStateChangeTime = TIME_NOT_SET; + } else { + mLastDeviceStateAutoRotateSettingChangeTime = TIME_NOT_SET; + } } + } } 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/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 51c3da098020..02b53b0106b8 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -512,9 +512,14 @@ class TransitionController { return false; } + /** Returns {@code true} if the display contains a collecting transition. */ + boolean isCollectingTransitionOnDisplay(@NonNull DisplayContent dc) { + return mCollectingTransition != null && mCollectingTransition.isOnDisplay(dc); + } + /** Returns {@code true} if the display contains a running or pending transition. */ boolean isTransitionOnDisplay(@NonNull DisplayContent dc) { - if (mCollectingTransition != null && mCollectingTransition.isOnDisplay(dc)) { + if (isCollectingTransitionOnDisplay(dc)) { return true; } for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) { 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..3ccbc868377e 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 -> { @@ -1845,9 +1845,12 @@ public class WindowManagerService extends IWindowManager.Stub // Only a presentation window needs a transition because its visibility affets the // lifecycle of apps below (b/390481865). if (enablePresentationForConnectedDisplays() && win.isPresentation()) { - Transition transition = null; + final boolean wasTransitionOnDisplay = + win.mTransitionController.isCollectingTransitionOnDisplay(displayContent); + Transition newlyCreatedTransition = null; if (!win.mTransitionController.isCollecting()) { - transition = win.mTransitionController.createAndStartCollecting(TRANSIT_OPEN); + newlyCreatedTransition = + win.mTransitionController.createAndStartCollecting(TRANSIT_OPEN); } win.mTransitionController.collect(win.mToken); res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState, @@ -1856,9 +1859,14 @@ public class WindowManagerService extends IWindowManager.Stub // A presentation hides all activities behind on the same display. win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, /*notifyClients=*/ true); - win.mTransitionController.getCollectingTransition().setReady(win.mToken, true); - if (transition != null) { - win.mTransitionController.requestStartTransition(transition, null, + if (!wasTransitionOnDisplay && win.mTransitionController + .isCollectingTransitionOnDisplay(displayContent)) { + // Set the display ready only when the display gets added to the collecting + // transition in this operation. + win.mTransitionController.setReady(win.mToken); + } + if (newlyCreatedTransition != null) { + win.mTransitionController.requestStartTransition(newlyCreatedTransition, null, null /* remoteTransition */, null /* displayChange */); } } else { @@ -2602,6 +2610,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..d43aba0d218d 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -2365,9 +2365,12 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // Only a presentation window needs a transition because its visibility affets the // lifecycle of apps below (b/390481865). if (enablePresentationForConnectedDisplays() && isPresentation()) { - Transition transition = null; + final boolean wasTransitionOnDisplay = + mTransitionController.isCollectingTransitionOnDisplay(displayContent); + Transition newlyCreatedTransition = null; if (!mTransitionController.isCollecting()) { - transition = mTransitionController.createAndStartCollecting(TRANSIT_CLOSE); + newlyCreatedTransition = + mTransitionController.createAndStartCollecting(TRANSIT_CLOSE); } mTransitionController.collect(mToken); mAnimatingExit = true; @@ -2376,9 +2379,14 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // A presentation hides all activities behind on the same display. mDisplayContent.ensureActivitiesVisible(/*starting=*/ null, /*notifyClients=*/ true); - mTransitionController.getCollectingTransition().setReady(mToken, true); - if (transition != null) { - mTransitionController.requestStartTransition(transition, null, + if (!wasTransitionOnDisplay && mTransitionController + .isCollectingTransitionOnDisplay(displayContent)) { + // Set the display ready only when the display gets added to the collecting + // transition in this operation. + mTransitionController.setReady(mToken); + } + if (newlyCreatedTransition != null) { + mTransitionController.requestStartTransition(newlyCreatedTransition, null, null /* remoteTransition */, null /* displayChange */); } } else { @@ -4995,18 +5003,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/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index ec8794f8073f..017284cded8e 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -1978,6 +1978,11 @@ NativeInputManager::interceptKeyBeforeDispatching(const sp<IBinder>& token, return inputdispatcher::KeyEntry::InterceptKeyResult::SKIP; } + // -2 : Skip sending even to application and go directly to post processing e.g. fallbacks. + if (delayMillis == -2) { + return inputdispatcher::KeyEntry::InterceptKeyResult::FALLBACK; + } + return milliseconds_to_nanoseconds(delayMillis); } 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/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/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java index bb6339c79502..0b5a95b0e888 100644 --- a/services/supervision/java/com/android/server/supervision/SupervisionService.java +++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java @@ -27,6 +27,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.UserIdInt; +import android.app.KeyguardManager; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManagerInternal; import android.app.supervision.ISupervisionManager; @@ -147,9 +148,21 @@ public class SupervisionService extends ISupervisionManager.Stub { @Override @Nullable public Intent createConfirmSupervisionCredentialsIntent() { - // TODO(b/392961554): (1) Return null if supervision is not enabled. - // (2) check if PIN exists before return a valid intent. enforceAnyPermission(QUERY_USERS, MANAGE_USERS); + if (!isSupervisionEnabledForUser(mContext.getUserId())) { + return null; + } + // Verify the supervising user profile exists and has a secure credential set. + final int supervisingUserId = mInjector.getUserManagerInternal().getSupervisingProfileId(); + final long token = Binder.clearCallingIdentity(); + try { + if (supervisingUserId == UserHandle.USER_NULL + || !mInjector.getKeyguardManager().isDeviceSecure(supervisingUserId)) { + return null; + } + } finally { + Binder.restoreCallingIdentity(token); + } final Intent intent = new Intent(ACTION_CONFIRM_SUPERVISION_CREDENTIALS); // explicitly set the package for security intent.setPackage("com.android.settings"); @@ -277,6 +290,7 @@ public class SupervisionService extends ISupervisionManager.Stub { static class Injector { private final Context mContext; private DevicePolicyManagerInternal mDpmInternal; + private KeyguardManager mKeyguardManager; private PackageManager mPackageManager; private UserManagerInternal mUserManagerInternal; @@ -292,6 +306,13 @@ public class SupervisionService extends ISupervisionManager.Stub { return mDpmInternal; } + KeyguardManager getKeyguardManager() { + if (mKeyguardManager == null) { + mKeyguardManager = mContext.getSystemService(KeyguardManager.class); + } + return mKeyguardManager; + } + PackageManager getPackageManager() { if (mPackageManager == null) { mPackageManager = mContext.getPackageManager(); diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java index 554b5b4297f2..740424813c2a 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java @@ -98,9 +98,9 @@ public class UserDataPreparerTest { File systemDeDir = mUserDataPreparer.getDataSystemDeDirectory(TEST_USER_ID); systemDeDir.mkdirs(); mUserDataPreparer.prepareUserData(TEST_USER, StorageManager.FLAG_STORAGE_DE); - verify(mStorageManagerMock).prepareUserStorage(isNull(String.class), eq(TEST_USER_ID), + verify(mStorageManagerMock).prepareUserStorage(isNull(), eq(TEST_USER_ID), eq(StorageManager.FLAG_STORAGE_DE)); - verify(mInstaller).createUserData(isNull(String.class), eq(TEST_USER_ID), + verify(mInstaller).createUserData(isNull(), eq(TEST_USER_ID), eq(TEST_USER_SERIAL), eq(StorageManager.FLAG_STORAGE_DE)); int serialNumber = UserDataPreparer.getSerialNumber(userDeDir); assertEquals(TEST_USER_SERIAL, serialNumber); @@ -115,9 +115,9 @@ public class UserDataPreparerTest { File systemCeDir = mUserDataPreparer.getDataSystemCeDirectory(TEST_USER_ID); systemCeDir.mkdirs(); mUserDataPreparer.prepareUserData(TEST_USER, StorageManager.FLAG_STORAGE_CE); - verify(mStorageManagerMock).prepareUserStorage(isNull(String.class), eq(TEST_USER_ID), + verify(mStorageManagerMock).prepareUserStorage(isNull(), eq(TEST_USER_ID), eq(StorageManager.FLAG_STORAGE_CE)); - verify(mInstaller).createUserData(isNull(String.class), eq(TEST_USER_ID), + verify(mInstaller).createUserData(isNull(), eq(TEST_USER_ID), eq(TEST_USER_SERIAL), eq(StorageManager.FLAG_STORAGE_CE)); int serialNumber = UserDataPreparer.getSerialNumber(userCeDir); assertEquals(TEST_USER_SERIAL, serialNumber); @@ -129,10 +129,10 @@ public class UserDataPreparerTest { public void testPrepareUserData_forNewUser_destroysOnFailure() throws Exception { TEST_USER.lastLoggedInTime = 0; doThrow(new IllegalStateException("expected exception for test")).when(mStorageManagerMock) - .prepareUserStorage(isNull(String.class), eq(TEST_USER_ID), + .prepareUserStorage(isNull(), eq(TEST_USER_ID), eq(StorageManager.FLAG_STORAGE_CE)); mUserDataPreparer.prepareUserData(TEST_USER, StorageManager.FLAG_STORAGE_CE); - verify(mStorageManagerMock).destroyUserStorage(isNull(String.class), eq(TEST_USER_ID), + verify(mStorageManagerMock).destroyUserStorage(isNull(), eq(TEST_USER_ID), eq(StorageManager.FLAG_STORAGE_CE)); } @@ -140,10 +140,10 @@ public class UserDataPreparerTest { public void testPrepareUserData_forExistingUser_doesNotDestroyOnFailure() throws Exception { TEST_USER.lastLoggedInTime = System.currentTimeMillis(); doThrow(new IllegalStateException("expected exception for test")).when(mStorageManagerMock) - .prepareUserStorage(isNull(String.class), eq(TEST_USER_ID), + .prepareUserStorage(isNull(), eq(TEST_USER_ID), eq(StorageManager.FLAG_STORAGE_CE)); mUserDataPreparer.prepareUserData(TEST_USER, StorageManager.FLAG_STORAGE_CE); - verify(mStorageManagerMock, never()).destroyUserStorage(isNull(String.class), + verify(mStorageManagerMock, never()).destroyUserStorage(isNull(), eq(TEST_USER_ID), eq(StorageManager.FLAG_STORAGE_CE)); } @@ -171,9 +171,9 @@ public class UserDataPreparerTest { mUserDataPreparer.destroyUserData(TEST_USER_ID, StorageManager.FLAG_STORAGE_DE); - verify(mInstaller).destroyUserData(isNull(String.class), eq(TEST_USER_ID), + verify(mInstaller).destroyUserData(isNull(), eq(TEST_USER_ID), eq(StorageManager.FLAG_STORAGE_DE)); - verify(mStorageManagerMock).destroyUserStorage(isNull(String.class), eq(TEST_USER_ID), + verify(mStorageManagerMock).destroyUserStorage(isNull(), eq(TEST_USER_ID), eq(StorageManager.FLAG_STORAGE_DE)); // systemDir (normal path: /data/system/users/$userId) should have been deleted. @@ -195,9 +195,9 @@ public class UserDataPreparerTest { mUserDataPreparer.destroyUserData(TEST_USER_ID, StorageManager.FLAG_STORAGE_CE); - verify(mInstaller).destroyUserData(isNull(String.class), eq(TEST_USER_ID), + verify(mInstaller).destroyUserData(isNull(), eq(TEST_USER_ID), eq(StorageManager.FLAG_STORAGE_CE)); - verify(mStorageManagerMock).destroyUserStorage(isNull(String.class), eq(TEST_USER_ID), + verify(mStorageManagerMock).destroyUserStorage(isNull(), eq(TEST_USER_ID), eq(StorageManager.FLAG_STORAGE_CE)); // systemCeDir (normal path: /data/system_ce/$userId) should still exist but be empty, since @@ -225,7 +225,7 @@ public class UserDataPreparerTest { .reconcileUsers(StorageManager.UUID_PRIVATE_INTERNAL, Arrays.asList(u1, u2), Arrays.asList(dir1, dir2, dir3)); // Verify that user 3 data is removed - verify(mInstaller).destroyUserData(isNull(String.class), eq(3), + verify(mInstaller).destroyUserData(isNull(), eq(3), eq(StorageManager.FLAG_STORAGE_DE|StorageManager.FLAG_STORAGE_CE)); } 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/ApplicationStartInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java index 3289d70b89ac..fe4baeb80ee7 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java @@ -79,7 +79,8 @@ import java.util.ArrayList; public class ApplicationStartInfoTest { private static final String TAG = ApplicationStartInfoTest.class.getSimpleName(); - private static final ComponentName COMPONENT = new ComponentName("com.android.test", ".Foo"); + private static final ComponentName COMPONENT = + new ComponentName("com.android.test", "com.android.test.Foo"); private static final int APP_1_UID = 10123; private static final int APP_1_PID_1 = 12345; diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index d702cae248a9..64e6d323bdfd 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: [ @@ -92,7 +93,6 @@ android_test { "net_flags_lib", "CtsVirtualDeviceCommonLib", "com_android_server_accessibility_flags_lib", - "locksettings_flags_lib", ] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), { "true": ["service-crashrecovery-pre-jarjar"], default: [], @@ -141,6 +141,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 e0bc3e76f31d..457d8a96fea4 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -87,6 +87,9 @@ import android.content.res.XmlResourceParser; import android.graphics.drawable.Icon; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerGlobal; +import android.hardware.input.IInputManager; +import android.hardware.input.InputManager; +import android.hardware.input.InputManagerGlobal; import android.hardware.input.KeyGestureEvent; import android.net.Uri; import android.os.Build; @@ -237,6 +240,9 @@ public class AccessibilityManagerServiceTest { @Mock private HearingDevicePhoneCallNotificationController mMockHearingDevicePhoneCallNotificationController; + @Mock + private IInputManager mMockInputManagerService; + private InputManagerGlobal.TestSession mInputManagerTestSession; @Spy private IUserInitializationCompleteCallback mUserInitializationCompleteCallback; @Captor private ArgumentCaptor<Intent> mIntentArgumentCaptor; private IAccessibilityManager mA11yManagerServiceOnDevice; @@ -270,6 +276,10 @@ public class AccessibilityManagerServiceTest { mInputFilter = mock(FakeInputFilter.class); mTestableContext.addMockSystemService(DevicePolicyManager.class, mDevicePolicyManager); + mInputManagerTestSession = InputManagerGlobal.createTestSession(mMockInputManagerService); + InputManager mockInputManager = new InputManager(mTestableContext); + mTestableContext.addMockSystemService(InputManager.class, mockInputManager); + when(mMockPackageManagerInternal.getSystemUiServiceComponent()).thenReturn( new ComponentName("com.android.systemui", "com.android.systemui.SystemUIService")); when(mMockPackageManagerInternal.getPackageUid(eq("com.android.systemui"), anyLong(), @@ -334,6 +344,9 @@ public class AccessibilityManagerServiceTest { FieldSetter.setField( am, AccessibilityManager.class.getDeclaredField("mService"), mA11yManagerServiceOnDevice); + if (mInputManagerTestSession != null) { + mInputManagerTestSession.close(); + } } private void setupAccessibilityServiceConnection(int serviceInfoFlag) { 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/accessibility/magnification/TwoFingersDownOrSwipeTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/TwoFingersDownOrSwipeTest.java index d94faec4cf01..49eb63b2c261 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/TwoFingersDownOrSwipeTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/TwoFingersDownOrSwipeTest.java @@ -28,10 +28,6 @@ import static org.mockito.Mockito.verify; import android.content.Context; import android.graphics.PointF; -import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.annotations.RequiresFlagsEnabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.Display; import android.view.MotionEvent; import android.view.ViewConfiguration; @@ -54,9 +50,6 @@ import java.util.List; */ public class TwoFingersDownOrSwipeTest { - @Rule - public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); - private static final float DEFAULT_X = 100f; private static final float DEFAULT_Y = 100f; @@ -94,22 +87,6 @@ public class TwoFingersDownOrSwipeTest { } @Test - @RequiresFlagsDisabled(android.view.accessibility.Flags.FLAG_COPY_EVENTS_FOR_GESTURE_DETECTION) - public void sendTwoFingerDownEvent_onGestureCompleted_withoutCopiedEvents() { - final List<MotionEvent> downEvents = twoPointersDownEvents(Display.DEFAULT_DISPLAY, - new PointF(DEFAULT_X, DEFAULT_Y), new PointF(DEFAULT_X + 10, DEFAULT_Y + 10)); - - for (MotionEvent event : downEvents) { - mGesturesObserver.onMotionEvent(event, event, 0); - } - - verify(mListener, timeout(sTimeoutMillis)).onGestureCompleted( - MagnificationGestureMatcher.GESTURE_TWO_FINGERS_DOWN_OR_SWIPE, downEvents.get(1), - downEvents.get(1), 0); - } - - @Test - @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_COPY_EVENTS_FOR_GESTURE_DETECTION) public void sendTwoFingerDownEvent_onGestureCompleted() { final List<MotionEvent> downEvents = twoPointersDownEvents(Display.DEFAULT_DISPLAY, new PointF(DEFAULT_X, DEFAULT_Y), new PointF(DEFAULT_X + 10, DEFAULT_Y + 10)); 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/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index c50c62323212..a1f73170e549 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -816,14 +816,14 @@ public class DevicePolicyManagerTest extends DpmTestBase { MockUtils.checkIntentAction( DeviceAdminReceiver.ACTION_DEVICE_ADMIN_DISABLED), MockUtils.checkUserHandle(CALLER_USER_HANDLE), - isNull(String.class), + isNull(), eq(AppOpsManager.OP_NONE), any(Bundle.class), any(BroadcastReceiver.class), eq(dpms.mHandler), eq(Activity.RESULT_OK), - isNull(String.class), - isNull(Bundle.class)); + isNull(), + isNull()); assertThat(dpm.isAdminActiveAsUser(admin1, CALLER_USER_HANDLE)).isFalse(); verify(getServices().usageStatsManagerInternal).setActiveAdminApps( @@ -873,14 +873,14 @@ public class DevicePolicyManagerTest extends DpmTestBase { MockUtils.checkIntentAction( DeviceAdminReceiver.ACTION_DEVICE_ADMIN_DISABLED), MockUtils.checkUserHandle(CALLER_USER_HANDLE), - isNull(String.class), + isNull(), eq(AppOpsManager.OP_NONE), any(Bundle.class), any(BroadcastReceiver.class), eq(dpms.mHandler), eq(Activity.RESULT_OK), - isNull(String.class), - isNull(Bundle.class)); + isNull(), + isNull()); assertThat(dpm.isAdminActiveAsUser(admin1, CALLER_USER_HANDLE)).isFalse(); verify(getServices().usageStatsManagerInternal).setActiveAdminApps( 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/locksettings/RebootEscrowManagerTests.java b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java index ae781dcb834a..52c7c48cc8b7 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java @@ -60,10 +60,6 @@ import android.os.RemoteException; import android.os.ServiceSpecificException; import android.os.UserManager; import android.platform.test.annotations.Presubmit; -import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.annotations.RequiresFlagsEnabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; @@ -74,7 +70,6 @@ import com.android.server.locksettings.ResumeOnRebootServiceProvider.ResumeOnReb import com.android.server.pm.UserManagerInternal; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -113,9 +108,6 @@ public class RebootEscrowManagerTests { 0x26, 0x52, 0x72, 0x63, 0x63, 0x61, 0x78, 0x23, }; - @Rule - public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); - private Context mContext; private UserManager mUserManager; private UserManagerInternal mUserManagerInternal; @@ -847,53 +839,6 @@ public class RebootEscrowManagerTests { } @Test - @RequiresFlagsDisabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR) - public void loadRebootEscrowDataIfAvailable_ServerBasedIoError_RetryFailure() throws Exception { - setServerBasedRebootEscrowProvider(); - - when(mInjected.getBootCount()).thenReturn(0); - RebootEscrowListener mockListener = mock(RebootEscrowListener.class); - mService.setRebootEscrowListener(mockListener); - mService.prepareRebootEscrow(); - - clearInvocations(mServiceConnection); - callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID); - verify(mockListener).onPreparedForReboot(eq(true)); - verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong()); - - // Use x -> x for both wrap & unwrap functions. - when(mServiceConnection.wrapBlob(any(), anyLong(), anyLong())) - .thenAnswer(invocation -> invocation.getArgument(0)); - assertEquals(ARM_REBOOT_ERROR_NONE, mService.armRebootEscrowIfNeeded()); - verify(mServiceConnection).wrapBlob(any(), anyLong(), anyLong()); - assertTrue(mStorage.hasRebootEscrowServerBlob()); - - // pretend reboot happens here - when(mInjected.getBootCount()).thenReturn(1); - ArgumentCaptor<Boolean> metricsSuccessCaptor = ArgumentCaptor.forClass(Boolean.class); - ArgumentCaptor<Integer> metricsErrorCodeCaptor = ArgumentCaptor.forClass(Integer.class); - doNothing() - .when(mInjected) - .reportMetric( - metricsSuccessCaptor.capture(), - metricsErrorCodeCaptor.capture(), - eq(2) /* Server based */, - eq(2) /* attempt count */, - anyInt(), - eq(0) /* vbmeta status */, - anyInt()); - when(mServiceConnection.unwrap(any(), anyLong())).thenThrow(IOException.class); - - mService.loadRebootEscrowDataIfAvailable(mHandler); - // Sleep 5s for the retry to complete - Thread.sleep(5 * 1000); - assertFalse(metricsSuccessCaptor.getValue()); - assertEquals( - Integer.valueOf(RebootEscrowManager.ERROR_NO_NETWORK), - metricsErrorCodeCaptor.getValue()); - } - - @Test public void loadRebootEscrowDataIfAvailable_ServerBased_RetrySuccess() throws Exception { setServerBasedRebootEscrowProvider(); @@ -941,7 +886,6 @@ public class RebootEscrowManagerTests { } @Test - @RequiresFlagsEnabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR) public void loadRebootEscrowDataIfAvailable_waitForInternet_networkUnavailable() throws Exception { setServerBasedRebootEscrowProvider(); @@ -989,7 +933,6 @@ public class RebootEscrowManagerTests { } @Test - @RequiresFlagsEnabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR) public void loadRebootEscrowDataIfAvailable_waitForInternet_networkLost() throws Exception { setServerBasedRebootEscrowProvider(); @@ -1044,7 +987,6 @@ public class RebootEscrowManagerTests { } @Test - @RequiresFlagsEnabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR) public void loadRebootEscrowDataIfAvailable_waitForInternet_networkAvailableWithDelay() throws Exception { setServerBasedRebootEscrowProvider(); @@ -1103,7 +1045,6 @@ public class RebootEscrowManagerTests { } @Test - @RequiresFlagsEnabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR) public void loadRebootEscrowDataIfAvailable_waitForInternet_timeoutExhausted() throws Exception { setServerBasedRebootEscrowProvider(); @@ -1163,7 +1104,6 @@ public class RebootEscrowManagerTests { } @Test - @RequiresFlagsEnabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR) public void loadRebootEscrowDataIfAvailable_serverBasedWaitForNetwork_retryCountExhausted() throws Exception { setServerBasedRebootEscrowProvider(); @@ -1219,7 +1159,6 @@ public class RebootEscrowManagerTests { } @Test - @RequiresFlagsEnabled(Flags.FLAG_WAIT_FOR_INTERNET_ROR) public void loadRebootEscrowDataIfAvailable_ServerBasedWaitForInternet_RetrySuccess() throws Exception { setServerBasedRebootEscrowProvider(); diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java index 88395a4889c4..071bd739072b 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java @@ -306,9 +306,9 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest { // This method is always called, even with PI == null. if (resultIntent == null) { - verify(mServiceContext, times(1)).sendIntentSender(isNull(IntentSender.class)); + verify(mServiceContext, times(1)).sendIntentSender(isNull()); } else { - verify(mServiceContext, times(1)).sendIntentSender(notNull(IntentSender.class)); + verify(mServiceContext, times(1)).sendIntentSender(notNull()); } runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> { @@ -619,7 +619,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest { makeResultIntent())); // The intent should be sent right away. - verify(mServiceContext, times(1)).sendIntentSender(notNull(IntentSender.class)); + verify(mServiceContext, times(1)).sendIntentSender(notNull()); }); // Already pinned. @@ -661,7 +661,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest { assertTrue(request.accept()); // The intent is only sent once, so times(1). - verify(mServiceContext, times(1)).sendIntentSender(isNull(IntentSender.class)); + verify(mServiceContext, times(1)).sendIntentSender(isNull()); }); // Still pinned. @@ -698,7 +698,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest { makeResultIntent())); // The intent should be sent right away. - verify(mServiceContext, times(1)).sendIntentSender(notNull(IntentSender.class)); + verify(mServiceContext, times(1)).sendIntentSender(notNull()); }); // Already pinned. @@ -742,7 +742,7 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest { assertTrue(request.accept()); // The intent is only sent once, so times(1). - verify(mServiceContext, times(1)).sendIntentSender(isNull(IntentSender.class)); + verify(mServiceContext, times(1)).sendIntentSender(isNull()); }); // Still pinned. 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/storage/DiskStatsLoggingServiceTest.java b/services/tests/servicestests/src/com/android/server/storage/DiskStatsLoggingServiceTest.java index 4a97b4670289..291d0ec8fbfc 100644 --- a/services/tests/servicestests/src/com/android/server/storage/DiskStatsLoggingServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/storage/DiskStatsLoggingServiceTest.java @@ -85,7 +85,7 @@ public class DiskStatsLoggingServiceTest extends AndroidTestCase { mDownloads = new TemporaryFolder(); mDownloads.create(); mStorageStats = new ExternalStorageStats(); - when(mSsm.queryExternalStatsForUser(isNull(String.class), any(UserHandle.class))) + when(mSsm.queryExternalStatsForUser((String)isNull(), any(UserHandle.class))) .thenReturn(mStorageStats); when(mJobService.getSystemService(anyString())).thenReturn(mSsm); } diff --git a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt index fbf906586e8b..c59f0a05c619 100644 --- a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt +++ b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt @@ -17,6 +17,7 @@ package com.android.server.supervision import android.app.Activity +import android.app.KeyguardManager import android.app.admin.DevicePolicyManager import android.app.admin.DevicePolicyManagerInternal import android.app.supervision.flags.Flags @@ -61,6 +62,9 @@ class SupervisionServiceTest { @get:Rule val mocks: MockitoRule = MockitoJUnit.rule() @Mock private lateinit var mockDpmInternal: DevicePolicyManagerInternal + + @Mock + private lateinit var mockKeyguardManager: KeyguardManager @Mock private lateinit var mockPackageManager: PackageManager @Mock private lateinit var mockUserManagerInternal: UserManagerInternal @@ -71,7 +75,7 @@ class SupervisionServiceTest { @Before fun setUp() { context = InstrumentationRegistry.getInstrumentation().context - context = SupervisionContextWrapper(context, mockPackageManager) + context = SupervisionContextWrapper(context, mockKeyguardManager, mockPackageManager) LocalServices.removeServiceForTest(DevicePolicyManagerInternal::class.java) LocalServices.addService(DevicePolicyManagerInternal::class.java, mockDpmInternal) @@ -250,11 +254,41 @@ class SupervisionServiceTest { @Test fun createConfirmSupervisionCredentialsIntent() { + service.mInternal.setSupervisionEnabledForUser(context.getUserId(), true) + whenever(mockUserManagerInternal.getSupervisingProfileId()).thenReturn(SUPERVISING_USER_ID) + whenever(mockKeyguardManager.isDeviceSecure(SUPERVISING_USER_ID)).thenReturn(true) + val intent = checkNotNull(service.createConfirmSupervisionCredentialsIntent()) assertThat(intent.action).isEqualTo(ACTION_CONFIRM_SUPERVISION_CREDENTIALS) assertThat(intent.getPackage()).isEqualTo("com.android.settings") } + @Test + fun createConfirmSupervisionCredentialsIntent_supervisionNotEnabled_returnsNull() { + service.mInternal.setSupervisionEnabledForUser(context.getUserId(), false) + whenever(mockUserManagerInternal.getSupervisingProfileId()).thenReturn(SUPERVISING_USER_ID) + whenever(mockKeyguardManager.isDeviceSecure(SUPERVISING_USER_ID)).thenReturn(true) + + assertThat(service.createConfirmSupervisionCredentialsIntent()).isNull() + } + + @Test + fun createConfirmSupervisionCredentialsIntent_noSupervisingUser_returnsNull() { + service.mInternal.setSupervisionEnabledForUser(context.getUserId(), true) + whenever(mockUserManagerInternal.getSupervisingProfileId()).thenReturn(UserHandle.USER_NULL) + + assertThat(service.createConfirmSupervisionCredentialsIntent()).isNull() + } + + @Test + fun createConfirmSupervisionCredentialsIntent_supervisingUserMissingSecureLock_returnsNull() { + service.mInternal.setSupervisionEnabledForUser(context.getUserId(), true) + whenever(mockUserManagerInternal.getSupervisingProfileId()).thenReturn(SUPERVISING_USER_ID) + whenever(mockKeyguardManager.isDeviceSecure(SUPERVISING_USER_ID)).thenReturn(false) + + assertThat(service.createConfirmSupervisionCredentialsIntent()).isNull() + } + private val systemSupervisionPackage: String get() = context.getResources().getString(R.string.config_systemSupervision) @@ -279,6 +313,7 @@ class SupervisionServiceTest { private companion object { const val USER_ID = 100 const val APP_UID = USER_ID * UserHandle.PER_USER_RANGE + const val SUPERVISING_USER_ID = 10 } } @@ -286,10 +321,19 @@ class SupervisionServiceTest { * A context wrapper that allows broadcast intents to immediately invoke the receivers without * performing checks on the sending user. */ -private class SupervisionContextWrapper(val context: Context, val pkgManager: PackageManager) : - ContextWrapper(context) { +private class SupervisionContextWrapper( + val context: Context, + val keyguardManager: KeyguardManager, + val pkgManager: PackageManager, +) : ContextWrapper(context) { val interceptors = mutableListOf<Pair<BroadcastReceiver, IntentFilter>>() + override fun getSystemService(name: String): Any = + when (name) { + Context.KEYGUARD_SERVICE -> keyguardManager + else -> super.getSystemService(name) + } + override fun getPackageManager() = pkgManager override fun registerReceiverForAllUsers( 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..8d164e1acf74 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; @@ -51,18 +53,21 @@ import android.app.AppOpsManager; import android.content.Context; import android.hardware.input.InputManager; import android.os.Bundle; +import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManagerInternal; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.service.dreams.DreamManagerInternal; import android.testing.TestableContext; -import android.view.contentprotection.flags.Flags; +import android.view.KeyEvent; 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; @@ -99,6 +104,8 @@ public class PhoneWindowManagerTests { public final TestableContext mContext = spy( new TestableContext(getInstrumentation().getContext())); + @Mock private IBinder mInputToken; + PhoneWindowManager mPhoneWindowManager; @Mock private ActivityTaskManagerInternal mAtmInternal; @@ -119,6 +126,10 @@ public class PhoneWindowManagerTests { private DisplayPolicy mDisplayPolicy; @Mock private KeyguardServiceDelegate mKeyguardServiceDelegate; + @Mock + private LockPatternUtils mLockPatternUtils; + + private static final int INTERCEPT_SYSTEM_KEY_NOT_CONSUMED_DELAY = 0; @Before public void setUp() { @@ -146,7 +157,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)); } @@ -211,7 +222,7 @@ public class PhoneWindowManagerTests { @Test public void testCheckAddPermission_withoutAccessibilityOverlay_noAccessibilityAppOpLogged() { - mSetFlagsRule.enableFlags(Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); + mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); int[] outAppOp = new int[1]; assertEquals(ADD_OKAY, mPhoneWindowManager.checkAddPermission(TYPE_WALLPAPER, /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp, DEFAULT_DISPLAY)); @@ -220,7 +231,7 @@ public class PhoneWindowManagerTests { @Test public void testCheckAddPermission_withAccessibilityOverlay() { - mSetFlagsRule.enableFlags(Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); + mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); int[] outAppOp = new int[1]; assertEquals(ADD_OKAY, mPhoneWindowManager.checkAddPermission(TYPE_ACCESSIBILITY_OVERLAY, /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp, DEFAULT_DISPLAY)); @@ -229,7 +240,7 @@ public class PhoneWindowManagerTests { @Test public void testCheckAddPermission_withAccessibilityOverlay_flagDisabled() { - mSetFlagsRule.disableFlags(Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); + mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); int[] outAppOp = new int[1]; assertEquals(ADD_OKAY, mPhoneWindowManager.checkAddPermission(TYPE_ACCESSIBILITY_OVERLAY, /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp, DEFAULT_DISPLAY)); @@ -253,6 +264,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 +286,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 +315,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 +336,106 @@ 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(); + } + + @EnableFlags(com.android.hardware.input.Flags.FLAG_FIX_SEARCH_MODIFIER_FALLBACKS) + public void testInterceptKeyBeforeDispatching() { + // Handle sub-tasks of init(). + doNothing().when(mPhoneWindowManager).updateSettings(any()); + doNothing().when(mPhoneWindowManager).initializeHdmiState(); + final DisplayPolicy displayPolicy = mock(DisplayPolicy.class); + mPhoneWindowManager.mDefaultDisplayPolicy = displayPolicy; + mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class); + final PowerManager pm = mock(PowerManager.class); + doReturn(true).when(pm).isInteractive(); + doReturn(pm).when(mContext).getSystemService(eq(Context.POWER_SERVICE)); + + mContext.getMainThreadHandler().runWithScissors(() -> mPhoneWindowManager.init( + new PhoneWindowManager.Injector(mContext, + mock(WindowManagerPolicy.WindowManagerFuncs.class))), 0); + + // Case: KeyNotConsumed with meta key. + KeyEvent keyEvent = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_A, 0, KeyEvent.META_META_ON); + long result = mPhoneWindowManager.interceptKeyBeforeDispatching(mInputToken, keyEvent, 0); + assertEquals(INTERCEPT_SYSTEM_KEY_NOT_CONSUMED_DELAY, result); + + // Case: KeyNotConsumed without meta key. + KeyEvent keyEvent1 = new KeyEvent(0, 0, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_ESCAPE, 0, 0); + long result1 = mPhoneWindowManager.interceptKeyBeforeDispatching(mInputToken, keyEvent1, 0); + assertEquals(INTERCEPT_SYSTEM_KEY_NOT_CONSUMED_DELAY, result1); + } + private void initPhoneWindowManager() { mPhoneWindowManager.mDefaultDisplayPolicy = mDisplayPolicy; mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class); @@ -345,6 +459,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/DeviceStateAutoRotateSettingIssueLoggerTests.java b/services/tests/wmtests/src/com/android/server/wm/DeviceStateAutoRotateSettingIssueLoggerTests.java index f76a9cdbb894..ba9bf1bf8045 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DeviceStateAutoRotateSettingIssueLoggerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DeviceStateAutoRotateSettingIssueLoggerTests.java @@ -24,6 +24,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import android.platform.test.annotations.Presubmit; @@ -143,4 +144,46 @@ public class DeviceStateAutoRotateSettingIssueLoggerTests { anyInt(), anyBoolean()), never()); } + + @Test + public void onStateChange_issueOccurredSettingChangedTwice_reportOnlyOnce() { + mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateAutoRotateSettingChange(); + mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateChange(); + mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateAutoRotateSettingChange(); + + verify(() -> + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_REPORTED), + anyInt(), + anyBoolean()), times(1)); + } + + @Test + public void onStateChange_issueOccurredDeviceStateChangedTwice_reportOnlyOnce() { + mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateChange(); + mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateAutoRotateSettingChange(); + mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateChange(); + + verify(() -> + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_REPORTED), + anyInt(), + anyBoolean()), times(1)); + } + + @Test + public void onStateChange_issueOccurredAfterDelay_reportOnce() { + mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateAutoRotateSettingChange(); + mTestTimeSupplier.delay( + DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_THRESHOLD_MILLIS + DELAY); + mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateChange(); + mTestTimeSupplier.delay(DELAY); + mDeviceStateAutoRotateSettingIssueLogger.onDeviceStateAutoRotateSettingChange(); + + verify(() -> + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DEVICE_STATE_AUTO_ROTATE_SETTING_ISSUE_REPORTED), + eq(DELAY), + anyBoolean()), times(1)); + } } 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/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/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index 7ec8f9ce9864..71c7a6b1119d 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -35,8 +35,8 @@ import android.os.PermissionEnforcer import android.os.SystemClock import android.os.test.FakePermissionEnforcer import android.os.test.TestLooper -import android.platform.test.annotations.Presubmit import android.platform.test.annotations.EnableFlags +import android.platform.test.annotations.Presubmit import android.platform.test.flag.junit.SetFlagsRule import android.provider.Settings import android.view.View.OnKeyListener @@ -373,6 +373,29 @@ class InputManagerServiceTests { } @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_FIX_SEARCH_MODIFIER_FALLBACKS) + fun testInterceptKeyBeforeDispatchingWithFallthroughEvent() { + service.systemRunning() + overrideSendActionKeyEventsToFocusedWindow( + /* hasPermission = */false, + /* hasPrivateFlag = */false + ) + whenever(wmCallbacks.interceptKeyBeforeDispatching(any(), any(), anyInt())).thenReturn(0) + + // Create a fallback for a key event with a meta modifier. Should result in -2, + // which represents the fallback event, which indicates that original key event will + // be ignored (not sent to app) and instead the fallback will be created and sent to the + // app. + val fallbackAction: KeyCharacterMap.FallbackAction = KeyCharacterMap.FallbackAction.obtain() + fallbackAction.keyCode = KeyEvent.KEYCODE_SEARCH + whenever(kcm.getFallbackAction(anyInt(), anyInt())).thenReturn(fallbackAction) + + val event = KeyEvent( /* downTime= */0, /* eventTime= */0, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_SPACE, /* repeat= */0, KeyEvent.META_META_ON) + assertEquals(-2, service.interceptKeyBeforeDispatching(null, event, 0)) + } + + @Test fun testKeyEventsNotForwardedToFocusedWindow_whenWmConsumes() { service.systemRunning() overrideSendActionKeyEventsToFocusedWindow( 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/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java index adefac64dbae..6846d489ecaa 100644 --- a/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java +++ b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java @@ -231,7 +231,7 @@ public class SmsApplicationTest { .replacePreferredActivity(intentFilterCaptor.capture(), eq(IntentFilter.MATCH_CATEGORY_SCHEME | IntentFilter.MATCH_ADJUSTMENT_NORMAL), - isNotNull(List.class), + (List<ComponentName>)isNotNull(), eq(new ComponentName(TEST_COMPONENT_NAME.getPackageName(), SEND_TO_NAME))); Set<String> capturedSchemes = intentFilterCaptor.getAllValues().stream() 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. diff --git a/tools/protologtool/src/com/android/protolog/tool/exceptions.kt b/tools/protologtool/src/com/android/protolog/tool/Exceptions.kt index ae00df123353..4f7e8d13c4ba 100644 --- a/tools/protologtool/src/com/android/protolog/tool/exceptions.kt +++ b/tools/protologtool/src/com/android/protolog/tool/Exceptions.kt @@ -18,21 +18,24 @@ package com.android.protolog.tool import java.lang.Exception -open class CodeProcessingException(message: String, context: ParsingContext) - : Exception("Code processing error in ${context.filePath}:${context.lineNumber}:\n" + - " $message") +open class CodeProcessingException( + message: String, context: ParsingContext, cause: Throwable? = null +) : Exception("Code processing error in ${context.filePath}:${context.lineNumber}:\n" + + " $message", cause) -class HashCollisionException(message: String, context: ParsingContext) : - CodeProcessingException(message, context) +class HashCollisionException( + message: String, context: ParsingContext, cause: Throwable? = null +) : CodeProcessingException(message, context, cause) -class IllegalImportException(message: String, context: ParsingContext) : - CodeProcessingException("Illegal import: $message", context) +class IllegalImportException(message: String, context: ParsingContext, cause: Throwable? = null) : + CodeProcessingException("Illegal import: $message", context, cause) -class InvalidProtoLogCallException(message: String, context: ParsingContext) - : CodeProcessingException("InvalidProtoLogCall: $message", context) +class InvalidProtoLogCallException( + message: String, context: ParsingContext, cause: Throwable? = null +) : CodeProcessingException("InvalidProtoLogCall: $message", context, cause) -class ParsingException(message: String, context: ParsingContext) - : CodeProcessingException(message, context) +class ParsingException(message: String, context: ParsingContext, cause: Throwable? = null) : + CodeProcessingException(message, context, cause) class InvalidViewerConfigException(message: String) : Exception(message) diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt index 272d8bb1793d..1f6db5fe2d37 100644 --- a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt +++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt @@ -118,9 +118,17 @@ class ProtoLogCallProcessorImpl( "- not a ProtoLogGroup enum member: $call", context) } - logCallVisitor?.processCall(call, messageString, getLevelForMethodName( - call.name.toString(), call, context), groupMap.getValue(groupName), - context.lineNumber) + try { + logCallVisitor?.processCall( + call, messageString, getLevelForMethodName( + call.name.toString(), call, context + ), groupMap.getValue(groupName), + context.lineNumber + ) + } catch (e: Throwable) { + throw InvalidProtoLogCallException("Error processing log call: $call", + context, e) + } } else if (call.name.id == "init") { // No processing } else { diff --git a/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorImplTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorImplTest.kt index 004d97babbad..732824ae841c 100644 --- a/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorImplTest.kt +++ b/tools/protologtool/tests/com/android/protolog/tool/ProtoLogCallProcessorImplTest.kt @@ -16,11 +16,14 @@ package com.android.protolog.tool +import com.android.internal.protolog.common.InvalidFormatStringException import com.android.internal.protolog.common.LogLevel import com.github.javaparser.StaticJavaParser import com.github.javaparser.ast.expr.MethodCallExpr import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows import org.junit.Test +import com.google.common.truth.Truth class ProtoLogCallProcessorImplTest { private data class LogCall( @@ -228,4 +231,38 @@ class ProtoLogCallProcessorImplTest { visitor.process(StaticJavaParser.parse(code), processor, "") checkCalls() } + + @Test + fun throws_clear_error_message_on_invalid_format_exception() { + val code = """ + package org.example; + + class Test { + void test() { + ProtoLog.d(ProtoLogGroup.TEST, "Invalid message %9 %"); + } + } + """ + groupMap["TEST"] = LogGroup("TEST", false, true, "WindowManager") + + val processor = object : ProtoLogCallVisitor { + override fun processCall( + call: MethodCallExpr, + messageString: String, + level: LogLevel, + group: LogGroup, + lineNumber: Int, + ) { + throw InvalidFormatStringException("Invalid Protolog message format") + } + } + + val exception = assertThrows(InvalidProtoLogCallException::class.java) { + visitor.process(StaticJavaParser.parse(code), processor, "MyTestFile.java") + } + Truth.assertThat(exception).hasMessageThat() + .contains("Code processing error in MyTestFile.java:6") + Truth.assertThat(exception.cause).hasMessageThat() + .contains("Invalid Protolog message format") + } } |