diff options
788 files changed, 18108 insertions, 9549 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index b4127c5660f7..3c5686bd6d13 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -26,6 +26,7 @@ aconfig_declarations_group { "android.app.flags-aconfig-java", "android.app.ondeviceintelligence-aconfig-java", "android.app.smartspace.flags-aconfig-java", + "android.app.supervision.flags-aconfig-java", "android.app.usage.flags-aconfig-java", "android.app.wearable.flags-aconfig-java", "android.appwidget.flags-aconfig-java", @@ -1212,6 +1213,21 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +// Supervision +aconfig_declarations { + name: "android.app.supervision.flags-aconfig", + exportable: true, + package: "android.app.supervision.flags", + container: "system", + srcs: ["core/java/android/app/supervision/flags.aconfig"], +} + +java_aconfig_library { + name: "android.app.supervision.flags-aconfig-java", + aconfig_declarations: "android.app.supervision.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // SurfaceFlinger java_aconfig_library { name: "surfaceflinger_flags_java_lib", diff --git a/TEST_MAPPING b/TEST_MAPPING index 5db077220b20..dfacbc425181 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -140,11 +140,17 @@ "ravenwood-presubmit": [ { "name": "CtsUtilTestCasesRavenwood", - "host": true + "host": true, + "file_patterns": [ + "[Rr]avenwood" + ] }, { "name": "RavenwoodBivalentTest", - "host": true + "host": true, + "file_patterns": [ + "[Rr]avenwood" + ] } ], "postsubmit-managedprofile-stress": [ diff --git a/apct-tests/perftests/protolog/Android.bp b/apct-tests/perftests/protolog/Android.bp new file mode 100644 index 000000000000..08e365be514a --- /dev/null +++ b/apct-tests/perftests/protolog/Android.bp @@ -0,0 +1,33 @@ +// Copyright (C) 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +android_test { + name: "ProtologPerfTests", + team: "trendy_team_windowing_tools", + srcs: ["src/**/*.java"], + static_libs: [ + "androidx.test.rules", + "androidx.annotation_annotation", + "apct-perftests-utils", + "collector-device-lib", + "platform-test-annotations", + ], + test_suites: [ + "device-tests", + "automotive-tests", + ], + data: [":perfetto_artifacts"], + platform_apis: true, + certificate: "platform", +} diff --git a/apct-tests/perftests/protolog/AndroidManifest.xml b/apct-tests/perftests/protolog/AndroidManifest.xml new file mode 100644 index 000000000000..68125df99ec3 --- /dev/null +++ b/apct-tests/perftests/protolog/AndroidManifest.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.perftests.protolog"> + + <!-- For perfetto trace files --> + <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + + <application> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.perftests.protolog"> + <!-- <meta-data android:name="listener" android:value="android.protolog.ProtologPerfRunListener" /> --> + </instrumentation> +</manifest> diff --git a/apct-tests/perftests/protolog/AndroidTest.xml b/apct-tests/perftests/protolog/AndroidTest.xml new file mode 100644 index 000000000000..871a20ce4cef --- /dev/null +++ b/apct-tests/perftests/protolog/AndroidTest.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="Runs ProtologPerfTests metric instrumentation."> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-metric-instrumentation" /> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="ProtologPerfTests.apk" /> + </target_preparer> + + <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <option name="force-skip-system-props" value="true" /> + <option name="run-command" value="input keyevent KEYCODE_WAKEUP" /> + <option name="run-command" value="cmd window dismiss-keyguard" /> + <option name="run-command" value="cmd package compile -m speed com.android.perftests.wm" /> + </target_preparer> + + <!-- Needed for pushing the trace config file --> + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + <option name="push-file" key="trace_config_detailed.textproto" value="/data/misc/perfetto-traces/trace_config.textproto" /> + </target_preparer> + + <!-- Needed for storing the perfetto trace files in the sdcard/test_results--> + <option name="isolated-storage" value="false" /> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.perftests.protolog" /> + <option name="hidden-api-checks" value="false"/> + + <!-- Listener related args for collecting the traces and waiting for the device to stabilize. --> + <option name="device-listeners" value="android.device.collectors.ProcLoadListener,android.device.collectors.PerfettoListener" /> + + <!-- Guarantee that user defined RunListeners will be running before any of the default listeners defined in this runner. --> + <option name="instrumentation-arg" key="newRunListenerMode" value="true" /> + + <!-- ProcLoadListener related arguments --> + <!-- Wait for device last minute threshold to reach 3 with 2 minute timeout before starting the test run --> + <option name="instrumentation-arg" key="procload-collector:per_run" value="true" /> + <option name="instrumentation-arg" key="proc-loadavg-threshold" value="3" /> + <option name="instrumentation-arg" key="proc-loadavg-timeout" value="120000" /> + <option name="instrumentation-arg" key="proc-loadavg-interval" value="10000" /> + + <!-- PerfettoListener related arguments --> + <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true" /> + <option name="instrumentation-arg" key="perfetto_config_file" value="trace_config.textproto" /> + </test> + + <!-- <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="directory-keys" value="/data/local/tmp/ProtologPerfTests" /> --> + <!-- Needed for pulling the collected trace config on to the host --> + <!-- <option name="pull-pattern-keys" value="perfetto_file_path" /> + </metrics_collector> --> +</configuration> diff --git a/apct-tests/perftests/protolog/OWNERS b/apct-tests/perftests/protolog/OWNERS new file mode 100644 index 000000000000..3f3308cfc75a --- /dev/null +++ b/apct-tests/perftests/protolog/OWNERS @@ -0,0 +1 @@ +include platform/development:/tools/winscope/OWNERS diff --git a/apct-tests/perftests/protolog/src/com/android/internal/protolog/ProtologPerfTest.java b/apct-tests/perftests/protolog/src/com/android/internal/protolog/ProtologPerfTest.java new file mode 100644 index 000000000000..e1edb3712ff0 --- /dev/null +++ b/apct-tests/perftests/protolog/src/com/android/internal/protolog/ProtologPerfTest.java @@ -0,0 +1,171 @@ +/* + * 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.internal.protolog; + +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; + +import com.android.internal.protolog.common.IProtoLogGroup; +import com.android.internal.protolog.common.LogLevel; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import java.util.Arrays; +import java.util.Collection; + +@RunWith(Parameterized.class) +public class ProtologPerfTest { + @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + @Parameters(name="logToProto_{0}_logToLogcat_{1}") + public static Collection<Object[]> params() { + return Arrays.asList(new Object[][] { + { true, true }, + { true, false }, + { false, true }, + { false, false } + }); + } + + private final boolean mLogToProto; + private final boolean mLogToLogcat; + + public ProtologPerfTest(boolean logToProto, boolean logToLogcat) { + mLogToProto = logToProto; + mLogToLogcat = logToLogcat; + } + + @BeforeClass + public static void init() { + ProtoLog.init(TestProtoLogGroup.values()); + } + + @Before + public void setUp() { + TestProtoLogGroup.TEST_GROUP.setLogToProto(mLogToProto); + TestProtoLogGroup.TEST_GROUP.setLogToLogcat(mLogToLogcat); + } + + @Test + public void logProcessedProtoLogMessageWithoutArgs() { + final var protoLog = ProtoLog.getSingleInstance(); + + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + protoLog.log( + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 123, + 0, (Object[]) null); + } + } + + @Test + public void logProcessedProtoLogMessageWithArgs() { + final var protoLog = ProtoLog.getSingleInstance(); + + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + protoLog.log( + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 123, + 0b1110101001010100, + new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true}); + } + } + + @Test + public void logNonProcessedProtoLogMessageWithNoArgs() { + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + ProtoLog.d(TestProtoLogGroup.TEST_GROUP, "Test message"); + } + } + + @Test + public void logNonProcessedProtoLogMessageWithArgs() { + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + ProtoLog.d(TestProtoLogGroup.TEST_GROUP, "Test messag %s, %d, %b", "arg1", 2, true); + } + } + + private enum TestProtoLogGroup implements IProtoLogGroup { + TEST_GROUP(true, true, false, "WindowManagetProtoLogTest"); + + private final boolean mEnabled; + private volatile boolean mLogToProto; + private volatile boolean mLogToLogcat; + private final String mTag; + + /** + * @param enabled set to false to exclude all log statements for this group from + * compilation, they will not be available in runtime. + * @param logToProto enable binary logging for the group + * @param logToLogcat enable text logging for the group + * @param tag name of the source of the logged message + */ + TestProtoLogGroup(boolean enabled, boolean logToProto, boolean logToLogcat, String tag) { + this.mEnabled = enabled; + this.mLogToProto = logToProto; + this.mLogToLogcat = logToLogcat; + this.mTag = tag; + } + + @Override + public boolean isEnabled() { + return mEnabled; + } + + @Override + public boolean isLogToProto() { + return mLogToProto; + } + + @Override + public boolean isLogToLogcat() { + return mLogToLogcat; + } + + @Override + public boolean isLogToAny() { + return mLogToLogcat || mLogToProto; + } + + @Override + public String getTag() { + return mTag; + } + + @Override + public void setLogToProto(boolean logToProto) { + this.mLogToProto = logToProto; + } + + @Override + public void setLogToLogcat(boolean logToLogcat) { + this.mLogToLogcat = logToLogcat; + } + + @Override + public int getId() { + return ordinal(); + } + } +} diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp index d991da59f167..b3a674fbd70e 100644 --- a/api/StubLibraries.bp +++ b/api/StubLibraries.bp @@ -890,7 +890,7 @@ java_genrule { cmd: "rm -f $(genDir)/framework.aidl.merged && " + "for i in $(in); do " + " rm -f $(genDir)/framework.aidl.tmp && " + - " $(location sdkparcelables) $$i $(genDir)/framework.aidl.tmp && " + + " $(location sdkparcelables) $$i $(genDir)/framework.aidl.tmp --guarantee_stable && " + " cat $(genDir)/framework.aidl.tmp >> $(genDir)/framework.aidl.merged; " + "done && " + "sort -u $(genDir)/framework.aidl.merged > $(out)", diff --git a/core/api/current.txt b/core/api/current.txt index c7df6623e36d..861be4079acc 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -7968,7 +7968,7 @@ package android.app.admin { field public static final String PERMISSION_GRANT_POLICY = "permissionGrant"; field public static final String PERSISTENT_PREFERRED_ACTIVITY_POLICY = "persistentPreferredActivity"; field public static final String RESET_PASSWORD_TOKEN_POLICY = "resetPasswordToken"; - field @FlaggedApi("android.app.admin.flags.security_log_v2_enabled") public static final String SECURITY_LOGGING_POLICY = "securityLogging"; + field public static final String SECURITY_LOGGING_POLICY = "securityLogging"; field public static final String STATUS_BAR_DISABLED_POLICY = "statusBarDisabled"; field @FlaggedApi("android.app.admin.flags.policy_engine_migration_v2_enabled") public static final String USB_DATA_SIGNALING_POLICY = "usbDataSignaling"; field public static final String USER_CONTROL_DISABLED_PACKAGES_POLICY = "userControlDisabledPackages"; @@ -54922,6 +54922,8 @@ package android.view.accessibility { method @Deprecated public void addAction(int); method public void addChild(android.view.View); method public void addChild(android.view.View, int); + method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public void addLabeledBy(@NonNull android.view.View); + method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public void addLabeledBy(@NonNull android.view.View, int); method public boolean canOpenPopup(); method public int describeContents(); method public java.util.List<android.view.accessibility.AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String); @@ -54950,6 +54952,7 @@ package android.view.accessibility { method public int getInputType(); method public android.view.accessibility.AccessibilityNodeInfo getLabelFor(); method public android.view.accessibility.AccessibilityNodeInfo getLabeledBy(); + method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") @NonNull public java.util.List<android.view.accessibility.AccessibilityNodeInfo> getLabeledByList(); method public int getLiveRegion(); method public int getMaxTextLength(); method @NonNull public java.time.Duration getMinDurationBetweenContentChanges(); @@ -55010,6 +55013,8 @@ package android.view.accessibility { method public boolean removeAction(android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction); method public boolean removeChild(android.view.View); method public boolean removeChild(android.view.View, int); + method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public boolean removeLabeledBy(@NonNull android.view.View); + method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public boolean removeLabeledBy(@NonNull android.view.View, int); method public void setAccessibilityDataSensitive(boolean); method public void setAccessibilityFocused(boolean); method public void setAvailableExtraData(java.util.List<java.lang.String>); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 539962fef236..5413c6606bcb 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -201,7 +201,7 @@ package android { field public static final String MANAGE_DEFAULT_APPLICATIONS = "android.permission.MANAGE_DEFAULT_APPLICATIONS"; field public static final String MANAGE_DEVICE_ADMINS = "android.permission.MANAGE_DEVICE_ADMINS"; field public static final String MANAGE_DEVICE_POLICY_APP_EXEMPTIONS = "android.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS"; - field @FlaggedApi("android.app.admin.flags.security_log_v2_enabled") public static final String MANAGE_DEVICE_POLICY_AUDIT_LOGGING = "android.permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING"; + field public static final String MANAGE_DEVICE_POLICY_AUDIT_LOGGING = "android.permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING"; field @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public static final String MANAGE_ENHANCED_CONFIRMATION_STATES = "android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES"; field public static final String MANAGE_ETHERNET_NETWORKS = "android.permission.MANAGE_ETHERNET_NETWORKS"; field public static final String MANAGE_FACTORY_RESET_PROTECTION = "android.permission.MANAGE_FACTORY_RESET_PROTECTION"; @@ -1296,7 +1296,7 @@ package android.app.admin { } public final class DevicePolicyIdentifiers { - field @FlaggedApi("android.app.admin.flags.security_log_v2_enabled") public static final String AUDIT_LOGGING_POLICY = "auditLogging"; + field public static final String AUDIT_LOGGING_POLICY = "auditLogging"; } public class DevicePolicyKeyguardService extends android.app.Service { @@ -1308,7 +1308,7 @@ package android.app.admin { public class DevicePolicyManager { method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public int checkProvisioningPrecondition(@NonNull String, @NonNull String); - method @FlaggedApi("android.app.admin.flags.security_log_v2_enabled") @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public void clearAuditLogEventCallback(); + method @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public void clearAuditLogEventCallback(); method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public android.os.UserHandle createAndProvisionManagedProfile(@NonNull android.app.admin.ManagedProfileProvisioningParams) throws android.app.admin.ProvisioningException; method @Nullable public android.content.Intent createProvisioningIntentFromNfcIntent(@NonNull android.content.Intent); method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void finalizeWorkProfileProvisioning(@NonNull android.os.UserHandle, @Nullable android.accounts.Account); @@ -1328,7 +1328,7 @@ package android.app.admin { method @Nullable public android.content.ComponentName getProfileOwner() throws java.lang.IllegalArgumentException; method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS}) public String getProfileOwnerNameAsUser(int) throws java.lang.IllegalArgumentException; method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS}) public int getUserProvisioningState(); - method @FlaggedApi("android.app.admin.flags.security_log_v2_enabled") @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public boolean isAuditLogEnabled(); + method @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public boolean isAuditLogEnabled(); method public boolean isDeviceManaged(); method @FlaggedApi("android.app.admin.flags.device_theft_api_enabled") @RequiresPermission(android.Manifest.permission.QUERY_DEVICE_STOLEN_STATE) public boolean isDevicePotentiallyStolen(); method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public boolean isDeviceProvisioned(); @@ -1344,8 +1344,8 @@ package android.app.admin { method @RequiresPermission(android.Manifest.permission.TRIGGER_LOST_MODE) public void sendLostModeLocationUpdate(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>); method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_ADMINS) public boolean setActiveProfileOwner(@NonNull android.content.ComponentName, String) throws java.lang.IllegalArgumentException; method @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS) public void setApplicationExemptions(@NonNull String, @NonNull java.util.Set<java.lang.Integer>) throws android.content.pm.PackageManager.NameNotFoundException; - method @FlaggedApi("android.app.admin.flags.security_log_v2_enabled") @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public void setAuditLogEnabled(boolean); - method @FlaggedApi("android.app.admin.flags.security_log_v2_enabled") @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public void setAuditLogEventCallback(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.app.admin.SecurityLog.SecurityEvent>>); + method @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public void setAuditLogEnabled(boolean); + method @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public void setAuditLogEventCallback(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.app.admin.SecurityLog.SecurityEvent>>); method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public void setDeviceProvisioningConfigApplied(); method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void setDpcDownloaded(boolean); method @FlaggedApi("android.app.admin.flags.device_policy_size_tracking_enabled") @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void setMaxPolicyStorageLimit(int); @@ -3443,7 +3443,7 @@ package android.companion.virtual { field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.ActivityPolicyExemption> CREATOR; } - public static final class ActivityPolicyExemption.Builder { + @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public static final class ActivityPolicyExemption.Builder { ctor public ActivityPolicyExemption.Builder(); method @NonNull public android.companion.virtual.ActivityPolicyExemption build(); method @NonNull public android.companion.virtual.ActivityPolicyExemption.Builder setComponentName(@NonNull android.content.ComponentName); @@ -8087,6 +8087,7 @@ package android.media.tv.tuner { public class Tuner implements java.lang.AutoCloseable { ctor @RequiresPermission(android.Manifest.permission.ACCESS_TV_TUNER) public Tuner(@NonNull android.content.Context, @Nullable String, int); method public int applyFrontend(@NonNull android.media.tv.tuner.frontend.FrontendInfo); + method @FlaggedApi("android.media.tv.flags.tuner_w_apis") @RequiresPermission(allOf={"android.permission.TUNER_RESOURCE_ACCESS", "android.permission.ACCESS_TV_TUNER"}) public int applyFrontendByType(int); method public int cancelScanning(); method public int cancelTuning(); method public void clearOnTuneEventListener(); @@ -14223,7 +14224,7 @@ package android.telecom { field public static final int CAPABILITY_EMERGENCY_PREFERRED = 8192; // 0x2000 field public static final int CAPABILITY_EMERGENCY_VIDEO_CALLING = 512; // 0x200 field public static final int CAPABILITY_MULTI_USER = 32; // 0x20 - field public static final String EXTRA_PLAY_CALL_RECORDING_TONE = "android.telecom.extra.PLAY_CALL_RECORDING_TONE"; + field @Deprecated @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String EXTRA_PLAY_CALL_RECORDING_TONE = "android.telecom.extra.PLAY_CALL_RECORDING_TONE"; field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String EXTRA_SKIP_CALL_FILTERING = "android.telecom.extra.SKIP_CALL_FILTERING"; field public static final String EXTRA_SORT_ORDER = "android.telecom.extra.SORT_ORDER"; } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index ce0d38ffe9ad..ec23cfe7c6bb 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1992,6 +1992,7 @@ package android.media { method @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_AUDIO_ROUTING, android.Manifest.permission.QUERY_AUDIO_STATE, android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED}) public boolean isFullVolumeDevice(); method @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public boolean isPstnCallAudioInterceptable(); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public boolean isVolumeControlUsingVolumeGroups(); + method public void permissionUpdateBarrier(); method @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public int requestAudioFocusForTest(@NonNull android.media.AudioFocusRequest, @NonNull String, int, int); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public void setCsd(float); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public void setNotifAliasRingForTest(boolean); @@ -3676,6 +3677,7 @@ package android.view { public static final class Display.Mode implements android.os.Parcelable { ctor public Display.Mode(int, int, float); + method public boolean isSynthetic(); method public boolean matches(int, int, float); } diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index e57630bfd5ed..68063c4a1a50 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -233,6 +233,10 @@ public class ActivityManager { private static final RateLimitingCache<List<RunningAppProcessInfo>> mRunningProcessesCache = new RateLimitingCache<>(10, 4); + /** Rate-Limiting Cache that allows no more than 200 calls to the service per second. */ + private static final RateLimitingCache<List<ProcessErrorStateInfo>> mErrorProcessesCache = + new RateLimitingCache<>(10, 2); + /** * Map of callbacks that have registered for {@link UidFrozenStateChanged} events. * Will be called when a Uid has become frozen or unfrozen. @@ -3685,6 +3689,16 @@ public class ActivityManager { * specified. */ public List<ProcessErrorStateInfo> getProcessesInErrorState() { + if (Flags.rateLimitGetProcessesInErrorState()) { + return mErrorProcessesCache.get(() -> { + return getProcessesInErrorStateInternal(); + }); + } else { + return getProcessesInErrorStateInternal(); + } + } + + private List<ProcessErrorStateInfo> getProcessesInErrorStateInternal() { try { return getService().getProcessesInErrorState(); } catch (RemoteException e) { diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index d4558533291f..4350545a1b1d 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -4583,7 +4583,7 @@ public final class ActivityThread extends ClientTransactionHandler public void handleAttachSplashScreenView(@NonNull ActivityClientRecord r, @Nullable SplashScreenView.SplashScreenViewParcelable parcelable, @NonNull SurfaceControl startingWindowLeash) { - final DecorView decorView = (DecorView) r.window.peekDecorView(); + final DecorView decorView = r.window != null ? (DecorView) r.window.peekDecorView() : null; if (parcelable != null && decorView != null) { createSplashScreen(r, decorView, parcelable, startingWindowLeash); } else { diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java index 0ff55149bf6e..1e9a79bca65a 100644 --- a/core/java/android/app/ApplicationStartInfo.java +++ b/core/java/android/app/ApplicationStartInfo.java @@ -210,6 +210,11 @@ public final class ApplicationStartInfo implements Parcelable { public static final int START_TIMESTAMP_SURFACEFLINGER_COMPOSITION_COMPLETE = 7; /** + * @see #getMonoticCreationTimeMs + */ + private long mMonoticCreationTimeMs; + + /** * @see #getStartupState */ private @StartupState int mStartupState; @@ -487,6 +492,15 @@ public final class ApplicationStartInfo implements Parcelable { } /** + * Monotonic elapsed time persisted across reboots. + * + * @hide + */ + public long getMonoticCreationTimeMs() { + return mMonoticCreationTimeMs; + } + + /** * The process id. * * <p class="note"> Note: field will be set for any {@link #getStartupState} value.</p> @@ -669,7 +683,9 @@ public final class ApplicationStartInfo implements Parcelable { } /** @hide */ - public ApplicationStartInfo() {} + public ApplicationStartInfo(long monotonicCreationTimeMs) { + mMonoticCreationTimeMs = monotonicCreationTimeMs; + } /** @hide */ public ApplicationStartInfo(ApplicationStartInfo other) { @@ -686,6 +702,7 @@ public final class ApplicationStartInfo implements Parcelable { mStartIntent = other.mStartIntent; mLaunchMode = other.mLaunchMode; mWasForceStopped = other.mWasForceStopped; + mMonoticCreationTimeMs = other.mMonoticCreationTimeMs; } private ApplicationStartInfo(@NonNull Parcel in) { @@ -708,6 +725,7 @@ public final class ApplicationStartInfo implements Parcelable { in.readParcelable(Intent.class.getClassLoader(), android.content.Intent.class); mLaunchMode = in.readInt(); mWasForceStopped = in.readBoolean(); + mMonoticCreationTimeMs = in.readLong(); } private static String intern(@Nullable String source) { @@ -786,6 +804,7 @@ public final class ApplicationStartInfo implements Parcelable { } proto.write(ApplicationStartInfoProto.LAUNCH_MODE, mLaunchMode); proto.write(ApplicationStartInfoProto.WAS_FORCE_STOPPED, mWasForceStopped); + proto.write(ApplicationStartInfoProto.MONOTONIC_CREATION_TIME_MS, mMonoticCreationTimeMs); proto.end(token); } @@ -869,6 +888,10 @@ public final class ApplicationStartInfo implements Parcelable { mWasForceStopped = proto.readBoolean( ApplicationStartInfoProto.WAS_FORCE_STOPPED); break; + case (int) ApplicationStartInfoProto.MONOTONIC_CREATION_TIME_MS: + mMonoticCreationTimeMs = proto.readLong( + ApplicationStartInfoProto.MONOTONIC_CREATION_TIME_MS); + break; } } proto.end(token); @@ -881,6 +904,8 @@ public final class ApplicationStartInfo implements Parcelable { sb.append(prefix) .append("ApplicationStartInfo ").append(seqSuffix).append(':') .append('\n') + .append(" monotonicCreationTimeMs=").append(mMonoticCreationTimeMs) + .append('\n') .append(" pid=").append(mPid) .append(" realUid=").append(mRealUid) .append(" packageUid=").append(mPackageUid) @@ -949,14 +974,15 @@ public final class ApplicationStartInfo implements Parcelable { && mDefiningUid == o.mDefiningUid && mReason == o.mReason && mStartupState == o.mStartupState && mStartType == o.mStartType && mLaunchMode == o.mLaunchMode && TextUtils.equals(mProcessName, o.mProcessName) - && timestampsEquals(o) && mWasForceStopped == o.mWasForceStopped; + && timestampsEquals(o) && mWasForceStopped == o.mWasForceStopped + && mMonoticCreationTimeMs == o.mMonoticCreationTimeMs; } @Override public int hashCode() { return Objects.hash(mPid, mRealUid, mPackageUid, mDefiningUid, mReason, mStartupState, - mStartType, mLaunchMode, mProcessName, - mStartupTimestampsNs); + mStartType, mLaunchMode, mProcessName, mStartupTimestampsNs, + mMonoticCreationTimeMs); } private boolean timestampsEquals(@NonNull ApplicationStartInfo other) { diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index cb38cf297cf6..8b3ee24db025 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -48,6 +48,8 @@ import android.app.sdksandbox.SdkSandboxManagerFrameworkInitializer; import android.app.search.SearchUiManager; import android.app.slice.SliceManager; import android.app.smartspace.SmartspaceManager; +import android.app.supervision.ISupervisionManager; +import android.app.supervision.SupervisionManager; import android.app.time.TimeManager; import android.app.timedetector.TimeDetector; import android.app.timedetector.TimeDetectorImpl; @@ -1703,6 +1705,21 @@ public final class SystemServiceRegistry { return new E2eeContactKeysManager(ctx); }}); + registerService(Context.SUPERVISION_SERVICE, SupervisionManager.class, + new CachedServiceFetcher<>() { + @Override + public SupervisionManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + if (!android.app.supervision.flags.Flags.supervisionApi()) { + throw new ServiceNotFoundException( + "SupervisionManager is not supported"); + } + IBinder iBinder = ServiceManager.getServiceOrThrow( + Context.SUPERVISION_SERVICE); + ISupervisionManager service = ISupervisionManager.Stub.asInterface(iBinder); + return new SupervisionManager(ctx, service); + } + }); // DO NOT do a flag check like this unless the flag is read-only. // (because this code is executed during preload in zygote.) // If the flag is mutable, the check should be inside CachedServiceFetcher. diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig index d9594d3e4c31..32e6e80cc3e9 100644 --- a/core/java/android/app/activity_manager.aconfig +++ b/core/java/android/app/activity_manager.aconfig @@ -93,3 +93,14 @@ flag { } } +flag { + namespace: "backstage_power" + name: "rate_limit_get_processes_in_error_state" + description: "Rate limit calls to getProcessesInErrorState using a cache" + is_fixed_read_only: true + bug: "361146083" + metadata { + purpose: PURPOSE_BUGFIX + } +} + diff --git a/core/java/android/app/admin/DevicePolicyIdentifiers.java b/core/java/android/app/admin/DevicePolicyIdentifiers.java index eeaf0b3706fc..156512a90295 100644 --- a/core/java/android/app/admin/DevicePolicyIdentifiers.java +++ b/core/java/android/app/admin/DevicePolicyIdentifiers.java @@ -17,7 +17,6 @@ package android.app.admin; import static android.app.admin.flags.Flags.FLAG_POLICY_ENGINE_MIGRATION_V2_ENABLED; -import static android.app.admin.flags.Flags.FLAG_SECURITY_LOG_V2_ENABLED; import android.annotation.FlaggedApi; import android.annotation.NonNull; @@ -50,7 +49,6 @@ public final class DevicePolicyIdentifiers { /** * String identifier for {@link DevicePolicyManager#setSecurityLoggingEnabled}. */ - @FlaggedApi(FLAG_SECURITY_LOG_V2_ENABLED) public static final String SECURITY_LOGGING_POLICY = "securityLogging"; /** @@ -58,7 +56,6 @@ public final class DevicePolicyIdentifiers { * * @hide */ - @FlaggedApi(FLAG_SECURITY_LOG_V2_ENABLED) @SystemApi public static final String AUDIT_LOGGING_POLICY = "auditLogging"; diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index ba1dc5677b21..5088ea6b603c 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -60,7 +60,6 @@ import static android.app.admin.flags.Flags.FLAG_ESIM_MANAGEMENT_ENABLED; import static android.app.admin.flags.Flags.FLAG_DEVICE_POLICY_SIZE_TRACKING_ENABLED; import static android.app.admin.flags.Flags.FLAG_HEADLESS_DEVICE_OWNER_PROVISIONING_FIX_ENABLED; import static android.app.admin.flags.Flags.FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED; -import static android.app.admin.flags.Flags.FLAG_SECURITY_LOG_V2_ENABLED; import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled; import static android.app.admin.flags.Flags.onboardingConsentlessBugreports; import static android.app.admin.flags.Flags.FLAG_IS_MTE_POLICY_ENFORCED; @@ -14335,7 +14334,6 @@ public class DevicePolicyManager { * @hide */ @SystemApi - @FlaggedApi(FLAG_SECURITY_LOG_V2_ENABLED) @RequiresPermission(permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public void setAuditLogEnabled(boolean enabled) { throwIfParentInstance("setAuditLogEnabled"); @@ -14352,7 +14350,6 @@ public class DevicePolicyManager { * @hide */ @SystemApi - @FlaggedApi(FLAG_SECURITY_LOG_V2_ENABLED) @RequiresPermission(permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public boolean isAuditLogEnabled() { throwIfParentInstance("isAuditLogEnabled"); @@ -14374,7 +14371,6 @@ public class DevicePolicyManager { * @hide */ @SystemApi - @FlaggedApi(FLAG_SECURITY_LOG_V2_ENABLED) @RequiresPermission(permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public void setAuditLogEventCallback( @NonNull @CallbackExecutor Executor executor, @@ -14401,7 +14397,6 @@ public class DevicePolicyManager { * @hide */ @SystemApi - @FlaggedApi(FLAG_SECURITY_LOG_V2_ENABLED) @RequiresPermission(permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public void clearAuditLogEventCallback() { throwIfParentInstance("clearAuditLogEventCallback"); diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 9148e3c3a072..56f47922b078 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -105,6 +105,7 @@ flag { bug: "289520697" } +# Fully rolled out and must not be used. flag { name: "security_log_v2_enabled" is_exported: true diff --git a/core/java/android/app/supervision/ISupervisionManager.aidl b/core/java/android/app/supervision/ISupervisionManager.aidl new file mode 100644 index 000000000000..8d25cad2fc67 --- /dev/null +++ b/core/java/android/app/supervision/ISupervisionManager.aidl @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2024, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.supervision; + +/** + * Internal IPC interface to the supervision service. + * {@hide} + */ +interface ISupervisionManager { + boolean isSupervisionEnabled(); +} diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java new file mode 100644 index 000000000000..8611a92074c0 --- /dev/null +++ b/core/java/android/app/supervision/SupervisionManager.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.supervision; + +import android.annotation.SystemService; +import android.compat.annotation.UnsupportedAppUsage; +import android.content.Context; +import android.os.RemoteException; + +/** + * Service for handling parental supervision. + * + * @hide + */ +@SystemService(Context.SUPERVISION_SERVICE) +public class SupervisionManager { + private final Context mContext; + private final ISupervisionManager mService; + + /** + * @hide + */ + @UnsupportedAppUsage + public SupervisionManager(Context context, ISupervisionManager service) { + mContext = context; + mService = service; + } + + /** + * Returns whether the device is supervised. + * + * @hide + */ + public boolean isSupervisionEnabled() { + try { + return mService.isSupervisionEnabled(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + +} diff --git a/core/java/android/app/supervision/flags.aconfig b/core/java/android/app/supervision/flags.aconfig new file mode 100644 index 000000000000..bcb5b3636c95 --- /dev/null +++ b/core/java/android/app/supervision/flags.aconfig @@ -0,0 +1,10 @@ +package: "android.app.supervision.flags" +container: "system" + +flag { + name: "supervision_api" + is_exported: true + namespace: "supervision" + description: "Flag to enable the SupervisionService" + bug: "340351729" +}
\ No newline at end of file diff --git a/core/java/android/companion/virtual/ActivityPolicyExemption.java b/core/java/android/companion/virtual/ActivityPolicyExemption.java index c81bb43ac1d3..dc285d4c08b7 100644 --- a/core/java/android/companion/virtual/ActivityPolicyExemption.java +++ b/core/java/android/companion/virtual/ActivityPolicyExemption.java @@ -118,6 +118,7 @@ public final class ActivityPolicyExemption implements Parcelable { /** * Builder for {@link ActivityPolicyExemption}. */ + @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API) public static final class Builder { private @Nullable ComponentName mComponentName; diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index ffcb1cbec94e..3bf0f0324716 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -106,6 +106,7 @@ import android.window.WindowContext; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.compat.IPlatformCompat; import com.android.internal.compat.IPlatformCompatNative; +import com.android.internal.protolog.ProtoLogConfigurationService; import java.io.File; import java.io.FileInputStream; @@ -6701,13 +6702,23 @@ public abstract class Context { /** * Use with {@link #getSystemService(String)} to retrieve the - * {@link com.android.internal.protolog.ProtoLogService} for registering ProtoLog clients. + * {@link ProtoLogConfigurationService} for registering ProtoLog clients. * * @see #getSystemService(String) - * @see com.android.internal.protolog.ProtoLogService + * @see ProtoLogConfigurationService * @hide */ - public static final String PROTOLOG_SERVICE = "protolog"; + public static final String PROTOLOG_CONFIGURATION_SERVICE = "protolog_configuration"; + + /** + * Use with {@link #getSystemService(String)} to retrieve a + * {@link android.app.supervision.SupervisionManager}. + * + * @see #getSystemService(String) + * @see android.app.supervision.SupervisionManager + * @hide + */ + public static final String SUPERVISION_SERVICE = "supervision"; /** * Determine whether the given permission is allowed for a particular diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index cb57c7bd565d..abb0d8d88cce 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -3767,7 +3767,7 @@ public class Intent implements Parcelable, Cloneable { * <p>The Intent will have the following extra value:</p> * <ul> * <li><em>{@link android.content.Intent#EXTRA_PHONE_NUMBER}</em> - - * the phone number originally intended to be dialed.</li> + * the phone number dialed.</li> * </ul> * <p class="note">Starting in Android 15, this broadcast is no longer sent as an ordered * broadcast. The <code>resultData</code> no longer has any effect and will not determine the @@ -3800,6 +3800,14 @@ public class Intent implements Parcelable, Cloneable { * {@link android.Manifest.permission#PROCESS_OUTGOING_CALLS} * permission to receive this Intent.</p> * + * <p class="note">Starting in {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, this broadcast is + * no longer sent as an ordered broadcast, and does not allow activity launches. This means + * that receivers may no longer change the phone number for the outgoing call, or cancel the + * outgoing call. This functionality is only possible using the + * {@link android.telecom.CallRedirectionService} API. Although background receivers are + * woken up to handle this intent, no guarantee is made as to the timeliness of the broadcast. + * </p> + * * <p class="note">This is a protected intent that can only be sent * by the system. * diff --git a/core/java/android/content/res/ColorStateList.java b/core/java/android/content/res/ColorStateList.java index 7b181176ae25..0a264e34b94a 100644 --- a/core/java/android/content/res/ColorStateList.java +++ b/core/java/android/content/res/ColorStateList.java @@ -26,6 +26,7 @@ import android.graphics.Color; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; import android.util.AttributeSet; import android.util.Log; import android.util.MathUtils; @@ -143,6 +144,7 @@ import java.util.List; * @attr ref android.R.styleable#ColorStateListItem_color * @attr ref android.R.styleable#ColorStateListItem_lStar */ +@RavenwoodKeepWholeClass public class ColorStateList extends ComplexColor implements Parcelable { private static final String TAG = "ColorStateList"; diff --git a/core/java/android/content/res/ComplexColor.java b/core/java/android/content/res/ComplexColor.java index 58c6fc5174d3..a385ee397f3b 100644 --- a/core/java/android/content/res/ComplexColor.java +++ b/core/java/android/content/res/ComplexColor.java @@ -18,13 +18,14 @@ package android.content.res; import android.annotation.ColorInt; import android.content.res.Resources.Theme; -import android.graphics.Color; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; /** * Defines an abstract class for the complex color information, like * {@link android.content.res.ColorStateList} or {@link android.content.res.GradientColor} * @hide */ +@RavenwoodKeepWholeClass public abstract class ComplexColor { private int mChangingConfigurations; diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index bf4d97d602d8..05596318aef5 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -1124,7 +1124,6 @@ public class Resources { */ @NonNull @Deprecated - @RavenwoodThrow(blockedBy = ColorStateList.class) public ColorStateList getColorStateList(@ColorRes int id) throws NotFoundException { final ColorStateList csl = getColorStateList(id, null); if (csl != null && csl.canApplyTheme()) { @@ -1155,7 +1154,6 @@ public class Resources { * color or multiple colors that can be selected based on a state. */ @NonNull - @RavenwoodThrow(blockedBy = ColorStateList.class) public ColorStateList getColorStateList(@ColorRes int id, @Nullable Theme theme) throws NotFoundException { final TypedValue value = obtainTempTypedValue(); @@ -1169,7 +1167,6 @@ public class Resources { } @NonNull - @RavenwoodThrow(blockedBy = ColorStateList.class) ColorStateList loadColorStateList(@NonNull TypedValue value, int id, @Nullable Theme theme) throws NotFoundException { return mResourcesImpl.loadColorStateList(this, value, id, theme); @@ -1179,7 +1176,6 @@ public class Resources { * @hide */ @NonNull - @RavenwoodThrow(blockedBy = ComplexColor.class) public ComplexColor loadComplexColor(@NonNull TypedValue value, int id, @Nullable Theme theme) { return mResourcesImpl.loadComplexColor(this, value, id, theme); } diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java index 90420dec64d1..e6b93427f413 100644 --- a/core/java/android/content/res/ResourcesImpl.java +++ b/core/java/android/content/res/ResourcesImpl.java @@ -1126,7 +1126,6 @@ public class ResourcesImpl { } @Nullable - @RavenwoodThrow(blockedBy = ComplexColor.class) ComplexColor loadComplexColor(Resources wrapper, @NonNull TypedValue value, int id, Resources.Theme theme) { if (TRACE_FOR_PRELOAD) { @@ -1168,7 +1167,6 @@ public class ResourcesImpl { } @NonNull - @RavenwoodThrow(blockedBy = ColorStateList.class) ColorStateList loadColorStateList(Resources wrapper, TypedValue value, int id, Resources.Theme theme) throws NotFoundException { diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java index f8eeaa93872a..79185a10e156 100644 --- a/core/java/android/content/res/TypedArray.java +++ b/core/java/android/content/res/TypedArray.java @@ -28,7 +28,6 @@ import android.graphics.drawable.Drawable; import android.os.Build; import android.os.StrictMode; import android.ravenwood.annotation.RavenwoodKeepWholeClass; -import android.ravenwood.annotation.RavenwoodReplace; import android.ravenwood.annotation.RavenwoodThrow; import android.util.AttributeSet; import android.util.DisplayMetrics; @@ -598,7 +597,6 @@ public class TypedArray implements AutoCloseable { * not an integer color or color state list. */ @Nullable - @RavenwoodThrow(blockedBy = ColorStateList.class) public ColorStateList getColorStateList(@StyleableRes int index) { if (mRecycled) { throw new RuntimeException("Cannot make calls to a recycled instance!"); diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index 48d27855db74..a60c48e0aa8c 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -80,7 +80,6 @@ import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -355,14 +354,7 @@ public class CameraDeviceImpl extends CameraDevice mCameraId = cameraId; if (Flags.singleThreadExecutor()) { mDeviceCallback = new ClientStateCallback(executor, callback); - mDeviceExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - Thread thread = Executors.defaultThreadFactory().newThread(r); - thread.setName("CameraDeviceExecutor"); - return thread; - } - }); + mDeviceExecutor = Executors.newSingleThreadExecutor(); } else { mDeviceCallback = callback; mDeviceExecutor = executor; diff --git a/core/java/android/hardware/radio/flags.aconfig b/core/java/android/hardware/radio/flags.aconfig index c9ab62ddc27f..c99dc049aac9 100644 --- a/core/java/android/hardware/radio/flags.aconfig +++ b/core/java/android/hardware/radio/flags.aconfig @@ -8,3 +8,11 @@ flag { description: "Feature flag for improved HD radio support with less vendor extensions" bug: "280300929" } + +flag { + name: "hd_radio_emergency_alert_system" + is_exported: true + namespace: "car_framework" + description: "Feature flag for HD radio emergency alert system support" + bug: "361348719" +} diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index c7751e3e5cea..c4d12d4336c6 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -1997,6 +1997,8 @@ public abstract class BatteryStats { STATE2_VIDEO_ON_FLAG | STATE2_FLASHLIGHT_FLAG | STATE2_CAMERA_FLAG | STATE2_GPS_SIGNAL_QUALITY_MASK; + public static final int GNSS_SIGNAL_QUALITY_NONE = 2; + @UnsupportedAppUsage public int states2; @@ -2220,7 +2222,7 @@ public abstract class BatteryStats { modemRailChargeMah = 0; wifiRailChargeMah = 0; states = 0; - states2 = 0; + states2 = GNSS_SIGNAL_QUALITY_NONE << HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT; wakelockTag = null; wakeReasonTag = null; eventCode = EVENT_NONE; diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index b7556dfb51af..4bc3dbedeb94 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -708,9 +708,16 @@ public class Binder implements IBinder { * * @hide */ + @android.ravenwood.annotation.RavenwoodReplace @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) public final native void markVintfStability(); + /** @hide */ + private void markVintfStability$ravenwood() { + // This is not useful for Ravenwood which uses local binder. + // TODO(b/361785059): Use real native libbinder. + } + /** * Use a VINTF-stability binder w/o VINTF requirements. Should be called * on a binder before it is sent out of process. diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index 4cc057a8e0ed..e80efd2a9380 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -41,7 +41,6 @@ import android.content.ContentResolver; import android.net.Uri; import android.os.MessageQueue.OnFileDescriptorEventListener; import android.ravenwood.annotation.RavenwoodKeepWholeClass; -import android.ravenwood.annotation.RavenwoodNativeSubstitutionClass; import android.ravenwood.annotation.RavenwoodReplace; import android.ravenwood.annotation.RavenwoodThrow; import android.system.ErrnoException; @@ -77,8 +76,6 @@ import java.nio.ByteOrder; * you to close it when done with it. */ @RavenwoodKeepWholeClass -@RavenwoodNativeSubstitutionClass( - "com.android.platform.test.ravenwood.nativesubstitution.ParcelFileDescriptor_host") public class ParcelFileDescriptor implements Parcelable, Closeable { private static final String TAG = "ParcelFileDescriptor"; @@ -206,11 +203,11 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { } mWrapped = null; mFd = fd; - setFdOwner(mFd); + IoUtils.setFdOwner(mFd, this); mCommFd = commChannel; if (mCommFd != null) { - setFdOwner(mCommFd); + IoUtils.setFdOwner(mCommFd, this); } mGuard.open("close"); @@ -298,7 +295,7 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { public static @NonNull ParcelFileDescriptor wrap(@NonNull ParcelFileDescriptor pfd, @NonNull Handler handler, @NonNull OnCloseListener listener) throws IOException { final FileDescriptor original = new FileDescriptor(); - setFdInt(original, pfd.detachFd()); + original.setInt$(pfd.detachFd()); return fromFd(original, handler, listener); } @@ -363,18 +360,10 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { } } - @RavenwoodReplace private static void closeInternal(FileDescriptor fd) { IoUtils.closeQuietly(fd); } - private static void closeInternal$ravenwood(FileDescriptor fd) { - try { - Os.close(fd); - } catch (ErrnoException ignored) { - } - } - /** * Create a new ParcelFileDescriptor that is a dup of an existing * FileDescriptor. This obeys standard POSIX semantics, where the @@ -385,7 +374,7 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { try { final FileDescriptor fd = new FileDescriptor(); int intfd = Os.fcntlInt(orig, (isAtLeastQ() ? F_DUPFD_CLOEXEC : F_DUPFD), 0); - setFdInt(fd, intfd); + fd.setInt$(intfd); return new ParcelFileDescriptor(fd); } catch (ErrnoException e) { throw e.rethrowAsIOException(); @@ -418,12 +407,12 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { */ public static ParcelFileDescriptor fromFd(int fd) throws IOException { final FileDescriptor original = new FileDescriptor(); - setFdInt(original, fd); + original.setInt$(fd); try { final FileDescriptor dup = new FileDescriptor(); int intfd = Os.fcntlInt(original, (isAtLeastQ() ? F_DUPFD_CLOEXEC : F_DUPFD), 0); - setFdInt(dup, intfd); + dup.setInt$(intfd); return new ParcelFileDescriptor(dup); } catch (ErrnoException e) { throw e.rethrowAsIOException(); @@ -446,7 +435,7 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { */ public static ParcelFileDescriptor adoptFd(int fd) { final FileDescriptor fdesc = new FileDescriptor(); - setFdInt(fdesc, fd); + fdesc.setInt$(fd); return new ParcelFileDescriptor(fdesc); } @@ -703,7 +692,7 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { @RavenwoodThrow(reason = "Os.readlink() and Os.stat()") public static File getFile(FileDescriptor fd) throws IOException { try { - final String path = Os.readlink("/proc/self/fd/" + getFdInt(fd)); + final String path = Os.readlink("/proc/self/fd/" + fd.getInt$()); if (OsConstants.S_ISREG(Os.stat(path).st_mode) || OsConstants.S_ISCHR(Os.stat(path).st_mode)) { return new File(path); @@ -783,7 +772,7 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { if (mClosed) { throw new IllegalStateException("Already closed"); } - return getFdInt(mFd); + return mFd.getInt$(); } } @@ -805,7 +794,7 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { if (mClosed) { throw new IllegalStateException("Already closed"); } - int fd = acquireRawFd(mFd); + int fd = IoUtils.acquireRawFd(mFd); writeCommStatusAndClose(Status.DETACHED, null); mClosed = true; mGuard.close(); @@ -1265,38 +1254,6 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { } } - private static native void setFdInt$ravenwood(FileDescriptor fd, int fdInt); - private static native int getFdInt$ravenwood(FileDescriptor fd); - - @RavenwoodReplace - private static void setFdInt(FileDescriptor fd, int fdInt) { - fd.setInt$(fdInt); - } - - @RavenwoodReplace - private static int getFdInt(FileDescriptor fd) { - return fd.getInt$(); - } - - @RavenwoodReplace - private void setFdOwner(FileDescriptor fd) { - IoUtils.setFdOwner(fd, this); - } - - private void setFdOwner$ravenwood(FileDescriptor fd) { - // FD owners currently unsupported under Ravenwood; ignored - } - - @RavenwoodReplace - private int acquireRawFd(FileDescriptor fd) { - return IoUtils.acquireRawFd(fd); - } - - private int acquireRawFd$ravenwood(FileDescriptor fd) { - // FD owners currently unsupported under Ravenwood; return FD directly - return getFdInt(fd); - } - @RavenwoodReplace private static boolean isAtLeastQ() { return (VMRuntime.getRuntime().getTargetSdkVersion() >= Build.VERSION_CODES.Q); diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index db06a6ba0ef5..3b2041b0d50a 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -838,16 +838,11 @@ public class Process { /** * Returns true if the current process is a 64-bit runtime. */ - @android.ravenwood.annotation.RavenwoodReplace + @android.ravenwood.annotation.RavenwoodKeep public static final boolean is64Bit() { return VMRuntime.getRuntime().is64Bit(); } - /** @hide */ - public static final boolean is64Bit$ravenwood() { - return "amd64".equals(System.getProperty("os.arch")); - } - private static volatile ThreadLocal<SomeArgs> sIdentity$ravenwood; /** @hide */ diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java index 17d2790eac96..013ec5f35761 100644 --- a/core/java/android/service/dreams/DreamOverlayService.java +++ b/core/java/android/service/dreams/DreamOverlayService.java @@ -28,7 +28,9 @@ import android.os.RemoteException; import android.util.Log; import android.view.WindowManager; +import java.lang.ref.WeakReference; import java.util.concurrent.Executor; +import java.util.function.Consumer; /** @@ -52,43 +54,51 @@ public abstract class DreamOverlayService extends Service { // An {@link IDreamOverlayClient} implementation that identifies itself when forwarding // requests to the {@link DreamOverlayService} private static class OverlayClient extends IDreamOverlayClient.Stub { - private final DreamOverlayService mService; + private final WeakReference<DreamOverlayService> mService; private boolean mShowComplications; private ComponentName mDreamComponent; IDreamOverlayCallback mDreamOverlayCallback; - OverlayClient(DreamOverlayService service) { + OverlayClient(WeakReference<DreamOverlayService> service) { mService = service; } + private void applyToDream(Consumer<DreamOverlayService> consumer) { + final DreamOverlayService service = mService.get(); + + if (service != null) { + consumer.accept(service); + } + } + @Override public void startDream(WindowManager.LayoutParams params, IDreamOverlayCallback callback, String dreamComponent, boolean shouldShowComplications) throws RemoteException { mDreamComponent = ComponentName.unflattenFromString(dreamComponent); mShowComplications = shouldShowComplications; mDreamOverlayCallback = callback; - mService.startDream(this, params); + applyToDream(dreamOverlayService -> dreamOverlayService.startDream(this, params)); } @Override public void wakeUp() { - mService.wakeUp(this); + applyToDream(dreamOverlayService -> dreamOverlayService.wakeUp(this)); } @Override public void endDream() { - mService.endDream(this); + applyToDream(dreamOverlayService -> dreamOverlayService.endDream(this)); } @Override public void comeToFront() { - mService.comeToFront(this); + applyToDream(dreamOverlayService -> dreamOverlayService.comeToFront(this)); } @Override public void onWakeRequested() { if (Flags.dreamWakeRedirect()) { - mService.onWakeRequested(); + applyToDream(DreamOverlayService::onWakeRequested); } } @@ -161,17 +171,24 @@ public abstract class DreamOverlayService extends Service { }); } - private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() { + private static class DreamOverlay extends IDreamOverlay.Stub { + private final WeakReference<DreamOverlayService> mService; + + DreamOverlay(DreamOverlayService service) { + mService = new WeakReference<>(service); + } + @Override public void getClient(IDreamOverlayClientCallback callback) { try { - callback.onDreamOverlayClient( - new OverlayClient(DreamOverlayService.this)); + callback.onDreamOverlayClient(new OverlayClient(mService)); } catch (RemoteException e) { Log.e(TAG, "could not send client to callback", e); } } - }; + } + + private final IDreamOverlay mDreamOverlay = new DreamOverlay(this); public DreamOverlayService() { } @@ -195,6 +212,12 @@ public abstract class DreamOverlayService extends Service { } } + @Override + public void onDestroy() { + mCurrentClient = null; + super.onDestroy(); + } + @Nullable @Override public final IBinder onBind(@NonNull Intent intent) { diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 0242de0b972c..c3585e3c5288 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -81,6 +81,7 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; import java.util.function.Consumer; /** @@ -1261,7 +1262,7 @@ public class DreamService extends Service implements Window.Callback { @Override public final IBinder onBind(Intent intent) { if (mDebug) Slog.v(mTag, "onBind() intent = " + intent); - mDreamServiceWrapper = new DreamServiceWrapper(); + mDreamServiceWrapper = new DreamServiceWrapper(new WeakReference<>(this)); final ComponentName overlayComponent = intent.getParcelableExtra( EXTRA_DREAM_OVERLAY_COMPONENT, ComponentName.class); @@ -1631,7 +1632,8 @@ public class DreamService extends Service implements Window.Callback { i.setComponent(mInjector.getDreamActivityComponent()); i.setPackage(mInjector.getDreamPackageName()); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION); - DreamActivity.setCallback(i, new DreamActivityCallbacks(mDreamToken)); + DreamActivity.setCallback(i, + new DreamActivityCallbacks(mDreamToken, new WeakReference<>(this))); final ServiceInfo serviceInfo = mInjector.getServiceInfo(); final CharSequence title = fetchDreamLabel(mInjector.getPackageManager(), mInjector.getResources(), serviceInfo, isPreviewMode); @@ -1845,22 +1847,37 @@ public class DreamService extends Service implements Window.Callback { * uses it to control the DreamService. It is also used to receive callbacks from the * DreamActivity. */ - final class DreamServiceWrapper extends IDreamService.Stub { + static final class DreamServiceWrapper extends IDreamService.Stub { + final WeakReference<DreamService> mService; + + DreamServiceWrapper(WeakReference<DreamService> service) { + mService = service; + } + + private void post(Consumer<DreamService> consumer) { + final DreamService service = mService.get(); + + if (service == null) { + return; + } + + service.mHandler.post(() -> consumer.accept(service)); + } + @Override public void attach(final IBinder dreamToken, final boolean canDoze, final boolean isPreviewMode, IRemoteCallback started) { - mHandler.post( - () -> DreamService.this.attach(dreamToken, canDoze, isPreviewMode, started)); + post(dreamService -> dreamService.attach(dreamToken, canDoze, isPreviewMode, started)); } @Override public void detach() { - mHandler.post(DreamService.this::detach); + post(DreamService::detach); } @Override public void wakeUp() { - mHandler.post(() -> DreamService.this.wakeUp(true /*fromSystem*/)); + post(dreamService -> dreamService.wakeUp(true /*fromSystem*/)); } @Override @@ -1868,48 +1885,70 @@ public class DreamService extends Service implements Window.Callback { if (!dreamHandlesBeingObscured()) { return; } + post(DreamService::comeToFront); + } + } - mHandler.post(DreamService.this::comeToFront); + private void onActivityCreated(DreamActivity activity, IBinder dreamToken) { + if (dreamToken != mDreamToken || mFinished) { + Slog.d(TAG, "DreamActivity was created after the dream was finished or " + + "a new dream started, finishing DreamActivity"); + if (!activity.isFinishing()) { + activity.finishAndRemoveTask(); + } + return; + } + if (mActivity != null) { + Slog.w(TAG, "A DreamActivity has already been started, " + + "finishing latest DreamActivity"); + if (!activity.isFinishing()) { + activity.finishAndRemoveTask(); + } + return; } + + mActivity = activity; + onWindowCreated(activity.getWindow()); + } + + private void onActivityDestroyed() { + mActivity = null; + mWindow = null; + detach(); } /** @hide */ @VisibleForTesting - public final class DreamActivityCallbacks extends Binder { + public static final class DreamActivityCallbacks extends Binder { private final IBinder mActivityDreamToken; + private WeakReference<DreamService> mService; - DreamActivityCallbacks(IBinder token) { + DreamActivityCallbacks(IBinder token, WeakReference<DreamService> service) { mActivityDreamToken = token; + mService = service; } /** Callback when the {@link DreamActivity} has been created */ public void onActivityCreated(DreamActivity activity) { - if (mActivityDreamToken != mDreamToken || mFinished) { - Slog.d(TAG, "DreamActivity was created after the dream was finished or " - + "a new dream started, finishing DreamActivity"); - if (!activity.isFinishing()) { - activity.finishAndRemoveTask(); - } - return; - } - if (mActivity != null) { - Slog.w(TAG, "A DreamActivity has already been started, " - + "finishing latest DreamActivity"); - if (!activity.isFinishing()) { - activity.finishAndRemoveTask(); - } + final DreamService service = mService.get(); + + if (service == null) { return; } - mActivity = activity; - onWindowCreated(activity.getWindow()); + service.onActivityCreated(activity, mActivityDreamToken); } /** Callback when the {@link DreamActivity} has been destroyed */ public void onActivityDestroyed() { - mActivity = null; - mWindow = null; - detach(); + final DreamService service = mService.get(); + + if (service == null) { + return; + } + + service.onActivityDestroyed(); + mService = null; } } diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 57acc71c20ea..918e591069fb 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -241,10 +241,11 @@ public class ZenModeConfig implements Parcelable { // ZenModeConfig XML versions distinguishing key changes. public static final int XML_VERSION_ZEN_UPGRADE = 8; public static final int XML_VERSION_MODES_API = 11; + public static final int XML_VERSION_MODES_UI = 12; - // TODO: b/310620812 - Update XML_VERSION and update default_zen_config.xml accordingly when - // modes_api is inlined. - private static final int XML_VERSION = 10; + // TODO: b/310620812, b/344831624 - Update XML_VERSION and update default_zen_config.xml + // accordingly when modes_api / modes_ui are inlined. + private static final int XML_VERSION_PRE_MODES = 10; public static final String ZEN_TAG = "zen"; private static final String ZEN_ATT_VERSION = "version"; private static final String ZEN_ATT_USER = "user"; @@ -952,7 +953,13 @@ public class ZenModeConfig implements Parcelable { } public static int getCurrentXmlVersion() { - return Flags.modesApi() ? XML_VERSION_MODES_API : XML_VERSION; + if (Flags.modesUi()) { + return XML_VERSION_MODES_UI; + } else if (Flags.modesApi()) { + return XML_VERSION_MODES_API; + } else { + return XML_VERSION_PRE_MODES; + } } public static ZenModeConfig readXml(TypedXmlPullParser parser) @@ -2607,7 +2614,7 @@ public class ZenModeConfig implements Parcelable { @AutomaticZenRule.Type public int type = AutomaticZenRule.TYPE_UNKNOWN; public String triggerDescription; - public String iconResName; + @Nullable public String iconResName; public boolean allowManualInvocation; @AutomaticZenRule.ModifiableField public int userModifiedFields; @ZenPolicy.ModifiableField public int zenPolicyUserModifiedFields; diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index 40070c7557bc..d33c95e06677 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -297,3 +297,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "typeface_redesign" + namespace: "text" + description: "Decouple variation settings, weight and style information from Typeface class" + bug: "361260253" +} diff --git a/core/java/android/util/LruCache.java b/core/java/android/util/LruCache.java index be1ec4187ddc..9845f9e7b804 100644 --- a/core/java/android/util/LruCache.java +++ b/core/java/android/util/LruCache.java @@ -18,7 +18,6 @@ package android.util; import android.compat.annotation.UnsupportedAppUsage; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; @@ -226,16 +225,10 @@ public class LruCache<K, V> { } } - @android.ravenwood.annotation.RavenwoodReplace private Map.Entry<K, V> eldest() { return map.eldest(); } - private Map.Entry<K, V> eldest$ravenwood() { - final Iterator<Map.Entry<K, V>> it = map.entrySet().iterator(); - return it.hasNext() ? it.next() : null; - } - /** * Removes the entry for {@code key} if it exists. * diff --git a/core/java/android/util/StateSet.java b/core/java/android/util/StateSet.java index 17adb32fb846..25f321ebb6b5 100644 --- a/core/java/android/util/StateSet.java +++ b/core/java/android/util/StateSet.java @@ -16,6 +16,8 @@ package android.util; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; + import com.android.internal.R; /** @@ -34,6 +36,7 @@ import com.android.internal.R; * and not have static methods here but there is some concern about * performance since these methods are called during view drawing. */ +@RavenwoodKeepWholeClass public class StateSet { /** * The order here is very important to diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index 15b0c13de524..1f7ed8be357c 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -2344,6 +2344,8 @@ public final class Display { * SurfaceControl.DisplayMode * @hide */ + @SuppressWarnings("UnflaggedApi") // For testing only + @TestApi public boolean isSynthetic() { return mIsSynthetic; } diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java index 8912035c0be3..ab9bd1fdfd72 100644 --- a/core/java/android/view/HandwritingInitiator.java +++ b/core/java/android/view/HandwritingInitiator.java @@ -730,13 +730,13 @@ public class HandwritingInitiator { /* The distance between point (x, y) and rect, there are 2 basic cases: * a) The distance is the distance from (x, y) to the closest corner on rect. - * o | | + * o | | * ---+-----+--- * | | * ---+-----+--- * | | * b) The distance is the distance from (x, y) to the closest edge on rect. - * | o | + * | o | * ---+-----+--- * | | * ---+-----+--- diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java index 6568912a82c0..91e9230cdc6a 100644 --- a/core/java/android/view/InsetsAnimationControlImpl.java +++ b/core/java/android/view/InsetsAnimationControlImpl.java @@ -35,8 +35,8 @@ import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_SHOWN import static android.view.InsetsController.LayoutInsetsDuringAnimation; import static android.view.InsetsSource.ID_IME; import static android.view.InsetsSource.SIDE_BOTTOM; -import static android.view.InsetsSource.SIDE_NONE; import static android.view.InsetsSource.SIDE_LEFT; +import static android.view.InsetsSource.SIDE_NONE; import static android.view.InsetsSource.SIDE_RIGHT; import static android.view.InsetsSource.SIDE_TOP; import static android.view.WindowInsets.Type.ime; @@ -100,6 +100,8 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro private @InsetsType int mControllingTypes; private final InsetsAnimationControlCallbacks mController; private final WindowInsetsAnimation mAnimation; + private final long mDurationMs; + private final Interpolator mInterpolator; /** @see WindowInsetsAnimationController#hasZeroInsetsIme */ private final boolean mHasZeroInsetsIme; private final CompatibilityInfo.Translator mTranslator; @@ -120,8 +122,8 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro @VisibleForTesting public InsetsAnimationControlImpl(SparseArray<InsetsSourceControl> controls, @Nullable Rect frame, InsetsState state, WindowInsetsAnimationControlListener listener, - @InsetsType int types, InsetsAnimationControlCallbacks controller, long durationMs, - Interpolator interpolator, @AnimationType int animationType, + @InsetsType int types, InsetsAnimationControlCallbacks controller, + InsetsAnimationSpec insetsAnimationSpec, @AnimationType int animationType, @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation, CompatibilityInfo.Translator translator, @Nullable ImeTracker.Token statsToken) { mControls = controls; @@ -155,8 +157,10 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro } mPendingInsets = mCurrentInsets; - mAnimation = new WindowInsetsAnimation(mTypes, interpolator, - durationMs); + mDurationMs = insetsAnimationSpec.getDurationMs(mHasZeroInsetsIme); + mInterpolator = insetsAnimationSpec.getInsetsInterpolator(mHasZeroInsetsIme); + + mAnimation = new WindowInsetsAnimation(mTypes, mInterpolator, mDurationMs); mAnimation.setAlpha(getCurrentAlpha()); mAnimationType = animationType; mLayoutInsetsDuringAnimation = layoutInsetsDuringAnimation; @@ -186,6 +190,16 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro } @Override + public long getDurationMs() { + return mDurationMs; + } + + @Override + public Interpolator getInsetsInterpolator() { + return mInterpolator; + } + + @Override public void setReadyDispatched(boolean dispatched) { mReadyDispatched = dispatched; } diff --git a/core/java/android/view/InsetsAnimationSpec.java b/core/java/android/view/InsetsAnimationSpec.java new file mode 100644 index 000000000000..7ad6661abffd --- /dev/null +++ b/core/java/android/view/InsetsAnimationSpec.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.view.animation.Interpolator; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * Used by {@link InsetsAnimationControlImpl} + * @hide + */ +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) +public interface InsetsAnimationSpec { + /** + * @param hasZeroInsetsIme whether IME has no insets (floating, fullscreen or non-overlapping). + * @return Duration of animation in {@link java.util.concurrent.TimeUnit#MILLISECONDS} + */ + long getDurationMs(boolean hasZeroInsetsIme); + /** + * @param hasZeroInsetsIme whether IME has no insets (floating, fullscreen or non-overlapping). + * @return The interpolator used for the animation + */ + Interpolator getInsetsInterpolator(boolean hasZeroInsetsIme); +} diff --git a/core/java/android/view/InsetsAnimationThreadControlRunner.java b/core/java/android/view/InsetsAnimationThreadControlRunner.java index 1b3b3ebbecfc..fc185bc73735 100644 --- a/core/java/android/view/InsetsAnimationThreadControlRunner.java +++ b/core/java/android/view/InsetsAnimationThreadControlRunner.java @@ -33,7 +33,6 @@ import android.view.InsetsController.LayoutInsetsDuringAnimation; import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsAnimation.Bounds; -import android.view.animation.Interpolator; import android.view.inputmethod.ImeTracker; /** @@ -110,15 +109,15 @@ public class InsetsAnimationThreadControlRunner implements InsetsAnimationContro @UiThread public InsetsAnimationThreadControlRunner(SparseArray<InsetsSourceControl> controls, @Nullable Rect frame, InsetsState state, WindowInsetsAnimationControlListener listener, - @InsetsType int types, InsetsAnimationControlCallbacks controller, long durationMs, - Interpolator interpolator, @AnimationType int animationType, + @InsetsType int types, InsetsAnimationControlCallbacks controller, + InsetsAnimationSpec insetsAnimationSpec, @AnimationType int animationType, @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation, CompatibilityInfo.Translator translator, Handler mainThreadHandler, @Nullable ImeTracker.Token statsToken) { mMainThreadHandler = mainThreadHandler; mOuterCallbacks = controller; mControl = new InsetsAnimationControlImpl(controls, frame, state, listener, types, - mCallbacks, durationMs, interpolator, animationType, layoutInsetsDuringAnimation, + mCallbacks, insetsAnimationSpec, animationType, layoutInsetsDuringAnimation, translator, statsToken); InsetsAnimationThread.getHandler().post(() -> { if (mControl.isCancelled()) { diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 7896cbde678a..b1df51f7affa 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -366,7 +366,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation * animate insets. */ public static class InternalAnimationControlListener - implements WindowInsetsAnimationControlListener { + implements WindowInsetsAnimationControlListener, InsetsAnimationSpec { private WindowInsetsAnimationController mController; private ValueAnimator mAnimator; @@ -374,7 +374,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation private final boolean mHasAnimationCallbacks; private final @InsetsType int mRequestedTypes; private final @Behavior int mBehavior; - private final long mDurationMs; private final boolean mDisable; private final int mFloatingImeBottomInset; private final WindowInsetsAnimationControlListener mLoggingListener; @@ -388,7 +387,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation mHasAnimationCallbacks = hasAnimationCallbacks; mRequestedTypes = requestedTypes; mBehavior = behavior; - mDurationMs = calculateDurationMs(); mDisable = disable; mFloatingImeBottomInset = floatingImeBottomInset; mLoggingListener = loggingListener; @@ -407,13 +405,14 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation onAnimationFinish(); return; } + final boolean hasZeroInsetsIme = controller.hasZeroInsetsIme(); mAnimator = ValueAnimator.ofFloat(0f, 1f); - mAnimator.setDuration(mDurationMs); + mAnimator.setDuration(controller.getDurationMs()); mAnimator.setInterpolator(new LinearInterpolator()); Insets hiddenInsets = controller.getHiddenStateInsets(); // IME with zero insets is a special case: it will animate-in from offscreen and end // with final insets of zero and vice-versa. - hiddenInsets = controller.hasZeroInsetsIme() + hiddenInsets = hasZeroInsetsIme ? Insets.of(hiddenInsets.left, hiddenInsets.top, hiddenInsets.right, mFloatingImeBottomInset) : hiddenInsets; @@ -423,7 +422,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation Insets end = mShow ? controller.getShownStateInsets() : hiddenInsets; - Interpolator insetsInterpolator = getInsetsInterpolator(); + Interpolator insetsInterpolator = controller.getInsetsInterpolator(); Interpolator alphaInterpolator = getAlphaInterpolator(); mAnimator.addUpdateListener(animation -> { float rawFraction = animation.getAnimatedFraction(); @@ -486,9 +485,10 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } } - protected Interpolator getInsetsInterpolator() { + @Override + public Interpolator getInsetsInterpolator(boolean hasZeroInsetsIme) { if ((mRequestedTypes & ime()) != 0) { - if (mHasAnimationCallbacks) { + if (mHasAnimationCallbacks && hasZeroInsetsIme) { return SYNC_IME_INTERPOLATOR; } else if (mShow) { return LINEAR_OUT_SLOW_IN_INTERPOLATOR; @@ -507,10 +507,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation Interpolator getAlphaInterpolator() { if ((mRequestedTypes & ime()) != 0) { - if (mHasAnimationCallbacks) { + if (mHasAnimationCallbacks && !mController.hasZeroInsetsIme()) { return input -> 1f; } else if (mShow) { - // Alpha animation takes half the time with linear interpolation; return input -> Math.min(1f, 2 * input); } else { @@ -534,16 +533,10 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation if (DEBUG) Log.d(TAG, "onAnimationFinish showOnFinish: " + mShow); } - /** - * To get the animation duration in MS. - */ - public long getDurationMs() { - return mDurationMs; - } - - private long calculateDurationMs() { + @Override + public long getDurationMs(boolean hasZeroInsetsIme) { if ((mRequestedTypes & ime()) != 0) { - if (mHasAnimationCallbacks) { + if (mHasAnimationCallbacks && hasZeroInsetsIme) { return ANIMATION_DURATION_SYNC_IME_MS; } else { return ANIMATION_DURATION_UNSYNC_IME_MS; @@ -593,13 +586,13 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation private static class PendingControlRequest { PendingControlRequest(@InsetsType int types, WindowInsetsAnimationControlListener listener, - long durationMs, Interpolator interpolator, @AnimationType int animationType, + InsetsAnimationSpec insetsAnimationSpec, + @AnimationType int animationType, @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation, CancellationSignal cancellationSignal, boolean useInsetsAnimationThread) { this.types = types; this.listener = listener; - this.durationMs = durationMs; - this.interpolator = interpolator; + this.mInsetsAnimationSpec = insetsAnimationSpec; this.animationType = animationType; this.layoutInsetsDuringAnimation = layoutInsetsDuringAnimation; this.cancellationSignal = cancellationSignal; @@ -608,8 +601,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation @InsetsType int types; final WindowInsetsAnimationControlListener listener; - final long durationMs; - final Interpolator interpolator; + final InsetsAnimationSpec mInsetsAnimationSpec; final @AnimationType int animationType; final @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation; final CancellationSignal cancellationSignal; @@ -1201,12 +1193,10 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation // We are about to playing the default animation. Passing a null frame indicates the // controlled types should be animated regardless of the frame. - controlAnimationUnchecked( - pendingRequest.types, pendingRequest.cancellationSignal, - pendingRequest.listener, null /* frame */, - true /* fromIme */, pendingRequest.durationMs, pendingRequest.interpolator, - pendingRequest.animationType, - pendingRequest.layoutInsetsDuringAnimation, + controlAnimationUnchecked(pendingRequest.types, pendingRequest.cancellationSignal, + pendingRequest.listener, null /* frame */, true /* fromIme */, + pendingRequest.mInsetsAnimationSpec, + pendingRequest.animationType, pendingRequest.layoutInsetsDuringAnimation, pendingRequest.useInsetsAnimationThread, statsToken); } @@ -1327,18 +1317,26 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation mHost.getInputMethodManager(), null /* icProto */); } + InsetsAnimationSpec spec = new InsetsAnimationSpec() { + @Override + public long getDurationMs(boolean hasZeroInsetsIme) { + return durationMs; + } + @Override + public Interpolator getInsetsInterpolator(boolean hasZeroInsetsIme) { + return interpolator; + } + }; // TODO(b/342111149): Create statsToken here once ImeTracker#onStart becomes async. - controlAnimationUnchecked(types, cancellationSignal, listener, mFrame, fromIme, durationMs, - interpolator, animationType, - getLayoutInsetsDuringAnimationMode(types, fromPredictiveBack), + controlAnimationUnchecked(types, cancellationSignal, listener, mFrame, fromIme, spec, + animationType, getLayoutInsetsDuringAnimationMode(types, fromPredictiveBack), false /* useInsetsAnimationThread */, null); } private void controlAnimationUnchecked(@InsetsType int types, @Nullable CancellationSignal cancellationSignal, WindowInsetsAnimationControlListener listener, @Nullable Rect frame, boolean fromIme, - long durationMs, Interpolator interpolator, - @AnimationType int animationType, + InsetsAnimationSpec insetsAnimationSpec, @AnimationType int animationType, @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation, boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken) { final boolean visible = layoutInsetsDuringAnimation == LAYOUT_INSETS_DURING_ANIMATION_SHOWN; @@ -1349,7 +1347,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation // However, we might reject the request in some cases, such as delaying showing IME or // rejecting showing IME. controlAnimationUncheckedInner(types, cancellationSignal, listener, frame, fromIme, - durationMs, interpolator, animationType, layoutInsetsDuringAnimation, + insetsAnimationSpec, animationType, layoutInsetsDuringAnimation, useInsetsAnimationThread, statsToken); // We are finishing setting the requested visible types. Report them to the server @@ -1360,8 +1358,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation private void controlAnimationUncheckedInner(@InsetsType int types, @Nullable CancellationSignal cancellationSignal, WindowInsetsAnimationControlListener listener, @Nullable Rect frame, boolean fromIme, - long durationMs, Interpolator interpolator, - @AnimationType int animationType, + InsetsAnimationSpec insetsAnimationSpec, @AnimationType int animationType, @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation, boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken) { if ((types & mTypesBeingCancelled) != 0) { @@ -1418,8 +1415,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation // TODO (b/323319146) remove layoutInsetsDuringAnimation from // PendingControlRequest, as it is now only used for showing final PendingControlRequest request = new PendingControlRequest(types, - listener, durationMs, - interpolator, animationType, LAYOUT_INSETS_DURING_ANIMATION_SHOWN, + listener, insetsAnimationSpec, animationType, + LAYOUT_INSETS_DURING_ANIMATION_SHOWN, cancellationSignal, false /* useInsetsAnimationThread */); mPendingImeControlRequest = request; // only add a timeout when the control is not currently showing @@ -1460,11 +1457,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation if (!imeReady) { // IME isn't ready, all requested types will be animated once IME is ready abortPendingImeControlRequest(); - final PendingControlRequest request = new PendingControlRequest(types, - listener, durationMs, - interpolator, animationType, layoutInsetsDuringAnimation, - cancellationSignal, - useInsetsAnimationThread); + final PendingControlRequest request = new PendingControlRequest(types, listener, + insetsAnimationSpec, animationType, layoutInsetsDuringAnimation, + cancellationSignal, useInsetsAnimationThread); mPendingImeControlRequest = request; mHandler.postDelayed(mPendingControlTimeout, PENDING_CONTROL_TIMEOUT_MS); if (DEBUG) Log.d(TAG, "Ime not ready. Create pending request"); @@ -1520,11 +1515,11 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation final InsetsAnimationControlRunner runner = useInsetsAnimationThread ? new InsetsAnimationThreadControlRunner(controls, - frame, mState, listener, typesReady, this, durationMs, interpolator, - animationType, layoutInsetsDuringAnimation, mHost.getTranslator(), - mHost.getHandler(), statsToken) + frame, mState, listener, typesReady, this, + insetsAnimationSpec, animationType, layoutInsetsDuringAnimation, + mHost.getTranslator(), mHost.getHandler(), statsToken) : new InsetsAnimationControlImpl(controls, - frame, mState, listener, typesReady, this, durationMs, interpolator, + frame, mState, listener, typesReady, this, insetsAnimationSpec, animationType, layoutInsetsDuringAnimation, mHost.getTranslator(), statsToken); if ((typesReady & WindowInsets.Type.ime()) != 0) { @@ -2023,7 +2018,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation // the controlled types should be animated regardless of the frame. controlAnimationUnchecked( types, null /* cancellationSignal */, listener, null /* frame */, fromIme, - listener.getDurationMs(), listener.getInsetsInterpolator(), + listener /* insetsAnimationSpec */, show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE, show ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN, !hasAnimationCallbacks /* useInsetsAnimationThread */, statsToken); diff --git a/core/java/android/view/InsetsResizeAnimationRunner.java b/core/java/android/view/InsetsResizeAnimationRunner.java index 6e6222187e49..f90b8411e333 100644 --- a/core/java/android/view/InsetsResizeAnimationRunner.java +++ b/core/java/android/view/InsetsResizeAnimationRunner.java @@ -233,6 +233,16 @@ public class InsetsResizeAnimationRunner implements InsetsAnimationControlRunner } @Override + public long getDurationMs() { + return 0; + } + + @Override + public Interpolator getInsetsInterpolator() { + return null; + } + + @Override public void setReadyDispatched(boolean dispatched) { } diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index 72d2d3b538b3..326e34b258e5 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -128,7 +128,16 @@ import java.util.Objects; * ev.getPointerId(p), ev.getX(p), ev.getY(p)); * } * } - * </code></pre></p> + * </code></pre></p><p> + * Developers should keep in mind that it is especially important to consume all samples + * in a batched event when processing relative values that report changes since the last + * event or sample. Examples of such relative axes include {@link #AXIS_RELATIVE_X}, + * {@link #AXIS_RELATIVE_Y}, and many of the axes prefixed with {@code AXIS_GESTURE_}. + * In these cases, developers should first consume all historical values using + * {@link #getHistoricalAxisValue(int, int)} and then consume the current values using + * {@link #getAxisValue(int)} like in the example above, as these relative values are + * not accumulated in a batched event. + * </p> * * <h3>Device Types</h3> * <p> @@ -1117,6 +1126,9 @@ public final class MotionEvent extends InputEvent implements Parcelable { * the location but this axis reports the difference which allows the app to see * how the mouse is moved. * </ul> + * </p><p> + * These values are relative to the state from the last sample, not accumulated, so developers + * should make sure to process this axis value for all batched historical samples. * </p> * * @see #getAxisValue(int, int) @@ -1130,6 +1142,9 @@ public final class MotionEvent extends InputEvent implements Parcelable { * Axis constant: The movement of y position of a motion event. * <p> * This is similar to {@link #AXIS_RELATIVE_X} but for y-axis. + * </p><p> + * These values are relative to the state from the last sample, not accumulated, so developers + * should make sure to process this axis value for all batched historical samples. * </p> * * @see #getAxisValue(int, int) @@ -1324,8 +1339,8 @@ public final class MotionEvent extends InputEvent implements Parcelable { * swipe gesture starts at X = 500 then moves to X = 400, this axis would have a value of * -0.1. * </ul> - * These values are relative to the state from the last event, not accumulated, so developers - * should make sure to process this axis value for all batched historical events. + * These values are relative to the state from the last sample, not accumulated, so developers + * should make sure to process this axis value for all batched historical samples. * <p> * This axis is only set on the first pointer in a motion event. */ @@ -1345,8 +1360,8 @@ public final class MotionEvent extends InputEvent implements Parcelable { * <li>For a touch pad, reports the distance that should be scrolled in the X axis as a result * of the user's two-finger scroll gesture, in display pixels. * </ul> - * These values are relative to the state from the last event, not accumulated, so developers - * should make sure to process this axis value for all batched historical events. + * These values are relative to the state from the last sample, not accumulated, so developers + * should make sure to process this axis value for all batched historical samples. * <p> * This axis is only set on the first pointer in a motion event. */ @@ -1367,8 +1382,8 @@ public final class MotionEvent extends InputEvent implements Parcelable { * making a pinch gesture, as a proportion of the previous distance. For example, if the fingers * were 50 units apart and are now 52 units apart, the scale factor would be 1.04. * </ul> - * These values are relative to the state from the last event, not accumulated, so developers - * should make sure to process this axis value for all batched historical events. + * These values are relative to the state from the last sample, not accumulated, so developers + * should make sure to process this axis value for all batched historical samples. * <p> * This axis is only set on the first pointer in a motion event. */ diff --git a/core/java/android/view/WindowInsetsAnimationController.java b/core/java/android/view/WindowInsetsAnimationController.java index 6578e9b6c20c..d3ea9829c680 100644 --- a/core/java/android/view/WindowInsetsAnimationController.java +++ b/core/java/android/view/WindowInsetsAnimationController.java @@ -23,6 +23,7 @@ import android.annotation.SuppressLint; import android.graphics.Insets; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsAnimation.Bounds; +import android.view.animation.Interpolator; /** * Controller for app-driven animation of system windows. @@ -188,4 +189,16 @@ public interface WindowInsetsAnimationController { * fullscreen or non-overlapping). */ boolean hasZeroInsetsIme(); + + /** + * @hide + * @return The duration of the animation in {@link java.util.concurrent.TimeUnit#MILLISECONDS}. + */ + long getDurationMs(); + + /** + * @hide + * @return The interpolator of the animation. + */ + Interpolator getInsetsInterpolator(); } diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index a5ba294d6a19..fe6aafbd7e16 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -982,6 +982,7 @@ public class AccessibilityNodeInfo implements Parcelable { private long mParentNodeId = UNDEFINED_NODE_ID; private long mLabelForId = UNDEFINED_NODE_ID; private long mLabeledById = UNDEFINED_NODE_ID; + private LongArray mLabeledByIds; private long mTraversalBefore = UNDEFINED_NODE_ID; private long mTraversalAfter = UNDEFINED_NODE_ID; @@ -3599,6 +3600,133 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Adds the view which serves as the label of the view represented by + * this info for accessibility purposes. When multiple labels are + * added, the content from each label is combined in the order that + * they are added. + * <p> + * If visible text can be used to describe or give meaning to this UI, + * this method is preferred. For example, a TextView before an EditText + * in the UI usually specifies what information is contained in the + * EditText. Hence, the EditText is labeled by the TextView. + * </p> + * + * @param label A view that labels this node's source. + */ + @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY) + public void addLabeledBy(@NonNull View label) { + addLabeledBy(label, AccessibilityNodeProvider.HOST_VIEW_ID); + } + + /** + * Adds the view which serves as the label of the view represented by + * this info for accessibility purposes. If <code>virtualDescendantId</code> + * is {@link View#NO_ID} the root is set as the label. When multiple + * labels are added, the content from each label is combined in the order + * that they are added. + * <p> + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report themselves as a tree of virtual views, thus conveying their + * logical structure. + * </p> + * <p> + * If visible text can be used to describe or give meaning to this UI, + * this method is preferred. For example, a TextView before an EditText + * in the UI usually specifies what information is contained in the + * EditText. Hence, the EditText is labeled by the TextView. + * </p> + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param root A root whose virtual descendant labels this node's source. + * @param virtualDescendantId The id of the virtual descendant. + */ + @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY) + public void addLabeledBy(@NonNull View root, int virtualDescendantId) { + enforceNotSealed(); + Preconditions.checkNotNull(root, "%s must not be null", root); + if (mLabeledByIds == null) { + mLabeledByIds = new LongArray(); + } + mLabeledById = makeNodeId(root.getAccessibilityViewId(), virtualDescendantId); + mLabeledByIds.add(mLabeledById); + } + + /** + * Gets the list of node infos which serve as the labels of the view represented by + * this info for accessibility purposes. + * + * @return The list of labels in the order that they were added. + */ + @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY) + public @NonNull List<AccessibilityNodeInfo> getLabeledByList() { + enforceSealed(); + List<AccessibilityNodeInfo> labels = new ArrayList<>(); + if (mLabeledByIds == null) { + return labels; + } + for (int i = 0; i < mLabeledByIds.size(); i++) { + labels.add(getNodeForAccessibilityId(mConnectionId, mWindowId, mLabeledByIds.get(i))); + } + return labels; + } + + /** + * Removes a label. If the label was not previously added to the node, + * calling this method has no effect. + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param label The node which serves as this node's label. + * @return true if the label was present + * @see #addLabeledBy(View) + */ + @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY) + public boolean removeLabeledBy(@NonNull View label) { + return removeLabeledBy(label, AccessibilityNodeProvider.HOST_VIEW_ID); + } + + /** + * Removes a label which is a virtual descendant of the given + * <code>root</code>. If <code>virtualDescendantId</code> is + * {@link View#NO_ID} the root is set as the label. If the label + * was not previously added to the node, calling this method has + * no effect. + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual node which serves as this node's label. + * @return true if the label was present + * @see #addLabeledBy(View, int) + */ + @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY) + public boolean removeLabeledBy(@NonNull View root, int virtualDescendantId) { + enforceNotSealed(); + final LongArray labeledByIds = mLabeledByIds; + if (labeledByIds == null) { + return false; + } + final int rootAccessibilityViewId = + (root != null) ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID; + final long labeledById = makeNodeId(rootAccessibilityViewId, virtualDescendantId); + if (mLabeledById == labeledById) { + mLabeledById = UNDEFINED_NODE_ID; + } + final int index = labeledByIds.indexOf(labeledById); + if (index < 0) { + return false; + } + labeledByIds.remove(index); + return true; + } + + /** * Sets the view which serves as the label of the view represented by * this info for accessibility purposes. * @@ -3631,7 +3759,17 @@ public class AccessibilityNodeInfo implements Parcelable { enforceNotSealed(); final int rootAccessibilityViewId = (root != null) ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID; + if (Flags.supportMultipleLabeledby()) { + if (mLabeledByIds == null) { + mLabeledByIds = new LongArray(); + } else { + mLabeledByIds.clear(); + } + } mLabeledById = makeNodeId(rootAccessibilityViewId, virtualDescendantId); + if (Flags.supportMultipleLabeledby()) { + mLabeledByIds.add(mLabeledById); + } } /** @@ -4242,6 +4380,10 @@ public class AccessibilityNodeInfo implements Parcelable { fieldIndex++; if (mLabeledById != DEFAULT.mLabeledById) nonDefaultFields |= bitAt(fieldIndex); fieldIndex++; + if (!LongArray.elementsEqual(mLabeledByIds, DEFAULT.mLabeledByIds)) { + nonDefaultFields |= bitAt(fieldIndex); + } + fieldIndex++; if (mTraversalBefore != DEFAULT.mTraversalBefore) nonDefaultFields |= bitAt(fieldIndex); fieldIndex++; if (mTraversalAfter != DEFAULT.mTraversalAfter) nonDefaultFields |= bitAt(fieldIndex); @@ -4383,6 +4525,18 @@ public class AccessibilityNodeInfo implements Parcelable { if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mParentNodeId); if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mLabelForId); if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mLabeledById); + if (isBitSet(nonDefaultFields, fieldIndex++)) { + final LongArray labeledByIds = mLabeledByIds; + if (labeledByIds == null) { + parcel.writeInt(0); + } else { + final int labeledByIdsSize = labeledByIds.size(); + parcel.writeInt(labeledByIdsSize); + for (int i = 0; i < labeledByIdsSize; i++) { + parcel.writeLong(labeledByIds.get(i)); + } + } + } if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mTraversalBefore); if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mTraversalAfter); if (isBitSet(nonDefaultFields, fieldIndex++)) { @@ -4550,6 +4704,7 @@ public class AccessibilityNodeInfo implements Parcelable { mParentNodeId = other.mParentNodeId; mLabelForId = other.mLabelForId; mLabeledById = other.mLabeledById; + mLabeledByIds = other.mLabeledByIds; mTraversalBefore = other.mTraversalBefore; mTraversalAfter = other.mTraversalAfter; mMinDurationBetweenContentChanges = other.mMinDurationBetweenContentChanges; @@ -4656,6 +4811,18 @@ public class AccessibilityNodeInfo implements Parcelable { if (isBitSet(nonDefaultFields, fieldIndex++)) mParentNodeId = parcel.readLong(); if (isBitSet(nonDefaultFields, fieldIndex++)) mLabelForId = parcel.readLong(); if (isBitSet(nonDefaultFields, fieldIndex++)) mLabeledById = parcel.readLong(); + if (isBitSet(nonDefaultFields, fieldIndex++)) { + final int labeledByIdsSize = parcel.readInt(); + if (labeledByIdsSize <= 0) { + mLabeledByIds = null; + } else { + mLabeledByIds = new LongArray(labeledByIdsSize); + for (int i = 0; i < labeledByIdsSize; i++) { + final long labeledById = parcel.readLong(); + mLabeledByIds.add(labeledById); + } + } + } if (isBitSet(nonDefaultFields, fieldIndex++)) mTraversalBefore = parcel.readLong(); if (isBitSet(nonDefaultFields, fieldIndex++)) mTraversalAfter = parcel.readLong(); if (isBitSet(nonDefaultFields, fieldIndex++)) { diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index b2810155098f..23d7732e469d 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -1365,10 +1365,10 @@ public final class InputMethodManager { ImeTracker.PHASE_CLIENT_HANDLE_SET_IME_VISIBILITY); if (visible) { insetsController.show(WindowInsets.Type.ime(), - false /* fromIme */, null /* statsToken */); + false /* fromIme */, statsToken); } else { insetsController.hide(WindowInsets.Type.ime(), - false /* fromIme */, null /* statsToken */); + false /* fromIme */, statsToken); } } } else { diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index fe2651030789..7366b9a443c3 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -497,25 +497,32 @@ public abstract class WebSettings { public abstract boolean getUseWebViewBackgroundForOverscrollBackground(); /** - * Sets whether the WebView should save form data. In Android O, the - * platform has implemented a fully functional Autofill feature to store - * form data. Therefore, the Webview form data save feature is disabled. + * Sets whether the WebView should save form data. In {@link android.os.Build.VERSION_CODES#O}, + * the platform has implemented a fully functional Autofill feature to store form data. + * Therefore, the Webview form data save feature is disabled. * - * Note that the feature will continue to be supported on older versions of + * <p>Note that the feature will continue to be supported on older versions of * Android as before. * - * @deprecated In Android O and afterwards, this function does not have - * any effect, the form data will be saved to platform's autofill service - * if applicable. + * @see #getSaveFormData + * @deprecated In Android O and afterwards, this function does not have any effect. Form data + * will be saved to platform's autofill service if applicable. */ @Deprecated public abstract void setSaveFormData(boolean save); /** - * Gets whether the WebView saves form data. + * Gets whether the WebView saves form data. In {@link android.os.Build.VERSION_CODES#O}, the + * platform has implemented a fully functional Autofill feature to store form data. Therefore, + * the Webview form data save feature is disabled. + * + * <p>Note that the feature will continue to be supported on older versions of + * Android as before. * * @return whether the WebView saves form data * @see #setSaveFormData + * @deprecated In Android O and afterwards, this function does not have any effect. Form data + * will be filled from the platform's autofill service if applicable. */ @Deprecated public abstract boolean getSaveFormData(); diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 72b268b440b5..1ea20fa85bd4 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -10627,7 +10627,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int startOffset = mLayout.getOffsetForHorizontal(line, point.x); if (mLayout.isLevelBoundary(startOffset)) { - // TODO(b/247551937): Support gesture at level boundaries. + // Gesture at level boundaries is not supported. return handleGestureFailure(gesture); } diff --git a/core/java/android/window/IBackAnimationRunner.aidl b/core/java/android/window/IBackAnimationRunner.aidl index b1d75826a948..a8017762dcc1 100644 --- a/core/java/android/window/IBackAnimationRunner.aidl +++ b/core/java/android/window/IBackAnimationRunner.aidl @@ -38,14 +38,13 @@ oneway interface IBackAnimationRunner { /** * Called when the system is ready for the handler to start animating all the visible tasks. * @param apps The list of departing (type=MODE_CLOSING) and entering (type=MODE_OPENING) - windows to animate, - * @param wallpapers The list of wallpapers to animate. - * @param nonApps The list of non-app windows such as Bubbles to animate. + * windows to animate, + * @param prepareOpenTransition If non-null, the animation should start after receive open + * transition * @param finishedCallback The callback to invoke when the animation is finished. */ void onAnimationStart( in RemoteAnimationTarget[] apps, - in RemoteAnimationTarget[] wallpapers, - in RemoteAnimationTarget[] nonApps, + in IBinder prepareOpenTransition, in IBackAnimationFinishedCallback finishedCallback) = 2; }
\ No newline at end of file diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 4f848175cd99..217bca77af0c 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -225,3 +225,13 @@ flag { description: "Adds a minimize button the the caption bar" bug: "356843241" } + +flag { + name: "skip_compat_ui_education_in_desktop_mode" + namespace: "lse_desktop_experience" + description: "Ignore Compat UI educations when in Desktop Mode." + bug: "357062954" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index be744fde96dd..67fc27042ff8 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -19,16 +19,6 @@ flag { } flag { - name: "do_not_skip_ime_by_target_visibility" - namespace: "windowing_frontend" - description: "Avoid window traversal missing IME" - bug: "339375944" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "apply_lifecycle_on_pip_change" namespace: "windowing_frontend" description: "Make pip activity lifecyle change with windowing mode" diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java index 4ccdf79da358..cf3a54b8437f 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java @@ -109,45 +109,13 @@ public final class AccessibilityTargetHelper { public static List<AccessibilityTarget> getInstalledTargets(Context context, @UserShortcutType int shortcutType) { final List<AccessibilityTarget> targets = new ArrayList<>(); - targets.addAll(getAccessibilityFilteredTargets(context, shortcutType)); + targets.addAll(getAccessibilityServiceTargets(context, shortcutType)); + targets.addAll(getAccessibilityActivityTargets(context, shortcutType)); targets.addAll(getAllowListingFeatureTargets(context, shortcutType)); return targets; } - private static List<AccessibilityTarget> getAccessibilityFilteredTargets(Context context, - @UserShortcutType int shortcutType) { - final List<AccessibilityTarget> serviceTargets = - getAccessibilityServiceTargets(context, shortcutType); - final List<AccessibilityTarget> activityTargets = - getAccessibilityActivityTargets(context, shortcutType); - - for (AccessibilityTarget activityTarget : activityTargets) { - serviceTargets.removeIf( - serviceTarget -> arePackageNameAndLabelTheSame(serviceTarget, activityTarget)); - } - - final List<AccessibilityTarget> targets = new ArrayList<>(); - targets.addAll(serviceTargets); - targets.addAll(activityTargets); - - return targets; - } - - private static boolean arePackageNameAndLabelTheSame(@NonNull AccessibilityTarget serviceTarget, - @NonNull AccessibilityTarget activityTarget) { - final ComponentName serviceComponentName = - ComponentName.unflattenFromString(serviceTarget.getId()); - final ComponentName activityComponentName = - ComponentName.unflattenFromString(activityTarget.getId()); - final boolean isSamePackageName = activityComponentName.getPackageName().equals( - serviceComponentName.getPackageName()); - final boolean isSameLabel = activityTarget.getLabel().equals( - serviceTarget.getLabel()); - - return isSamePackageName && isSameLabel; - } - private static List<AccessibilityTarget> getAccessibilityServiceTargets(Context context, @UserShortcutType int shortcutType) { final AccessibilityManager am = (AccessibilityManager) context.getSystemService( diff --git a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java index 1c26687c97e9..2e0ff3db6c50 100644 --- a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java +++ b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java @@ -34,6 +34,7 @@ import static com.android.internal.accessibility.common.ShortcutConstants.UserSh import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.NonNull; +import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Context; @@ -176,24 +177,19 @@ public final class ShortcutUtils { * @param type The shortcut type. * @return Mapping key in Settings. */ + @SuppressLint("SwitchIntDef") public static String convertToKey(@UserShortcutType int type) { - switch (type) { - case SOFTWARE: - return Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS; - case GESTURE: - return Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS; - case HARDWARE: - return Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE; - case TRIPLETAP: - return Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED; - case TWOFINGER_DOUBLETAP: - return Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED; - case QUICK_SETTINGS: - return Settings.Secure.ACCESSIBILITY_QS_TARGETS; - default: - throw new IllegalArgumentException( - "Unsupported user shortcut type: " + type); - } + return switch (type) { + case SOFTWARE -> Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS; + case GESTURE -> Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS; + case HARDWARE -> Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE; + case TRIPLETAP -> Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED; + case TWOFINGER_DOUBLETAP -> + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED; + case QUICK_SETTINGS -> Settings.Secure.ACCESSIBILITY_QS_TARGETS; + default -> throw new IllegalArgumentException( + "Unsupported user shortcut type: " + type); + }; } /** diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java index 0d0207ff7c0e..e14249cc028b 100644 --- a/core/java/com/android/internal/policy/PhoneWindow.java +++ b/core/java/com/android/internal/policy/PhoneWindow.java @@ -2513,9 +2513,16 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { } mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false); + + // For floating windows that are *allowed* to fill the screen (like Wear) content + // should still be wrapped if they're not explicitly requested as fullscreen. + final boolean isFloatingAndFullscreen = mIsFloating + && mAllowFloatingWindowsFillScreen + && a.getBoolean(R.styleable.Window_windowFullscreen, false); + int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR) & (~getForcedWindowFlags()); - if (mIsFloating && !mAllowFloatingWindowsFillScreen) { + if (mIsFloating && !isFloatingAndFullscreen) { setLayout(WRAP_CONTENT, WRAP_CONTENT); setFlags(0, flagsToUpdate); } else { diff --git a/core/java/com/android/internal/protolog/IProtoLogClient.aidl b/core/java/com/android/internal/protolog/IProtoLogClient.aidl index 969ed99d3aca..64944f42374b 100644 --- a/core/java/com/android/internal/protolog/IProtoLogClient.aidl +++ b/core/java/com/android/internal/protolog/IProtoLogClient.aidl @@ -20,7 +20,7 @@ package com.android.internal.protolog; * The ProtoLog client interface. * * These clients will communicate bi-directionally with the ProtoLog service - * (@see IProtoLogService.aidl) running in the system process. + * (@see IProtoLogConfigurationService.aidl) running in the system process. * * {@hide} */ diff --git a/core/java/com/android/internal/protolog/IProtoLogService.aidl b/core/java/com/android/internal/protolog/IProtoLogConfigurationService.aidl index cc349ea2985a..ce948281bbd6 100644 --- a/core/java/com/android/internal/protolog/IProtoLogService.aidl +++ b/core/java/com/android/internal/protolog/IProtoLogConfigurationService.aidl @@ -40,7 +40,7 @@ import com.android.internal.protolog.IProtoLogClient; * * {@hide} */ -interface IProtoLogService { +interface IProtoLogConfigurationService { interface IRegisterClientArgs { String[] getGroups(); boolean[] getGroupsDefaultLogcatStatus(); diff --git a/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java b/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java index b82c660fcf57..34e04181388d 100644 --- a/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java @@ -40,6 +40,8 @@ import java.util.List; */ @Deprecated public class LogcatOnlyProtoLogImpl implements IProtoLog { + private static final String LOG_TAG = LogcatOnlyProtoLogImpl.class.getName(); + @Override public void log(LogLevel logLevel, IProtoLogGroup group, long messageHash, int paramsMask, Object[] args) { @@ -48,19 +50,21 @@ public class LogcatOnlyProtoLogImpl implements IProtoLog { @Override public void log(LogLevel logLevel, IProtoLogGroup group, String messageString, Object[] args) { - if (REQUIRE_PROTOLOGTOOL) { - throw new RuntimeException( - "REQUIRE_PROTOLOGTOOL not set to false before the first log call."); + if (REQUIRE_PROTOLOGTOOL && group.isLogToProto()) { + Log.w(LOG_TAG, "ProtoLog message not processed. Failed to log it to proto. " + + "Logging it below to logcat instead."); } - String formattedString = TextUtils.formatSimple(messageString, args); - switch (logLevel) { - case VERBOSE -> Log.v(group.getTag(), formattedString); - case INFO -> Log.i(group.getTag(), formattedString); - case DEBUG -> Log.d(group.getTag(), formattedString); - case WARN -> Log.w(group.getTag(), formattedString); - case ERROR -> Log.e(group.getTag(), formattedString); - case WTF -> Log.wtf(group.getTag(), formattedString); + if (group.isLogToLogcat() || group.isLogToProto()) { + String formattedString = TextUtils.formatSimple(messageString, args); + switch (logLevel) { + case VERBOSE -> Log.v(group.getTag(), formattedString); + case INFO -> Log.i(group.getTag(), formattedString); + case DEBUG -> Log.d(group.getTag(), formattedString); + case WARN -> Log.w(group.getTag(), formattedString); + case ERROR -> Log.e(group.getTag(), formattedString); + case WTF -> Log.wtf(group.getTag(), formattedString); + } } } diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index 5517967341b5..b2fdf17f8564 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -16,7 +16,7 @@ package com.android.internal.protolog; -import static android.content.Context.PROTOLOG_SERVICE; +import static android.content.Context.PROTOLOG_CONFIGURATION_SERVICE; import static android.internal.perfetto.protos.InternedDataOuterClass.InternedData.PROTOLOG_STACKTRACE; import static android.internal.perfetto.protos.InternedDataOuterClass.InternedData.PROTOLOG_STRING_ARGS; import static android.internal.perfetto.protos.ProfileCommon.InternedString.IID; @@ -114,7 +114,7 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto private final Runnable mCacheUpdater; @Nullable // null when the flag android.tracing.client_side_proto_logging is not flipped - private final IProtoLogService mProtoLogService; + private final IProtoLogConfigurationService mProtoLogConfigurationService; @NonNull private final int[] mDefaultLogLevelCounts = new int[LogLevel.values().length]; @@ -186,30 +186,32 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto registerGroupsLocally(groups); if (android.tracing.Flags.clientSideProtoLogging()) { - mProtoLogService = - IProtoLogService.Stub.asInterface(ServiceManager.getService(PROTOLOG_SERVICE)); - Objects.requireNonNull(mProtoLogService, + mProtoLogConfigurationService = + IProtoLogConfigurationService.Stub.asInterface(ServiceManager.getService( + PROTOLOG_CONFIGURATION_SERVICE)); + Objects.requireNonNull(mProtoLogConfigurationService, "ServiceManager returned a null ProtoLog service"); try { - var args = new ProtoLogService.RegisterClientArgs(); + var args = new ProtoLogConfigurationService.RegisterClientArgs(); if (viewerConfigFilePath != null) { args.setViewerConfigFile(viewerConfigFilePath); } final var groupArgs = Stream.of(groups) - .map(group -> new ProtoLogService.RegisterClientArgs.GroupConfig( - group.name(), group.isLogToLogcat())) - .toArray(ProtoLogService.RegisterClientArgs.GroupConfig[]::new); + .map(group -> new ProtoLogConfigurationService.RegisterClientArgs + .GroupConfig(group.name(), group.isLogToLogcat())) + .toArray( + ProtoLogConfigurationService.RegisterClientArgs.GroupConfig[]::new); args.setGroups(groupArgs); - mProtoLogService.registerClient(this, args); + mProtoLogConfigurationService.registerClient(this, args); } catch (RemoteException e) { throw new RuntimeException("Failed to register ProtoLog client"); } } else { - mProtoLogService = null; + mProtoLogConfigurationService = null; } } diff --git a/core/java/com/android/internal/protolog/ProtoLog.java b/core/java/com/android/internal/protolog/ProtoLog.java index 660d3c99f538..bf77db7b6a33 100644 --- a/core/java/com/android/internal/protolog/ProtoLog.java +++ b/core/java/com/android/internal/protolog/ProtoLog.java @@ -63,6 +63,9 @@ public class ProtoLog { * @param groups The ProtoLog groups that will be used in the process. */ public static void init(IProtoLogGroup... groups) { + // These tracing instances are only used when we cannot or do not preprocess the source + // files to extract out the log strings. Otherwise, the trace calls are replaced with calls + // directly to the generated tracing implementations. if (android.tracing.Flags.perfettoProtologTracing()) { synchronized (sInitLock) { if (sProtoLogInstance != null) { @@ -76,8 +79,6 @@ public class ProtoLog { sProtoLogInstance = new PerfettoProtoLogImpl(groups); } } else { - // The first call to ProtoLog is likely to flip REQUIRE_PROTOLOGTOOL, which is when this - // static block will be executed before REQUIRE_PROTOLOGTOOL is actually set. sProtoLogInstance = new LogcatOnlyProtoLogImpl(); } } diff --git a/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java b/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java index 3dab2e39b852..82d8d3431a9d 100644 --- a/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java +++ b/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java @@ -29,18 +29,20 @@ import java.util.Set; public class ProtoLogCommandHandler extends ShellCommand { @NonNull - private final ProtoLogService mProtoLogService; + private final ProtoLogConfigurationService mProtoLogConfigurationService; @Nullable private final PrintWriter mPrintWriter; - public ProtoLogCommandHandler(@NonNull ProtoLogService protoLogService) { - this(protoLogService, null); + public ProtoLogCommandHandler( + @NonNull ProtoLogConfigurationService protoLogConfigurationService) { + this(protoLogConfigurationService, null); } @VisibleForTesting public ProtoLogCommandHandler( - @NonNull ProtoLogService protoLogService, @Nullable PrintWriter printWriter) { - this.mProtoLogService = protoLogService; + @NonNull ProtoLogConfigurationService protoLogConfigurationService, + @Nullable PrintWriter printWriter) { + this.mProtoLogConfigurationService = protoLogConfigurationService; this.mPrintWriter = printWriter; } @@ -94,7 +96,7 @@ public class ProtoLogCommandHandler extends ShellCommand { switch (cmd) { case "list": { - final String[] availableGroups = mProtoLogService.getGroups(); + final String[] availableGroups = mProtoLogConfigurationService.getGroups(); if (availableGroups.length == 0) { pw.println("No ProtoLog groups registered with ProtoLog service."); return 0; @@ -117,12 +119,13 @@ public class ProtoLogCommandHandler extends ShellCommand { pw.println("ProtoLog group " + group + "'s status:"); - if (!Set.of(mProtoLogService.getGroups()).contains(group)) { + if (!Set.of(mProtoLogConfigurationService.getGroups()).contains(group)) { pw.println("UNREGISTERED"); return 0; } - pw.println("LOG_TO_LOGCAT = " + mProtoLogService.isLoggingToLogcat(group)); + pw.println("LOG_TO_LOGCAT = " + + mProtoLogConfigurationService.isLoggingToLogcat(group)); return 0; } default: { @@ -142,11 +145,11 @@ public class ProtoLogCommandHandler extends ShellCommand { switch (cmd) { case "enable" -> { - mProtoLogService.enableProtoLogToLogcat(processGroups()); + mProtoLogConfigurationService.enableProtoLogToLogcat(processGroups()); return 0; } case "disable" -> { - mProtoLogService.disableProtoLogToLogcat(processGroups()); + mProtoLogConfigurationService.disableProtoLogToLogcat(processGroups()); return 0; } default -> { @@ -159,7 +162,7 @@ public class ProtoLogCommandHandler extends ShellCommand { @NonNull private String[] processGroups() { if (getRemainingArgsCount() == 0) { - return mProtoLogService.getGroups(); + return mProtoLogConfigurationService.getGroups(); } final List<String> groups = new ArrayList<>(); diff --git a/core/java/com/android/internal/protolog/ProtoLogService.java b/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java index 2333a062d897..176573870679 100644 --- a/core/java/com/android/internal/protolog/ProtoLogService.java +++ b/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java @@ -70,9 +70,9 @@ import java.util.TreeMap; * <p> * This service is intended to run on the system server, such that it never gets frozen. */ -@SystemService(Context.PROTOLOG_SERVICE) -public final class ProtoLogService extends IProtoLogService.Stub { - private static final String LOG_TAG = "ProtoLogService"; +@SystemService(Context.PROTOLOG_CONFIGURATION_SERVICE) +public final class ProtoLogConfigurationService extends IProtoLogConfigurationService.Stub { + private static final String LOG_TAG = "ProtoLogConfigurationService"; private final ProtoLogDataSource mDataSource = new ProtoLogDataSource( this::onTracingInstanceStart, @@ -114,12 +114,12 @@ public final class ProtoLogService extends IProtoLogService.Stub { private final ViewerConfigFileTracer mViewerConfigFileTracer; - public ProtoLogService() { - this(ProtoLogService::dumpTransitionTraceConfig); + public ProtoLogConfigurationService() { + this(ProtoLogConfigurationService::dumpTransitionTraceConfig); } @VisibleForTesting - public ProtoLogService(@NonNull ViewerConfigFileTracer tracer) { + public ProtoLogConfigurationService(@NonNull ViewerConfigFileTracer tracer) { // Initialize the Perfetto producer and register the Perfetto ProtoLog datasource to be // receive the lifecycle callbacks of the datasource and write the viewer configs if and // when required to the datasource. diff --git a/core/java/com/android/internal/protolog/ProtoLogDataSource.java b/core/java/com/android/internal/protolog/ProtoLogDataSource.java index 6dc6585b5caf..5c06b87f78ac 100644 --- a/core/java/com/android/internal/protolog/ProtoLogDataSource.java +++ b/core/java/com/android/internal/protolog/ProtoLogDataSource.java @@ -17,6 +17,7 @@ package com.android.internal.protolog; import static android.internal.perfetto.protos.ProtologConfig.ProtoLogConfig.DEFAULT; +import static android.internal.perfetto.protos.ProtologConfig.ProtoLogConfig.DEFAULT_LOG_FROM_LEVEL; import static android.internal.perfetto.protos.ProtologConfig.ProtoLogConfig.ENABLE_ALL; import static android.internal.perfetto.protos.ProtologConfig.ProtoLogConfig.GROUP_OVERRIDES; import static android.internal.perfetto.protos.ProtologConfig.ProtoLogConfig.TRACING_MODE; @@ -43,7 +44,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.function.Consumer; public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, ProtoLogDataSource.TlsState, @@ -190,73 +190,54 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, final Map<String, GroupConfig> groupConfigs = new HashMap<>(); while (configStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { - if (configStream.getFieldNumber() == (int) TRACING_MODE) { - int tracingMode = configStream.readInt(TRACING_MODE); - switch (tracingMode) { - case DEFAULT: - break; - case ENABLE_ALL: - defaultLogFromLevel = LogLevel.DEBUG; - break; - default: - throw new RuntimeException("Unhandled ProtoLog tracing mode type"); - } - } - if (configStream.getFieldNumber() == (int) GROUP_OVERRIDES) { - final long group_overrides_token = configStream.start(GROUP_OVERRIDES); - - String tag = null; - LogLevel logFromLevel = defaultLogFromLevel; - boolean collectStackTrace = false; - while (configStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { - if (configStream.getFieldNumber() == (int) GROUP_NAME) { - tag = configStream.readString(GROUP_NAME); + switch (configStream.getFieldNumber()) { + case (int) DEFAULT_LOG_FROM_LEVEL: + int defaultLogFromLevelInt = configStream.readInt(DEFAULT_LOG_FROM_LEVEL); + if (defaultLogFromLevelInt < defaultLogFromLevel.ordinal()) { + defaultLogFromLevel = + logLevelFromInt(configStream.readInt(DEFAULT_LOG_FROM_LEVEL)); } - if (configStream.getFieldNumber() == (int) LOG_FROM) { - final int logFromInt = configStream.readInt(LOG_FROM); - switch (logFromInt) { - case (ProtologCommon.PROTOLOG_LEVEL_DEBUG): { - logFromLevel = LogLevel.DEBUG; - break; - } - case (ProtologCommon.PROTOLOG_LEVEL_VERBOSE): { - logFromLevel = LogLevel.VERBOSE; - break; - } - case (ProtologCommon.PROTOLOG_LEVEL_INFO): { - logFromLevel = LogLevel.INFO; - break; - } - case (ProtologCommon.PROTOLOG_LEVEL_WARN): { - logFromLevel = LogLevel.WARN; - break; - } - case (ProtologCommon.PROTOLOG_LEVEL_ERROR): { - logFromLevel = LogLevel.ERROR; - break; - } - case (ProtologCommon.PROTOLOG_LEVEL_WTF): { - logFromLevel = LogLevel.WTF; - break; - } - default: { - throw new RuntimeException("Unhandled log level"); - } - } + break; + case (int) TRACING_MODE: + int tracingMode = configStream.readInt(TRACING_MODE); + switch (tracingMode) { + case DEFAULT: + break; + case ENABLE_ALL: + defaultLogFromLevel = LogLevel.DEBUG; + break; + default: + throw new RuntimeException("Unhandled ProtoLog tracing mode type"); } - if (configStream.getFieldNumber() == (int) COLLECT_STACKTRACE) { - collectStackTrace = configStream.readBoolean(COLLECT_STACKTRACE); + break; + case (int) GROUP_OVERRIDES: + final long group_overrides_token = configStream.start(GROUP_OVERRIDES); + + String tag = null; + LogLevel logFromLevel = defaultLogFromLevel; + boolean collectStackTrace = false; + while (configStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (configStream.getFieldNumber() == (int) GROUP_NAME) { + tag = configStream.readString(GROUP_NAME); + } + if (configStream.getFieldNumber() == (int) LOG_FROM) { + final int logFromInt = configStream.readInt(LOG_FROM); + logFromLevel = logLevelFromInt(logFromInt); + } + if (configStream.getFieldNumber() == (int) COLLECT_STACKTRACE) { + collectStackTrace = configStream.readBoolean(COLLECT_STACKTRACE); + } } - } - if (tag == null) { - throw new RuntimeException("Failed to decode proto config. " - + "Got a group override without a group tag."); - } + if (tag == null) { + throw new RuntimeException("Failed to decode proto config. " + + "Got a group override without a group tag."); + } - groupConfigs.put(tag, new GroupConfig(logFromLevel, collectStackTrace)); + groupConfigs.put(tag, new GroupConfig(logFromLevel, collectStackTrace)); - configStream.end(group_overrides_token); + configStream.end(group_overrides_token); + break; } } @@ -265,6 +246,18 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, return new ProtoLogConfig(defaultLogFromLevel, groupConfigs); } + private LogLevel logLevelFromInt(int logFromInt) { + return switch (logFromInt) { + case (ProtologCommon.PROTOLOG_LEVEL_DEBUG) -> LogLevel.DEBUG; + case (ProtologCommon.PROTOLOG_LEVEL_VERBOSE) -> LogLevel.VERBOSE; + case (ProtologCommon.PROTOLOG_LEVEL_INFO) -> LogLevel.INFO; + case (ProtologCommon.PROTOLOG_LEVEL_WARN) -> LogLevel.WARN; + case (ProtologCommon.PROTOLOG_LEVEL_ERROR) -> LogLevel.ERROR; + case (ProtologCommon.PROTOLOG_LEVEL_WTF) -> LogLevel.WTF; + default -> throw new RuntimeException("Unhandled log level"); + }; + } + public static class Instance extends DataSourceInstance { public interface TracingInstanceStartCallback { diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 1d43f6f083e6..c834dde56bfe 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -209,11 +209,6 @@ oneway interface IStatusBar void onDisplayReady(int displayId); /** - * Notifies System UI whether the recents animation is running or not. - */ - void onRecentsAnimationStateChanged(boolean running); - - /** * Notifies System UI side of system bar attribute change on the specified display. * * @param displayId the ID of the display to notify. diff --git a/core/java/com/android/internal/statusbar/StatusBarIcon.java b/core/java/com/android/internal/statusbar/StatusBarIcon.java index 76ce452858ad..f8a143693c83 100644 --- a/core/java/com/android/internal/statusbar/StatusBarIcon.java +++ b/core/java/com/android/internal/statusbar/StatusBarIcon.java @@ -16,6 +16,7 @@ package com.android.internal.statusbar; +import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.Parcel; import android.os.Parcelable; @@ -23,7 +24,18 @@ import android.os.UserHandle; import android.text.TextUtils; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +/** + * Representation of an icon that should appear in the status bar. + * + * <p>This includes notifications, conversations, and icons displayed on the right side (e.g. + * Wifi, Vibration/Silence, Priority Modes, etc). + * + * <p>This class is {@link Parcelable} but the {@link #preloadedIcon} is not (and will be lost if + * the object is copied through parcelling). If {@link #preloadedIcon} is supplied, it must match + * the {@link #icon} resource/bitmap. + */ public class StatusBarIcon implements Parcelable { public enum Type { // Notification: the sender avatar for important conversations @@ -34,7 +46,9 @@ public class StatusBarIcon implements Parcelable { // Notification: the small icon from the notification NotifSmallIcon, // The wi-fi, cellular or battery icon. - SystemIcon + SystemIcon, + // Some other icon, corresponding to a resource (possibly in a different package). + ResourceIcon } public UserHandle user; @@ -46,6 +60,13 @@ public class StatusBarIcon implements Parcelable { public CharSequence contentDescription; public Type type; + /** + * Optional {@link Drawable} corresponding to {@link #icon}. This field is not parcelable, so + * will be lost if the object is sent to a different process. If you set it, make sure to + * <em>also</em> set {@link #icon} pointing to the corresponding resource. + */ + @Nullable public Drawable preloadedIcon; + public StatusBarIcon(UserHandle user, String resPackage, Icon icon, int iconLevel, int number, CharSequence contentDescription, Type type) { if (icon.getType() == Icon.TYPE_RESOURCE @@ -88,6 +109,7 @@ public class StatusBarIcon implements Parcelable { StatusBarIcon that = new StatusBarIcon(this.user, this.pkg, this.icon, this.iconLevel, this.number, this.contentDescription, this.type); that.visible = this.visible; + that.preloadedIcon = this.preloadedIcon; return that; } diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java index 1e2cad41065d..1e965c5db7ae 100644 --- a/core/java/com/android/internal/util/ArrayUtils.java +++ b/core/java/com/android/internal/util/ArrayUtils.java @@ -49,81 +49,41 @@ public class ArrayUtils { private ArrayUtils() { /* cannot be instantiated */ } - @android.ravenwood.annotation.RavenwoodReplace public static byte[] newUnpaddedByteArray(int minLen) { return (byte[])VMRuntime.getRuntime().newUnpaddedArray(byte.class, minLen); } - @android.ravenwood.annotation.RavenwoodReplace public static char[] newUnpaddedCharArray(int minLen) { return (char[])VMRuntime.getRuntime().newUnpaddedArray(char.class, minLen); } - @android.ravenwood.annotation.RavenwoodReplace @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public static int[] newUnpaddedIntArray(int minLen) { return (int[])VMRuntime.getRuntime().newUnpaddedArray(int.class, minLen); } - @android.ravenwood.annotation.RavenwoodReplace public static boolean[] newUnpaddedBooleanArray(int minLen) { return (boolean[])VMRuntime.getRuntime().newUnpaddedArray(boolean.class, minLen); } - @android.ravenwood.annotation.RavenwoodReplace public static long[] newUnpaddedLongArray(int minLen) { return (long[])VMRuntime.getRuntime().newUnpaddedArray(long.class, minLen); } - @android.ravenwood.annotation.RavenwoodReplace public static float[] newUnpaddedFloatArray(int minLen) { return (float[])VMRuntime.getRuntime().newUnpaddedArray(float.class, minLen); } - @android.ravenwood.annotation.RavenwoodReplace public static Object[] newUnpaddedObjectArray(int minLen) { return (Object[])VMRuntime.getRuntime().newUnpaddedArray(Object.class, minLen); } - @android.ravenwood.annotation.RavenwoodReplace @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @SuppressWarnings("unchecked") public static <T> T[] newUnpaddedArray(Class<T> clazz, int minLen) { return (T[])VMRuntime.getRuntime().newUnpaddedArray(clazz, minLen); } - public static byte[] newUnpaddedByteArray$ravenwood(int minLen) { - return new byte[minLen]; - } - - public static char[] newUnpaddedCharArray$ravenwood(int minLen) { - return new char[minLen]; - } - - public static int[] newUnpaddedIntArray$ravenwood(int minLen) { - return new int[minLen]; - } - - public static boolean[] newUnpaddedBooleanArray$ravenwood(int minLen) { - return new boolean[minLen]; - } - - public static long[] newUnpaddedLongArray$ravenwood(int minLen) { - return new long[minLen]; - } - - public static float[] newUnpaddedFloatArray$ravenwood(int minLen) { - return new float[minLen]; - } - - public static Object[] newUnpaddedObjectArray$ravenwood(int minLen) { - return new Object[minLen]; - } - - public static <T> T[] newUnpaddedArray$ravenwood(Class<T> clazz, int minLen) { - return (T[]) Array.newInstance(clazz, minLen); - } - /** * Checks if the beginnings of two byte arrays are equal. * diff --git a/core/java/com/android/internal/widget/floatingtoolbar/LocalFloatingToolbarPopup.java b/core/java/com/android/internal/widget/floatingtoolbar/LocalFloatingToolbarPopup.java index bc729f10d74c..b6383d9f0754 100644 --- a/core/java/com/android/internal/widget/floatingtoolbar/LocalFloatingToolbarPopup.java +++ b/core/java/com/android/internal/widget/floatingtoolbar/LocalFloatingToolbarPopup.java @@ -437,9 +437,8 @@ public final class LocalFloatingToolbarPopup implements FloatingToolbarPopup { // Initialize x ensuring that the toolbar isn't rendered behind the nav bar in // landscape. - final int x = Math.min( - contentRectOnScreen.centerX() - mPopupWindow.getWidth() / 2, - mViewPortOnScreen.right - mPopupWindow.getWidth()); + final int x = Math.clamp(contentRectOnScreen.centerX() - mPopupWindow.getWidth() / 2, + mViewPortOnScreen.left, mViewPortOnScreen.right - mPopupWindow.getWidth()); final int y; diff --git a/core/jni/android_graphics_SurfaceTexture.cpp b/core/jni/android_graphics_SurfaceTexture.cpp index 50832a5c256a..8dd63cc07b8a 100644 --- a/core/jni/android_graphics_SurfaceTexture.cpp +++ b/core/jni/android_graphics_SurfaceTexture.cpp @@ -256,9 +256,21 @@ static void SurfaceTexture_classInit(JNIEnv* env, jclass clazz) } } -static void SurfaceTexture_init(JNIEnv* env, jobject thiz, jboolean isDetached, - jint texName, jboolean singleBufferMode, jobject weakThiz) -{ +static void SurfaceTexture_init(JNIEnv* env, jobject thiz, jboolean isDetached, jint texName, + jboolean singleBufferMode, jobject weakThiz) { +#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) + sp<SurfaceTexture> surfaceTexture; + if (isDetached) { + surfaceTexture = new SurfaceTexture(GL_TEXTURE_EXTERNAL_OES, true, !singleBufferMode); + } else { + surfaceTexture = + new SurfaceTexture(texName, GL_TEXTURE_EXTERNAL_OES, true, !singleBufferMode); + } + + if (singleBufferMode) { + surfaceTexture->setMaxBufferCount(1); + } +#else sp<IGraphicBufferProducer> producer; sp<IGraphicBufferConsumer> consumer; BufferQueue::createBufferQueue(&producer, &consumer); @@ -275,6 +287,7 @@ static void SurfaceTexture_init(JNIEnv* env, jobject thiz, jboolean isDetached, surfaceTexture = new SurfaceTexture(consumer, texName, GL_TEXTURE_EXTERNAL_OES, true, !singleBufferMode); } +#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) if (surfaceTexture == 0) { jniThrowException(env, OutOfResourcesException, @@ -287,11 +300,27 @@ static void SurfaceTexture_init(JNIEnv* env, jobject thiz, jboolean isDetached, createProcessUniqueId())); // If the current context is protected, inform the producer. +#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) + surfaceTexture->setConsumerIsProtected(isProtectedContext()); + + SurfaceTexture_setSurfaceTexture(env, thiz, surfaceTexture); + sp<Surface> surface = surfaceTexture->getSurface(); + if (nullptr == surface) { + jniThrowException(env, IllegalStateException, "Unable to get surface from SurfaceTexture"); + return; + } + sp<IGraphicBufferProducer> igbp = surface->getIGraphicBufferProducer(); + if (nullptr == igbp) { + jniThrowException(env, IllegalStateException, "Unable to get IGBP from Surface"); + return; + } + SurfaceTexture_setProducer(env, thiz, igbp); +#else consumer->setConsumerIsProtected(isProtectedContext()); SurfaceTexture_setSurfaceTexture(env, thiz, surfaceTexture); SurfaceTexture_setProducer(env, thiz, producer); - +#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) jclass clazz = env->GetObjectClass(thiz); if (clazz == NULL) { jniThrowRuntimeException(env, diff --git a/core/proto/android/app/appstartinfo.proto b/core/proto/android/app/appstartinfo.proto index 8de54586ab73..78cf6f464558 100644 --- a/core/proto/android/app/appstartinfo.proto +++ b/core/proto/android/app/appstartinfo.proto @@ -40,4 +40,5 @@ message ApplicationStartInfoProto { optional bytes start_intent = 10; optional AppStartLaunchMode launch_mode = 11; optional bool was_force_stopped = 12; + optional int64 monotonic_creation_time_ms = 13; } diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto index 58f39a9208da..42c591be60cc 100644 --- a/core/proto/android/server/activitymanagerservice.proto +++ b/core/proto/android/server/activitymanagerservice.proto @@ -1045,7 +1045,7 @@ message AppsExitInfoProto { repeated Package packages = 2; } -// sync with com.android.server.am.am.ProcessList.java +// LINT.IfChange message AppsStartInfoProto { option (.android.msg_privacy).dest = DEST_AUTOMATIC; @@ -1064,4 +1064,6 @@ message AppsStartInfoProto { repeated User users = 2; } repeated Package packages = 2; + optional int64 monotonic_time = 3; } +// LINT.ThenChange(/services/core/java/com/android/server/am/AppStartInfoTracker.java) diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 2dd560c69a8c..91c33702d3e3 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3836,7 +3836,6 @@ <!-- Allows an application to use audit logging API. @hide @SystemApi - @FlaggedApi("android.app.admin.flags.security_log_v2_enabled") --> <permission android:name="android.permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING" android:protectionLevel="internal|role" /> diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index 118acacc5e52..a7240ffc6012 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -443,4 +443,20 @@ <!-- Telephony satellite gateway intent for handling carrier roaming to satellite is using ESOS messaging. --> <string name="config_satellite_carrier_roaming_esos_provisioned_intent_action" translatable="false"></string> <java-symbol type="string" name="config_satellite_carrier_roaming_esos_provisioned_intent_action" /> + + <!-- The time duration in minutes to wait before retry validating a possible change + in satellite allowed region. The default value is 10 minutes. --> + <integer name="config_satellite_delay_minutes_before_retry_validating_possible_change_in_allowed_region">10</integer> + <java-symbol type="integer" name="config_satellite_delay_minutes_before_retry_validating_possible_change_in_allowed_region" /> + + <!-- The maximum retry count to validate a possible change in satellite allowed region. + The default value is 3 minutes. --> + <integer name="config_satellite_max_retry_count_for_validating_possible_change_in_allowed_region">3</integer> + <java-symbol type="integer" name="config_satellite_max_retry_count_for_validating_possible_change_in_allowed_region" /> + + <!-- The time duration in minutes for location query throttle interval. + The default value is 10 minutes. --> + <integer name="config_satellite_location_query_throttle_interval_minutes">10</integer> + <java-symbol type="integer" name="config_satellite_location_query_throttle_interval_minutes" /> + </resources> diff --git a/core/res/res/values/config_tv_external_input_logging.xml b/core/res/res/values/config_tv_external_input_logging.xml index 72e30be00a02..293a183ffeca 100644 --- a/core/res/res/values/config_tv_external_input_logging.xml +++ b/core/res/res/values/config_tv_external_input_logging.xml @@ -24,27 +24,39 @@ entries do not follow the convention, but all new entries should. --> <resources> - <bool name="config_tvExternalInputLoggingDisplayNameFilterEnabled">false</bool> + <bool name="config_tvExternalInputLoggingDisplayNameFilterEnabled">true</bool> <string-array name="config_tvExternalInputLoggingDeviceOnScreenDisplayNames"> - <item>Chromecast</item> + <item>ADT-4</item> <item>Chromecast HD</item> - <item>SHIELD</item> - <item>Roku</item> - <item>Roku Express 4</item> - <item>Home Theater</item> <item>Fire TV Stick</item> - <item>PlayStation 5</item> + <item>Freebox Player</item> + <item>Home Theater</item> + <item>Jarvis</item> <item>NintendoSwitch</item> + <item>onn. 4K Plus S</item> + <item>onn. Streaming</item> + <item>PlayStation 4</item> + <item>PlayStation 5</item> + <item>Roku 3</item> + <item>Roku Express 4</item> </string-array> <string-array name="config_tvExternalInputLoggingDeviceBrandNames"> - <item>Chromecast</item> - <item>SHIELD</item> - <item>Roku</item> <item>Apple</item> + <item>Chromecast</item> <item>Fire TV</item> - <item>PlayStation</item> + <item>Freebox</item> + <item>Google</item> + <item>MiBOX</item> + <item>Microsoft</item> <item>Nintendo</item> + <item>NVIDIA</item> + <item>onn.</item> + <item>PlayStation</item> + <item>Roku</item> + <item>SHIELD</item> + <item>Sony</item> + <item>XBOX</item> </string-array> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 9a52bd4d266e..cf0fc6159f42 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5466,6 +5466,8 @@ <!-- For HapticFeedbackConstants configurability defined at HapticFeedbackCustomization --> <java-symbol type="string" name="config_hapticFeedbackCustomizationFile" /> <java-symbol type="xml" name="haptic_feedback_customization" /> + <java-symbol type="xml" name="haptic_feedback_customization_source_rotary_encoder" /> + <java-symbol type="xml" name="haptic_feedback_customization_source_touchscreen" /> <!-- For ActivityManager PSS profiling configurability --> <java-symbol type="bool" name="config_am_disablePssProfiling" /> diff --git a/core/res/res/xml/haptic_feedback_customization_source_rotary_encoder.xml b/core/res/res/xml/haptic_feedback_customization_source_rotary_encoder.xml new file mode 100644 index 000000000000..7ac0787ab7a0 --- /dev/null +++ b/core/res/res/xml/haptic_feedback_customization_source_rotary_encoder.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<haptic-feedback-constants/> diff --git a/core/res/res/xml/haptic_feedback_customization_source_touchscreen.xml b/core/res/res/xml/haptic_feedback_customization_source_touchscreen.xml new file mode 100644 index 000000000000..7ac0787ab7a0 --- /dev/null +++ b/core/res/res/xml/haptic_feedback_customization_source_touchscreen.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<haptic-feedback-constants/> diff --git a/core/tests/coretests/src/android/util/StateSetTest.java b/core/tests/coretests/src/android/util/StateSetTest.java index 14e4e2000a65..c9df83d84f3e 100644 --- a/core/tests/coretests/src/android/util/StateSetTest.java +++ b/core/tests/coretests/src/android/util/StateSetTest.java @@ -19,7 +19,6 @@ package android.util; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import android.platform.test.annotations.IgnoreUnderRavenwood; import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -33,7 +32,6 @@ import org.junit.runner.RunWith; * Tests for {@link StateSet} */ @RunWith(AndroidJUnit4.class) -@IgnoreUnderRavenwood(blockedBy = StateSet.class) public class StateSetTest { @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule(); diff --git a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java index 668487dcc490..786f1e84728d 100644 --- a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java +++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java @@ -40,6 +40,7 @@ import android.platform.test.annotations.Presubmit; import android.util.SparseArray; import android.view.SurfaceControl.Transaction; import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; +import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -117,11 +118,21 @@ public class InsetsAnimationControlImplTest { SparseArray<InsetsSourceControl> controls = new SparseArray<>(); controls.put(ID_STATUS_BAR, topConsumer.getControl()); controls.put(ID_NAVIGATION_BAR, navConsumer.getControl()); + InsetsAnimationSpec spec = new InsetsAnimationSpec() { + @Override + public long getDurationMs(boolean hasZeroInsetsIme) { + return 10; + } + @Override + public Interpolator getInsetsInterpolator(boolean hasZeroInsetsIme) { + return new LinearInterpolator(); + } + }; + mController = new InsetsAnimationControlImpl(controls, new Rect(0, 0, 500, 500), mInsetsState, mMockListener, systemBars(), - mMockController, 10 /* durationMs */, new LinearInterpolator(), - 0 /* animationType */, 0 /* layoutInsetsDuringAnimation */, null /* translator */, - null /* statsToken */); + mMockController, spec /* insetsAnimationSpecCreator */, 0 /* animationType */, + 0 /* layoutInsetsDuringAnimation */, null /* translator */, null /* statsToken */); mController.setReadyDispatched(true); } diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java index 7bc0d2f2fcd3..ce7e85868e8c 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -69,6 +69,7 @@ import android.view.WindowInsetsController.OnControllableInsetsChangedListener; import android.view.WindowManager.BadTokenException; import android.view.WindowManager.LayoutParams; import android.view.animation.LinearInterpolator; +import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; import android.widget.TextView; @@ -136,7 +137,7 @@ public class InsetsControllerTest { mTestHandler = new TestHandler(null, mTestClock); mTestHost = spy(new TestHost(mViewRoot)); mController = new InsetsController(mTestHost, (controller, id, type) -> { - if (type == ime()) { + if (!Flags.refactorInsetsController() && type == ime()) { return new InsetsSourceConsumer(id, type, controller.getState(), Transaction::new, controller) { @@ -260,7 +261,11 @@ public class InsetsControllerTest { mController.setSystemDrivenInsetsAnimationLoggingListener(loggingListener); mController.getSourceConsumer(ID_IME, ime()).onWindowFocusGained(true); // since there is no focused view, forcefully make IME visible. - mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + if (!Flags.refactorInsetsController()) { + mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + } else { + mController.show(ime(), false /* fromIme */, ImeTracker.Token.empty()); + } // When using the animation thread, this must not invoke onReady() mViewRoot.getView().getViewTreeObserver().dispatchOnPreDraw(); }); @@ -277,7 +282,12 @@ public class InsetsControllerTest { InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { mController.getSourceConsumer(ID_IME, ime()).onWindowFocusGained(true); // since there is no focused view, forcefully make IME visible. - mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + if (!Flags.refactorInsetsController()) { + mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + } else { + InsetsSourceControl ime = createControl(ID_IME, ime()); + mController.onControlsChanged(new InsetsSourceControl[]{ime}); + } mController.show(all()); // quickly jump to final state by cancelling it. mController.cancelExistingAnimations(); @@ -299,7 +309,11 @@ public class InsetsControllerTest { mController.onControlsChanged(new InsetsSourceControl[] { ime }); InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { mController.getSourceConsumer(ID_IME, ime()).onWindowFocusGained(true); - mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + if (!Flags.refactorInsetsController()) { + mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + } else { + mController.show(ime(), false /* fromIme */, ImeTracker.Token.empty()); + } mController.cancelExistingAnimations(); assertTrue(isRequestedVisible(mController, ime())); mController.hide(ime(), true /* fromIme */, ImeTracker.Token.empty()); @@ -469,7 +483,12 @@ public class InsetsControllerTest { assertFalse(mController.getState().peekSource(ID_IME).isVisible()); // Pretend IME is calling - mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + if (!Flags.refactorInsetsController()) { + mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + } else { + InsetsSourceControl ime = createControl(ID_IME, ime()); + mController.onControlsChanged(new InsetsSourceControl[]{ime}); + } // Gaining control shortly after mController.onControlsChanged(createSingletonControl(ID_IME, ime())); @@ -493,7 +512,12 @@ public class InsetsControllerTest { mController.onControlsChanged(createSingletonControl(ID_IME, ime())); // Pretend IME is calling - mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + if (!Flags.refactorInsetsController()) { + mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + } else { + InsetsSourceControl ime = createControl(ID_IME, ime()); + mController.onControlsChanged(new InsetsSourceControl[]{ime}); + } assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ime())); mController.cancelExistingAnimations(); @@ -558,7 +582,13 @@ public class InsetsControllerTest { @Test public void testControlImeNotReady() { - prepareControls(); + if (!Flags.refactorInsetsController()) { + prepareControls(); + } else { + // With the flag on, the IME control should not contain a leash, otherwise the custom + // animation will start immediately. + prepareControls(false /* imeControlHasLeash */); + } InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { WindowInsetsAnimationControlListener listener = mock(WindowInsetsAnimationControlListener.class); @@ -571,7 +601,13 @@ public class InsetsControllerTest { verify(listener, never()).onReady(any(), anyInt()); // Pretend that IME is calling. - mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + if (!Flags.refactorInsetsController()) { + mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + } else { + // Send the IME control with leash, so that the animation can start + InsetsSourceControl ime = createControl(ID_IME, ime(), true /* hasLeash */); + mController.onControlsChanged(new InsetsSourceControl[]{ime}); + } // Ready gets deferred until next predraw mViewRoot.getView().getViewTreeObserver().dispatchOnPreDraw(); @@ -583,7 +619,13 @@ public class InsetsControllerTest { @Test public void testControlImeNotReady_controlRevoked() { - prepareControls(); + if (!Flags.refactorInsetsController()) { + prepareControls(); + } else { + // With the flag on, the IME control should not contain a leash, otherwise the custom + // animation will start immediately. + prepareControls(false /* imeControlHasLeash */); + } InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { WindowInsetsAnimationControlListener listener = mock(WindowInsetsAnimationControlListener.class); @@ -604,7 +646,13 @@ public class InsetsControllerTest { @Test public void testControlImeNotReady_timeout() { - prepareControls(); + if (!Flags.refactorInsetsController()) { + prepareControls(); + } else { + // With the flag on, the IME control should not contain a leash, otherwise the custom + // animation will start immediately. + prepareControls(false /* imeControlHasLeash */); + } InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { WindowInsetsAnimationControlListener listener = mock(WindowInsetsAnimationControlListener.class); @@ -655,7 +703,11 @@ public class InsetsControllerTest { mController.onControlsChanged(createSingletonControl(ID_IME, ime())); // Pretend IME is calling - mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + if (!Flags.refactorInsetsController()) { + mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + } else { + mController.show(ime(), false /* fromIme */, ImeTracker.Token.empty()); + } InsetsState copy = new InsetsState(mController.getState(), true /* copySources */); copy.peekSource(ID_IME).setFrame(0, 1, 2, 3); @@ -886,7 +938,11 @@ public class InsetsControllerTest { // Showing invisible ime should only causes insets change once. clearInvocations(mTestHost); - mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + if (!Flags.refactorInsetsController()) { + mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + } else { + mController.show(ime(), false /* fromIme */, ImeTracker.Token.empty()); + } verify(mTestHost, times(1)).notifyInsetsChanged(); // Sending the same insets state should not cause insets change. @@ -953,7 +1009,11 @@ public class InsetsControllerTest { assertNull(imeInsetsConsumer.getControl()); // Verify IME requested visibility should be updated to IME consumer from controller. - mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + if (!Flags.refactorInsetsController()) { + mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + } else { + mController.show(ime(), false /* fromIme */, ImeTracker.Token.empty()); + } assertTrue(isRequestedVisible(mController, ime())); mController.hide(ime()); @@ -966,7 +1026,11 @@ public class InsetsControllerTest { prepareControls(); InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { // show ime as initial state - mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + if (!Flags.refactorInsetsController()) { + mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + } else { + mController.show(ime(), false /* fromIme */, ImeTracker.Token.empty()); + } mController.cancelExistingAnimations(); // fast forward show animation assertTrue(mController.getState().peekSource(ID_IME).isVisible()); @@ -990,8 +1054,13 @@ public class InsetsControllerTest { public void testImeShowRequestCancelsPredictiveBackPostCommitAnim() { prepareControls(); InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + InsetsSourceControl ime = createControl(ID_IME, ime()); // show ime as initial state - mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + if (!Flags.refactorInsetsController()) { + mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + } else { + mController.show(ime(), false /* fromIme */, ImeTracker.Token.empty()); + } mController.cancelExistingAnimations(); // fast forward show animation mViewRoot.getView().getViewTreeObserver().dispatchOnPreDraw(); assertTrue(mController.getState().peekSource(ID_IME).isVisible()); @@ -1008,12 +1077,20 @@ public class InsetsControllerTest { assertEquals(ANIMATION_TYPE_USER, mController.getAnimationType(ime())); // verify show request is ignored during pre commit phase of predictive back anim - mController.show(ime(), true /* fromIme */, null /* statsToken */); + if (!Flags.refactorInsetsController()) { + mController.show(ime(), true /* fromIme */, null /* statsToken */); + } else { + mController.onControlsChanged(new InsetsSourceControl[]{ime}); + } assertEquals(ANIMATION_TYPE_USER, mController.getAnimationType(ime())); // verify show request is applied during post commit phase of predictive back anim mController.setPredictiveBackImeHideAnimInProgress(true); - mController.show(ime(), true /* fromIme */, null /* statsToken */); + if (!Flags.refactorInsetsController()) { + mController.show(ime(), true /* fromIme */, null /* statsToken */); + } else { + mController.show(ime(), false /* fromIme */, null /* statsToken */); + } assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ime())); // additionally verify that IME ends up visible @@ -1027,7 +1104,11 @@ public class InsetsControllerTest { prepareControls(); InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { // show ime as initial state - mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + if (!Flags.refactorInsetsController()) { + mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + } else { + mController.show(ime(), false /* fromIme */, ImeTracker.Token.empty()); + } mController.cancelExistingAnimations(); // fast forward show animation mViewRoot.getView().getViewTreeObserver().dispatchOnPreDraw(); assertTrue(mController.getState().peekSource(ID_IME).isVisible()); @@ -1058,11 +1139,15 @@ public class InsetsControllerTest { } private InsetsSourceControl createControl(int id, @InsetsType int type) { + return createControl(id, type, true); + } + + private InsetsSourceControl createControl(int id, @InsetsType int type, boolean hasLeash) { // Simulate binder behavior by copying SurfaceControl. Otherwise, InsetsController will // attempt to release mLeash directly. SurfaceControl copy = new SurfaceControl(mLeash, "InsetsControllerTest.createControl"); - return new InsetsSourceControl(id, type, copy, + return new InsetsSourceControl(id, type, hasLeash ? copy : null, (type & WindowInsets.Type.defaultVisible()) != 0, new Point(), Insets.NONE); } @@ -1071,9 +1156,13 @@ public class InsetsControllerTest { } private InsetsSourceControl[] prepareControls() { + return prepareControls(true); + } + + private InsetsSourceControl[] prepareControls(boolean imeControlHasLeash) { final InsetsSourceControl navBar = createControl(ID_NAVIGATION_BAR, navigationBars()); final InsetsSourceControl statusBar = createControl(ID_STATUS_BAR, statusBars()); - final InsetsSourceControl ime = createControl(ID_IME, ime()); + final InsetsSourceControl ime = createControl(ID_IME, ime(), imeControlHasLeash); InsetsSourceControl[] controls = new InsetsSourceControl[3]; controls[0] = navBar; diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java index a5137bdf80b8..6e563ff44478 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java @@ -46,7 +46,7 @@ public class AccessibilityNodeInfoTest { // The number of fields tested in the corresponding CTS AccessibilityNodeInfoTest: // See fullyPopulateAccessibilityNodeInfo, assertEqualsAccessibilityNodeInfo, // and assertAccessibilityNodeInfoCleared in that class. - private static final int NUM_MARSHALLED_PROPERTIES = 43; + private static final int NUM_MARSHALLED_PROPERTIES = 44; /** * The number of properties that are purposely not marshalled diff --git a/core/tests/coretests/src/com/android/internal/statusbar/StatusBarIconTest.java b/core/tests/coretests/src/com/android/internal/statusbar/StatusBarIconTest.java index a895378eaaf9..b183ecb50591 100644 --- a/core/tests/coretests/src/com/android/internal/statusbar/StatusBarIconTest.java +++ b/core/tests/coretests/src/com/android/internal/statusbar/StatusBarIconTest.java @@ -18,6 +18,8 @@ package com.android.internal.statusbar; import static com.google.common.truth.Truth.assertThat; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; import android.os.Parcel; import android.os.UserHandle; @@ -37,18 +39,55 @@ public class StatusBarIconTest { */ @Test public void testParcelable() { + final StatusBarIcon original = newStatusBarIcon(); + + final StatusBarIcon copy = parcelAndUnparcel(original); + + assertSerializableFieldsEqual(copy, original); + } + + @Test + public void testClone_withPreloaded() { + final StatusBarIcon original = newStatusBarIcon(); + original.preloadedIcon = new ColorDrawable(Color.RED); + + final StatusBarIcon copy = original.clone(); + + assertSerializableFieldsEqual(copy, original); + assertThat(copy.preloadedIcon).isNotNull(); + assertThat(copy.preloadedIcon).isInstanceOf(ColorDrawable.class); + assertThat(((ColorDrawable) copy.preloadedIcon).getColor()).isEqualTo(Color.RED); + } + + @Test + public void testClone_noPreloaded() { + final StatusBarIcon original = newStatusBarIcon(); + + final StatusBarIcon copy = original.clone(); + + assertSerializableFieldsEqual(copy, original); + assertThat(copy.preloadedIcon).isEqualTo(original.preloadedIcon); + } + + + private static StatusBarIcon newStatusBarIcon() { final UserHandle dummyUserHandle = UserHandle.of(100); final String dummyIconPackageName = "com.android.internal.statusbar.test"; final int dummyIconId = 123; final int dummyIconLevel = 1; final int dummyIconNumber = 2; final CharSequence dummyIconContentDescription = "dummyIcon"; - final StatusBarIcon original = new StatusBarIcon(dummyIconPackageName, dummyUserHandle, - dummyIconId, dummyIconLevel, dummyIconNumber, dummyIconContentDescription, + return new StatusBarIcon( + dummyIconPackageName, + dummyUserHandle, + dummyIconId, + dummyIconLevel, + dummyIconNumber, + dummyIconContentDescription, StatusBarIcon.Type.SystemIcon); + } - final StatusBarIcon copy = clone(original); - + private static void assertSerializableFieldsEqual(StatusBarIcon copy, StatusBarIcon original) { assertThat(copy.user).isEqualTo(original.user); assertThat(copy.pkg).isEqualTo(original.pkg); assertThat(copy.icon.sameAs(original.icon)).isTrue(); @@ -56,19 +95,17 @@ public class StatusBarIconTest { assertThat(copy.visible).isEqualTo(original.visible); assertThat(copy.number).isEqualTo(original.number); assertThat(copy.contentDescription).isEqualTo(original.contentDescription); + assertThat(copy.type).isEqualTo(original.type); } - private StatusBarIcon clone(StatusBarIcon original) { - Parcel parcel = null; + private static StatusBarIcon parcelAndUnparcel(StatusBarIcon original) { + Parcel parcel = Parcel.obtain(); try { - parcel = Parcel.obtain(); original.writeToParcel(parcel, 0); parcel.setDataPosition(0); return StatusBarIcon.CREATOR.createFromParcel(parcel); } finally { - if (parcel != null) { - parcel.recycle(); - } + parcel.recycle(); } } } diff --git a/data/fonts/Android.bp b/data/fonts/Android.bp index 1a3a0f60f2dc..cbaac21aa10b 100644 --- a/data/fonts/Android.bp +++ b/data/fonts/Android.bp @@ -71,6 +71,8 @@ prebuilt_fonts_xml { }, } +// TODO(nona): Change this to use generate_font_fallback to be able to generate XML from +// per family JSON config prebuilt_fonts_xml { name: "font_fallback.xml", src: "font_fallback.xml", @@ -94,3 +96,33 @@ filegroup { "DroidSansMono.ttf", ], } + +genrule { + name: "generate_font_fallback", + tools: [":generate_fonts_xml"], + tool_files: [ + "alias.json", + "fallback_order.json", + ], + srcs: [ + ":CarroisGothicSC", + ":ComingSoon", + ":CutiveMono", + ":DancingScript", + ":DroidSansMono", + ":Roboto", + ":RobotoFlex", + ":SourceSansPro", + ":noto-fonts", + ], + exclude_srcs: [ + "alias.json", + "fallback_order.json", + ], + out: ["font_fallback.xml"], + cmd: "$(location :generate_fonts_xml) " + + "--alias=$(location alias.json) " + + "--fallback=$(location fallback_order.json) " + + "$(in) " + + "-o $(out)", +} diff --git a/data/fonts/alias.json b/data/fonts/alias.json new file mode 100644 index 000000000000..b5b867a49259 --- /dev/null +++ b/data/fonts/alias.json @@ -0,0 +1,37 @@ +[ + // sans-serif aliases + { "name": "arial", "to": "sans-serif" }, + { "name": "helvetica", "to": "sans-serif" }, + { "name": "tahoma", "to": "sans-serif" }, + { "name": "verdana", "to": "sans-serif" }, + { "name": "sans-serif-black", "to": "sans-serif", "weight": "900" }, + { "name": "sans-serif-light", "to": "sans-serif", "weight": "300" }, + { "name": "sans-serif-medium", "to": "sans-serif", "weight": "500" }, + { "name": "sans-serif-thin", "to": "sans-serif", "weight": "100" }, + + // sans-serif-condensed aliases + { "name": "sans-serif-condensed-light", "to": "sans-serif-condensed", "weight": "300" }, + { "name": "sans-serif-condensed-medium", "to": "sans-serif-condensed", "weight": "500" }, + + // serif aliases + { "name": "ITC Stone Serif", "to": "serif" }, + { "name": "baskerville", "to": "serif" }, + { "name": "fantasy", "to": "serif" }, + { "name": "georgia", "to": "serif" }, + { "name": "goudy", "to": "serif" }, + { "name": "palatino", "to": "serif" }, + { "name": "times new roman", "to": "serif" }, + { "name": "times", "to": "serif" }, + { "name": "serif-bold", "to": "serif", "weight": "700" }, + + // monospace aliases + { "name": "monaco", "to": "monospace" }, + { "name": "sans-serif-monospace", "to": "monospace" }, + + // serif-monospace aliases + { "name": "courier new", "to": "serif-monospace" }, + { "name": "courier", "to": "serif-monospace" }, + + // source-sans-pro aliases + { "name": "source-sans-pro-semi-bold", "to": "source-sans-pro", "weight": "600" } +] diff --git a/data/fonts/fallback_order.json b/data/fonts/fallback_order.json new file mode 100644 index 000000000000..2fc3f3e3fc5b --- /dev/null +++ b/data/fonts/fallback_order.json @@ -0,0 +1,136 @@ +[ + { "lang": "und-Arab" }, + { "lang": "und-Ethi" }, + { "lang": "und-Hebr" }, + { "lang": "und-Thai" }, + { "lang": "und-Armn" }, + { "lang": "und-Geor,und-Geok" }, + { "lang": "und-Deva" }, + { "lang": "und-Gujr" }, + { "lang": "und-Guru" }, + { "lang": "und-Taml" }, + { "lang": "und-Mlym" }, + { "lang": "und-Beng" }, + { "lang": "und-Telu" }, + { "lang": "und-Knda" }, + { "lang": "und-Orya" }, + { "lang": "und-Sinh" }, + { "lang": "und-Khmr" }, + { "lang": "und-Laoo" }, + { "lang": "und-Mymr" }, + { "lang": "und-Thaa" }, + { "lang": "und-Cham" }, + { "lang": "und-Ahom" }, + { "lang": "und-Adlm" }, + { "lang": "und-Avst" }, + { "lang": "und-Bali" }, + { "lang": "und-Bamu" }, + { "lang": "und-Batk" }, + { "lang": "und-Brah" }, + { "lang": "und-Bugi" }, + { "lang": "und-Buhd" }, + { "lang": "und-Cans" }, + { "lang": "und-Cari" }, + { "lang": "und-Cakm" }, + { "lang": "und-Cher" }, + { "lang": "und-Copt" }, + { "lang": "und-Xsux" }, + { "lang": "und-Cprt" }, + { "lang": "und-Dsrt" }, + { "lang": "und-Egyp" }, + { "lang": "und-Elba" }, + { "lang": "und-Glag" }, + { "lang": "und-Goth" }, + { "lang": "und-Hano" }, + { "lang": "und-Armi" }, + { "lang": "und-Phli" }, + { "lang": "und-Prti" }, + { "lang": "und-Java" }, + { "lang": "und-Kthi" }, + { "lang": "und-Kali" }, + { "lang": "und-Khar" }, + { "lang": "und-Lepc" }, + { "lang": "und-Limb" }, + { "lang": "und-Linb" }, + { "lang": "und-Lisu" }, + { "lang": "und-Lyci" }, + { "lang": "und-Lydi" }, + { "lang": "und-Mand" }, + { "lang": "und-Mtei" }, + { "lang": "und-Talu" }, + { "lang": "und-Nkoo" }, + { "lang": "und-Ogam" }, + { "lang": "und-Olck" }, + { "lang": "und-Ital" }, + { "lang": "und-Xpeo" }, + { "lang": "und-Sarb" }, + { "lang": "und-Orkh" }, + { "lang": "und-Osge" }, + { "lang": "und-Osma" }, + { "lang": "und-Phnx" }, + { "lang": "und-Rjng" }, + { "lang": "und-Runr" }, + { "lang": "und-Samr" }, + { "lang": "und-Saur" }, + { "lang": "und-Shaw" }, + { "lang": "und-Sund" }, + { "lang": "und-Sylo" }, + { "lang": "und-Syre" }, + { "lang": "und-Syrn" }, + { "lang": "und-Syrj" }, + { "lang": "und-Tglg" }, + { "lang": "und-Tagb" }, + { "lang": "und-Lana" }, + { "lang": "und-Tavt" }, + { "lang": "und-Tibt" }, + { "lang": "und-Tfng" }, + { "lang": "und-Ugar" }, + { "lang": "und-Vaii" }, + // NotoSansSymbol-Regular-Subsetted doesn't have any language but should be + // placed before the CJK fonts for reproducing the same fallback order. + { "id": "NotoSansSymbols-Regular-Subsetted" }, + { "lang": "zh-Hans" }, + { "lang": "zh-Hant,zh-Bopo" }, + { "lang": "ja" }, + { "lang": "ko" }, + { "lang": "und-Zsye" }, + { "lang": "und-Zsym" }, + { "lang": "und-Tale" }, + { "lang": "und-Yiii" }, + { "lang": "und-Mong" }, + { "lang": "und-Phag" }, + { "lang": "und-Hluw" }, + { "lang": "und-Bass" }, + { "lang": "und-Bhks" }, + { "lang": "und-Hatr" }, + { "lang": "und-Lina" }, + { "lang": "und-Mani" }, + { "lang": "und-Marc" }, + { "lang": "und-Merc" }, + { "lang": "und-Plrd" }, + { "lang": "und-Mroo" }, + { "lang": "und-Mult" }, + { "lang": "und-Nbat" }, + { "lang": "und-Newa" }, + { "lang": "und-Narb" }, + { "lang": "und-Perm" }, + { "lang": "und-Hmng" }, + { "lang": "und-Palm" }, + { "lang": "und-Pauc" }, + { "lang": "und-Shrd" }, + { "lang": "und-Sora" }, + { "lang": "und-Gong" }, + { "lang": "und-Rohg" }, + { "lang": "und-Khoj" }, + { "lang": "und-Gonm" }, + { "lang": "und-Wcho" }, + { "lang": "und-Wara" }, + { "lang": "und-Gran" }, + { "lang": "und-Modi" }, + { "lang": "und-Dogr" }, + { "lang": "und-Medf" }, + { "lang": "und-Soyo" }, + { "lang": "und-Takr" }, + { "lang": "und-Hmnp" }, + { "lang": "und-Yezi" } +] diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 1a3aa8eec5ec..b338a2ae2b79 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -46,9 +46,6 @@ filegroup { srcs: [ "src/com/android/wm/shell/common/bubbles/*.kt", "src/com/android/wm/shell/common/bubbles/*.java", - "src/com/android/wm/shell/common/desktopmode/*.kt", - "src/com/android/wm/shell/pip/PipContentOverlay.java", - "src/com/android/wm/shell/util/**/*.java", ], path: "src", } @@ -208,6 +205,7 @@ android_library { // TODO(b/168581922) protologtool do not support kotlin(*.kt) ":wm_shell-sources-kt", ":wm_shell-aidls", + ":wm_shell-shared-aidls", ], resource_dirs: [ "res", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedRecentTaskInfo.aidl index 15797cdb9aba..e21bf8fb723c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.aidl +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedRecentTaskInfo.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,6 @@ * limitations under the License. */ -package com.android.wm.shell.util; +package com.android.wm.shell.shared; parcelable GroupedRecentTaskInfo;
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedRecentTaskInfo.java index a2d2b9aff597..65e079ef4f72 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedRecentTaskInfo.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.util; +package com.android.wm.shell.shared; import android.annotation.IntDef; import android.app.ActivityManager; @@ -25,6 +25,8 @@ import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.wm.shell.shared.split.SplitBounds; + import java.util.Arrays; import java.util.List; import java.util.Set; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.aidl index c968e809bf61..f7ddf71245e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.aidl +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.aidl @@ -14,6 +14,6 @@ * limitations under the License. */ -package com.android.wm.shell.common.desktopmode; +package com.android.wm.shell.shared.desktopmode; parcelable DesktopModeTransitionSource;
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt index dbbf178613b5..d15fbed409b8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.desktopmode +package com.android.wm.shell.shared.desktopmode import android.os.Parcel import android.os.Parcelable diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java index ff2d46e11107..cf39415b3fe6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip; +package com.android.wm.shell.shared.pip; import static android.util.TypedValue.COMPLEX_UNIT_DIP; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitBounds.java index 88b752822a20..7c1faa667d9a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitBounds.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.wm.shell.util; +package com.android.wm.shell.shared.split; import android.graphics.Rect; import android.os.Parcel; 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 d7da0515f228..27194b344780 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 @@ -1066,14 +1066,30 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return true; } + private void kickStartAnimation() { + startSystemAnimation(); + + // Dispatch the first progress after animation start for + // smoothing the initial animation, instead of waiting for next + // onMove. + final BackMotionEvent backFinish = mCurrentTracker + .createProgressEvent(); + dispatchOnBackProgressed(mActiveCallback, backFinish); + if (!mBackGestureStarted) { + // if the down -> up gesture happened before animation + // start, we have to trigger the uninterruptible transition + // to finish the back animation. + startPostCommitAnimation(); + } + } + private void createAdapter() { IBackAnimationRunner runner = new IBackAnimationRunner.Stub() { @Override public void onAnimationStart( RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, - RemoteAnimationTarget[] nonApps, + IBinder token, IBackAnimationFinishedCallback finishedCallback) { mShellExecutor.execute( () -> { @@ -1085,21 +1101,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } mBackAnimationFinishedCallback = finishedCallback; mApps = apps; - startSystemAnimation(); - mBackTransitionHandler.consumeQueuedTransitionIfNeeded(); - - // Dispatch the first progress after animation start for - // smoothing the initial animation, instead of waiting for next - // onMove. - final BackMotionEvent backFinish = mCurrentTracker - .createProgressEvent(); - dispatchOnBackProgressed(mActiveCallback, backFinish); - if (!mBackGestureStarted) { - // if the down -> up gesture happened before animation - // start, we have to trigger the uninterruptible transition - // to finish the back animation. - startPostCommitAnimation(); + // app only visible after transition ready, break for now. + if (token != null) { + return; } + kickStartAnimation(); + mBackTransitionHandler.consumeQueuedTransitionIfNeeded(); }); } @@ -1199,6 +1206,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @NonNull SurfaceControl.Transaction st, @NonNull SurfaceControl.Transaction ft, @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) { + kickStartAnimation(); + } // Both mShellExecutor and Transitions#mMainExecutor are ShellMainThread, so we don't // need to post to ShellExecutor when called. if (info.getType() == WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 3dc33c264044..b508c1ba7fe4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -1225,7 +1225,7 @@ public class BubbleController implements ConfigurationChangeListener, mBubblePositioner.setBubbleBarLocation(location); mBubblePositioner.setBubbleBarTopOnScreen(topOnScreen); if (mBubbleData.getSelectedBubble() != null) { - mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ true); + showExpandedViewForBubbleBar(); } } @@ -1243,7 +1243,7 @@ public class BubbleController implements ConfigurationChangeListener, } if (selectedBubbleKey != null && !selectedBubbleKey.equals(bubbleKey)) { // We did not remove the selected bubble. Expand it again - mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ true); + showExpandedViewForBubbleBar(); } } @@ -1997,15 +1997,10 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void expansionChanged(boolean isExpanded) { - if (mLayerView != null) { - if (!isExpanded) { - mLayerView.collapse(); - } else { - BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); - if (selectedBubble != null) { - mLayerView.showExpandedView(selectedBubble); - } - } + // in bubble bar mode, let the request to show the expanded view come from launcher. + // only collapse here if we're collapsing. + if (mLayerView != null && !isExpanded) { + mLayerView.collapse(); } } @@ -2151,6 +2146,13 @@ public class BubbleController implements ConfigurationChangeListener, } }; + private void showExpandedViewForBubbleBar() { + BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); + if (selectedBubble != null && mLayerView != null) { + mLayerView.showExpandedView(selectedBubble); + } + } + private void updateOverflowButtonDot() { BubbleOverflow overflow = mBubbleData.getOverflow(); if (overflow == null) return; @@ -2532,6 +2534,15 @@ public class BubbleController implements ConfigurationChangeListener, if (mLayerView != null) mLayerView.updateExpandedView(); }); } + + @Override + public void showExpandedView() { + mMainExecutor.execute(() -> { + if (mLayerView != null) { + showExpandedViewForBubbleBar(); + } + }); + } } private class BubblesImpl implements Bubbles { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl index 5c789749412c..5779a8f7bcc4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl @@ -53,4 +53,6 @@ interface IBubbles { oneway void showShortcutBubble(in ShortcutInfo info) = 12; oneway void showAppBubble(in Intent intent) = 13; + + oneway void showExpandedView() = 14; }
\ No newline at end of file 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 c2ee223b916a..972b78f6ca9a 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 @@ -39,6 +39,7 @@ import android.view.InsetsState; import android.view.accessibility.AccessibilityManager; import com.android.internal.annotations.VisibleForTesting; +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.DisplayController.OnDisplaysChangedListener; @@ -67,6 +68,7 @@ import java.util.List; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.IntPredicate; import java.util.function.Predicate; /** @@ -189,6 +191,9 @@ public class CompatUIController implements OnDisplaysChangedListener, @NonNull private final CompatUIStatusManager mCompatUIStatusManager; + @NonNull + private final IntPredicate mInDesktopModePredicate; + public CompatUIController(@NonNull Context context, @NonNull ShellInit shellInit, @NonNull ShellController shellController, @@ -202,7 +207,8 @@ public class CompatUIController implements OnDisplaysChangedListener, @NonNull CompatUIConfiguration compatUIConfiguration, @NonNull CompatUIShellCommandHandler compatUIShellCommandHandler, @NonNull AccessibilityManager accessibilityManager, - @NonNull CompatUIStatusManager compatUIStatusManager) { + @NonNull CompatUIStatusManager compatUIStatusManager, + @NonNull IntPredicate isDesktopModeEnablePredicate) { mContext = context; mShellController = shellController; mDisplayController = displayController; @@ -218,6 +224,7 @@ public class CompatUIController implements OnDisplaysChangedListener, mDisappearTimeSupplier = flags -> accessibilityManager.getRecommendedTimeoutMillis( DISAPPEAR_DELAY_MS, flags); mCompatUIStatusManager = compatUIStatusManager; + mInDesktopModePredicate = isDesktopModeEnablePredicate; shellInit.addInitCallback(this::onInit, this); } @@ -251,7 +258,9 @@ public class CompatUIController implements OnDisplaysChangedListener, updateActiveTaskInfo(taskInfo); } - if (taskInfo.configuration == null || taskListener == null) { + // We close all the Compat UI educations in case we're in desktop mode. + if (taskInfo.configuration == null || taskListener == null + || isInDesktopMode(taskInfo.displayId)) { // Null token means the current foreground activity is not in compatibility mode. removeLayouts(taskInfo.taskId); return; @@ -350,7 +359,6 @@ public class CompatUIController implements OnDisplaysChangedListener, mOnInsetsChangedListeners.remove(displayId); } - @Override public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { updateDisplayLayout(displayId); @@ -692,7 +700,8 @@ public class CompatUIController implements OnDisplaysChangedListener, mContext.startActivityAsUser(intent, userHandle); } - private void removeLayouts(int taskId) { + @VisibleForTesting + void removeLayouts(int taskId) { final CompatUIWindowManager compatLayout = mActiveCompatLayouts.get(taskId); if (compatLayout != null) { compatLayout.release(); @@ -825,4 +834,9 @@ public class CompatUIController implements OnDisplaysChangedListener, boolean mHasShownCameraCompatHint; boolean mHasShownUserAspectRatioSettingsButtonHint; } + + private boolean isInDesktopMode(int displayId) { + return Flags.skipCompatUiEducationInDesktopMode() + && mInDesktopModePredicate.test(displayId); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 98536bf98f0b..42937c134e7f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -137,6 +137,7 @@ import dagger.Module; import dagger.Provides; import java.util.Optional; +import java.util.function.IntPredicate; /** * Provides basic dependencies from {@link com.android.wm.shell}, these dependencies are only @@ -261,6 +262,7 @@ public abstract class WMShellBaseModule { Lazy<CompatUIShellCommandHandler> compatUIShellCommandHandler, Lazy<AccessibilityManager> accessibilityManager, CompatUIRepository compatUIRepository, + Optional<DesktopModeTaskRepository> desktopModeTaskRepository, @NonNull CompatUIState compatUIState, @NonNull CompatUIComponentIdGenerator componentIdGenerator, @NonNull CompatUIComponentFactory compatUIComponentFactory, @@ -273,6 +275,10 @@ public abstract class WMShellBaseModule { new DefaultCompatUIHandler(compatUIRepository, compatUIState, componentIdGenerator, compatUIComponentFactory, mainExecutor)); } + final IntPredicate inDesktopModePredicate = + desktopModeTaskRepository.<IntPredicate>map(modeTaskRepository -> displayId -> + modeTaskRepository.getVisibleTaskCount(displayId) > 0) + .orElseGet(() -> displayId -> false); return Optional.of( new CompatUIController( context, @@ -288,7 +294,8 @@ public abstract class WMShellBaseModule { compatUIConfiguration.get(), compatUIShellCommandHandler.get(), accessibilityManager.get(), - compatUIStatusManager)); + compatUIStatusManager, + inDesktopModePredicate)); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 51ce2c6707ac..3464fef07f33 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -42,6 +42,7 @@ import com.android.wm.shell.pip2.phone.PhonePipMenuController; import com.android.wm.shell.pip2.phone.PipController; import com.android.wm.shell.pip2.phone.PipMotionHelper; import com.android.wm.shell.pip2.phone.PipScheduler; +import com.android.wm.shell.pip2.phone.PipTaskListener; import com.android.wm.shell.pip2.phone.PipTouchHandler; import com.android.wm.shell.pip2.phone.PipTransition; import com.android.wm.shell.pip2.phone.PipTransitionState; @@ -73,12 +74,13 @@ public abstract class Pip2Module { PipBoundsAlgorithm pipBoundsAlgorithm, Optional<PipController> pipController, PipTouchHandler pipTouchHandler, + PipTaskListener pipTaskListener, @NonNull PipScheduler pipScheduler, @NonNull PipTransitionState pipStackListenerController, @NonNull PipUiStateChangeController pipUiStateChangeController) { return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, - pipBoundsState, null, pipBoundsAlgorithm, pipScheduler, - pipStackListenerController, pipUiStateChangeController); + pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener, + pipScheduler, pipStackListenerController, pipUiStateChangeController); } @WMSingleton @@ -123,9 +125,11 @@ public abstract class Pip2Module { @Provides static PipScheduler providePipScheduler(Context context, PipBoundsState pipBoundsState, + PhonePipMenuController pipMenuController, @ShellMainThread ShellExecutor mainExecutor, PipTransitionState pipTransitionState) { - return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState); + return new PipScheduler(context, pipBoundsState, pipMenuController, + mainExecutor, pipTransitionState); } @WMSingleton @@ -190,4 +194,17 @@ public abstract class Pip2Module { PipTransitionState pipTransitionState) { return new PipUiStateChangeController(pipTransitionState); } + + @WMSingleton + @Provides + static PipTaskListener providePipTaskListener(Context context, + ShellTaskOrganizer shellTaskOrganizer, + PipTransitionState pipTransitionState, + PipScheduler pipScheduler, + PipBoundsState pipBoundsState, + PipBoundsAlgorithm pipBoundsAlgorithm, + @ShellMainThread ShellExecutor mainExecutor) { + return new PipTaskListener(context, shellTaskOrganizer, pipTransitionState, + pipScheduler, pipBoundsState, pipBoundsAlgorithm, mainExecutor); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java index 31c8f1e45007..cca750014fc1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java @@ -18,8 +18,8 @@ package com.android.wm.shell.desktopmode; import android.graphics.Region; -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.shared.annotations.ExternalThread; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import java.util.concurrent.Executor; import java.util.function.Consumer; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt index eca3c1fdc65a..dba8c9367654 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -16,7 +16,7 @@ package com.android.wm.shell.desktopmode -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.UNKNOWN +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN import com.android.wm.shell.sysui.ShellCommandHandler import java.io.PrintWriter diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt index b24bd10eaa0d..d6fccd116061 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt @@ -17,7 +17,7 @@ package com.android.wm.shell.desktopmode import android.view.WindowManager.TransitionType -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_TYPES /** 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 ffd534b6b814..2852631656b5 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 @@ -69,7 +69,6 @@ import com.android.wm.shell.common.RemoteCallable import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SingleInstanceRemoteListener import com.android.wm.shell.common.SyncTransactionQueue -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.DragStartState @@ -89,6 +88,7 @@ import com.android.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVI import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useDesktopOverrideDensity +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource 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.splitscreen.SplitScreenController diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index 1a103d345ca7..d72ec90957fc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -22,13 +22,15 @@ import android.graphics.Rect import android.os.Bundle import android.os.IBinder import android.os.SystemClock +import android.os.SystemProperties import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CLOSE import android.window.TransitionInfo import android.window.TransitionInfo.Change import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction -import androidx.dynamicanimation.animation.SpringForce +import com.android.internal.annotations.VisibleForTesting +import com.android.internal.dynamicanimation.animation.SpringForce import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE import com.android.internal.jank.InteractionJankMonitor @@ -893,13 +895,10 @@ constructor( ) { private val positionSpringConfig = - PhysicsAnimator.SpringConfig( - SpringForce.STIFFNESS_LOW, - SpringForce.DAMPING_RATIO_LOW_BOUNCY - ) + PhysicsAnimator.SpringConfig(POSITION_SPRING_STIFFNESS, POSITION_SPRING_DAMPING_RATIO) private val sizeSpringConfig = - PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_NO_BOUNCY) + PhysicsAnimator.SpringConfig(SIZE_SPRING_STIFFNESS, SIZE_SPRING_DAMPING_RATIO) /** * @return layers in order: @@ -929,7 +928,7 @@ constructor( finishTransaction.hide(homeLeash) // Setup freeform tasks before animation state.freeformTaskChanges.forEach { change -> - val startScale = DRAG_TO_DESKTOP_FREEFORM_TASK_INITIAL_SCALE + val startScale = FREEFORM_TASKS_INITIAL_SCALE val startX = change.endAbsBounds.left + change.endAbsBounds.width() * (1 - startScale) / 2 val startY = @@ -994,9 +993,22 @@ constructor( (animBounds.width() - startBounds.width()).toFloat() / (endBounds.width() - startBounds.width()) val animScale = startScale + animFraction * (1 - startScale) - // Freeform animation starts 50% in the animation - val freeformAnimFraction = max(animFraction - 0.5f, 0f) * 2f - val freeformStartScale = DRAG_TO_DESKTOP_FREEFORM_TASK_INITIAL_SCALE + // Freeform animation starts with freeform animation offset relative to the commit + // animation and plays until the commit animation ends. For instance: + // - if the freeform animation offset is `0.0` the freeform tasks animate alongside + // - if the freeform animation offset is `0.6` the freeform tasks will + // start animating at 60% fraction of the commit animation and will complete when + // the commit animation fraction is 100%. + // - if the freeform animation offset is `1.0` then freeform tasks will appear + // without animation after commit animation finishes. + val freeformAnimFraction = + if (FREEFORM_TASKS_ANIM_OFFSET != 1f) { + max(animFraction - FREEFORM_TASKS_ANIM_OFFSET, 0f) / + (1f - FREEFORM_TASKS_ANIM_OFFSET) + } else { + 0f + } + val freeformStartScale = FREEFORM_TASKS_INITIAL_SCALE val freeformAnimScale = freeformStartScale + freeformAnimFraction * (1 - freeformStartScale) tx.apply { @@ -1032,10 +1044,53 @@ constructor( } companion object { + /** The freeform tasks initial scale when committing the drag-to-desktop gesture. */ + private val FREEFORM_TASKS_INITIAL_SCALE = + propertyValue("freeform_tasks_initial_scale", scale = 100f, default = 0.9f) + + /** The freeform tasks animation offset relative to the whole animation duration. */ + private val FREEFORM_TASKS_ANIM_OFFSET = + propertyValue("freeform_tasks_anim_offset", scale = 100f, default = 0.5f) + + /** The spring force stiffness used to place the window into the final position. */ + private val POSITION_SPRING_STIFFNESS = + propertyValue("position_stiffness", default = SpringForce.STIFFNESS_LOW) + + /** The spring force damping ratio used to place the window into the final position. */ + private val POSITION_SPRING_DAMPING_RATIO = + propertyValue( + "position_damping_ratio", + scale = 100f, + default = SpringForce.DAMPING_RATIO_LOW_BOUNCY + ) + + /** The spring force stiffness used to resize the window into the final bounds. */ + private val SIZE_SPRING_STIFFNESS = + propertyValue("size_stiffness", default = SpringForce.STIFFNESS_LOW) + + /** The spring force damping ratio used to resize the window into the final bounds. */ + private val SIZE_SPRING_DAMPING_RATIO = + propertyValue( + "size_damping_ratio", + scale = 100f, + default = SpringForce.DAMPING_RATIO_NO_BOUNCY + ) + + /** Drag to desktop transition system properties group. */ + @VisibleForTesting + const val SYSTEM_PROPERTIES_GROUP = "persist.wm.debug.desktop_transitions.drag_to_desktop" + /** - * The initial scale of the freeform tasks in the animation to commit the drag-to-desktop - * gesture. + * Drag to desktop transition system property value with [name]. + * + * @param scale an optional scale to apply to the value read from the system property. + * @param default a default value to return if the system property isn't set. */ - private const val DRAG_TO_DESKTOP_FREEFORM_TASK_INITIAL_SCALE = 0.9f + @VisibleForTesting + fun propertyValue(name: String, scale: Float = 1f, default: Float = 0f): Float = + SystemProperties.getInt( + /* key= */ "$SYSTEM_PROPERTIES_GROUP.$name", + /* def= */ (default * scale).toInt() + ) / scale } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java index 04506c1e66f2..80e106f3990b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java @@ -41,7 +41,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.jank.InteractionJankMonitor; -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java index 171378f9a164..e87be520cc91 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java @@ -44,7 +44,7 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.Cuj; import com.android.internal.jank.InteractionJankMonitor; -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.transition.Transitions; import java.util.ArrayList; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl index a7ec2037706d..b036e40e6e16 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl @@ -18,8 +18,8 @@ package com.android.wm.shell.desktopmode; import android.app.ActivityManager.RunningTaskInfo; import android.window.RemoteTransition; -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.desktopmode.IDesktopTaskListener; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; /** * Interface that is exposed to remote callers to manipulate desktop mode features. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md index 84f6af4125b8..72d1a76b17e3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md @@ -27,10 +27,13 @@ building to check the log state (is enabled) before printing the print format st traces in Winscope) ### Kotlin +Kotlin protologging is supported but not as optimized as in Java. -Protolog tool does not yet have support for Kotlin code (see [b/168581922](https://b.corp.google.com/issues/168581922)). -For logging in Kotlin, use the [KtProtoLog](/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt) -class which has a similar API to the Java ProtoLog class. +The Protolog tool does not yet have support for Kotlin code ([b/168581922](https://b.corp.google.com/issues/168581922)). + +What this implies is that ProtoLogs are not pre-processed to extract the static strings out when used in Kotlin. So, +there is no memory gain when using ProtoLogging in Kotlin. The logs will still be traced to Perfetto, but with a subtly +worse performance due to the additional string interning that needs to be done at run time instead of at build time. ### Enabling ProtoLog command line logging Run these commands to enable protologs (in logcat) for WM Core ([list of all core tags](/core/java/com/android/internal/protolog/ProtoLogGroup.java)): diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index b0c896fbe516..4df649ca8c93 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -43,6 +43,7 @@ import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.animation.Interpolators; +import com.android.wm.shell.shared.pip.PipContentOverlay; import com.android.wm.shell.transition.Transitions; import java.lang.annotation.Retention; @@ -418,7 +419,7 @@ public class PipAnimationController { } SurfaceControl getContentOverlayLeash() { - return mContentOverlay == null ? null : mContentOverlay.mLeash; + return mContentOverlay == null ? null : mContentOverlay.getLeash(); } void setColorContentOverlay(Context context) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 2de545a829ad..e4cd10f37d37 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -91,6 +91,7 @@ import com.android.wm.shell.pip.phone.PipMotionHelper; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.animation.Interpolators; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.pip.PipContentOverlay; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.transition.Transitions; @@ -361,8 +362,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, SurfaceControl mPipOverlay; /** - * The app bounds used for the buffer size of the - * {@link com.android.wm.shell.pip.PipContentOverlay.PipAppIconOverlay}. + * The app bounds used for the buffer size of the {@link PipContentOverlay.PipAppIconOverlay}. * * Note that this is empty if the overlay is removed or if it's some other type of overlay * defined in {@link PipContentOverlay}. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index b102e40147e1..05d19846bfee 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -75,6 +75,7 @@ import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.TransitionUtil; +import com.android.wm.shell.shared.pip.PipContentOverlay; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.CounterRotatorHelper; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java index 88f9e4c740e3..d565776c9917 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java @@ -134,9 +134,10 @@ public class PipResizeAnimator extends ValueAnimator Rect baseBounds, Rect targetBounds, float degrees) { Matrix transformTensor = new Matrix(); final float[] mMatrixTmp = new float[9]; - final float scale = (float) targetBounds.width() / baseBounds.width(); + final float scaleX = (float) targetBounds.width() / baseBounds.width(); + final float scaleY = (float) targetBounds.height() / baseBounds.height(); - transformTensor.setScale(scale, scale); + transformTensor.setScale(scaleX, scaleY); transformTensor.postTranslate(targetBounds.left, targetBounds.top); transformTensor.postRotate(degrees, targetBounds.centerX(), targetBounds.centerY()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java index 218d456e9596..0324fdba0fbf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java @@ -56,7 +56,6 @@ import kotlin.Unit; import kotlin.jvm.functions.Function0; import java.util.Optional; -import java.util.function.Consumer; /** * A helper to animate and manipulate the PiP. @@ -134,18 +133,6 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, private final PhysicsAnimator.SpringConfig mConflictResolutionSpringConfig = new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_NO_BOUNCY); - @Nullable private Runnable mUpdateMovementBoundsRunnable; - - private final Consumer<Rect> mUpdateBoundsCallback = (Rect newBounds) -> { - if (mPipBoundsState.getBounds().equals(newBounds)) { - return; - } - - mMenuController.updateMenuLayout(newBounds); - mPipBoundsState.setBounds(newBounds); - maybeUpdateMovementBounds(); - }; - /** * Whether we're springing to the touch event location (vs. moving it to that position * instantly). We spring-to-touch after PIP is dragged out of the magnetic target, since it was @@ -683,16 +670,6 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, cleanUpHighPerfSessionMaybe(); } - void setUpdateMovementBoundsRunnable(Runnable updateMovementBoundsRunnable) { - mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; - } - - private void maybeUpdateMovementBounds() { - if (mUpdateMovementBoundsRunnable != null) { - mUpdateMovementBoundsRunnable.run(); - } - } - /** * Notifies the floating coordinator that we're moving, and sets the animating to bounds so * we return these bounds from @@ -720,7 +697,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, /** * Directly resizes the PiP to the given {@param bounds}. */ - private void resizeAndAnimatePipUnchecked(Rect toBounds, int duration) { + void resizeAndAnimatePipUnchecked(Rect toBounds, int duration) { if (mPipBoundsState.getMotionBoundsState().isInMotion()) { // Do not carry out any resizing if we are dragging or physics animator is running. return; @@ -813,7 +790,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, cleanUpHighPerfSessionMaybe(); // Signal that the transition is done - should update transition state by default. - mPipScheduler.scheduleFinishResizePip(false /* configAtEnd */); + mPipScheduler.scheduleFinishResizePip(destinationBounds, false /* configAtEnd */); } private void startResizeAnimation(SurfaceControl.Transaction startTx, @@ -829,8 +806,6 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, startTx, finishTx, mPipBoundsState.getBounds(), mPipBoundsState.getBounds(), destinationBounds, duration, 0f /* angle */); animator.setAnimationEndCallback(() -> { - mUpdateBoundsCallback.accept(destinationBounds); - // In case an ongoing drag/fling was present before a deterministic resize transition // kicked in, we need to update the update bounds properly before cleaning in-motion // state. @@ -839,7 +814,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, cleanUpHighPerfSessionMaybe(); // Signal that we are done with resize transition - mPipScheduler.scheduleFinishResizePip(true /* configAtEnd */); + mPipScheduler.scheduleFinishResizePip(destinationBounds, true /* configAtEnd */); }); animator.start(); } @@ -849,7 +824,6 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, // The physics animation ended, though we may not necessarily be done animating, such as // when we're still dragging after moving out of the magnetic target. Only set the final // bounds state and clear motion bounds completely if the whole animation is over. - mPipBoundsState.setBounds(mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded(); } mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java index d28204add0ac..f5ef64dff94b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java @@ -50,7 +50,6 @@ import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.pip2.animation.PipResizeAnimator; import java.io.PrintWriter; -import java.util.function.Consumer; /** * Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to @@ -86,8 +85,6 @@ public class PipResizeGestureHandler implements private final Rect mUserResizeBounds = new Rect(); private final Rect mDownBounds = new Rect(); private final Rect mStartBoundsAfterRelease = new Rect(); - private final Runnable mUpdateMovementBoundsRunnable; - private final Consumer<Rect> mUpdateResizeBoundsCallback; private float mTouchSlop; @@ -121,7 +118,6 @@ public class PipResizeGestureHandler implements PipTouchState pipTouchState, PipScheduler pipScheduler, PipTransitionState pipTransitionState, - Runnable updateMovementBoundsRunnable, PipUiEventLogger pipUiEventLogger, PhonePipMenuController menuActivityController, ShellExecutor mainExecutor, @@ -138,18 +134,9 @@ public class PipResizeGestureHandler implements mPipTransitionState = pipTransitionState; mPipTransitionState.addPipTransitionStateChangedListener(this); - mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; mPhonePipMenuController = menuActivityController; mPipUiEventLogger = pipUiEventLogger; mPinchResizingAlgorithm = new PipPinchResizingAlgorithm(); - - mUpdateResizeBoundsCallback = (rect) -> { - mUserResizeBounds.set(rect); - // mMotionHelper.synchronizePinnedStackBounds(); - mPipBoundsState.setBounds(rect); - mUpdateMovementBoundsRunnable.run(); - resetState(); - }; } void init() { @@ -563,11 +550,13 @@ public class PipResizeGestureHandler implements mLastResizeBounds, duration, mAngle); animator.setAnimationEndCallback(() -> { // All motion operations have actually finished, so make bounds cache updates. - mUpdateResizeBoundsCallback.accept(mLastResizeBounds); + mUserResizeBounds.set(mLastResizeBounds); + resetState(); cleanUpHighPerfSessionMaybe(); // Signal that we are done with resize transition - mPipScheduler.scheduleFinishResizePip(true /* configAtEnd */); + mPipScheduler.scheduleFinishResizePip( + mLastResizeBounds, true /* configAtEnd */); }); animator.start(); break; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index ac670cf3f828..f4defdc7963c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -52,11 +52,14 @@ public class PipScheduler { private final Context mContext; private final PipBoundsState mPipBoundsState; + private final PhonePipMenuController mPipMenuController; private final ShellExecutor mMainExecutor; private final PipTransitionState mPipTransitionState; private PipSchedulerReceiver mSchedulerReceiver; private PipTransitionController mPipTransitionController; + @Nullable private Runnable mUpdateMovementBoundsRunnable; + /** * Temporary PiP CUJ codes to schedule PiP related transitions directly from Shell. * This is used for a broadcast receiver to resolve intents. This should be removed once @@ -94,10 +97,12 @@ public class PipScheduler { public PipScheduler(Context context, PipBoundsState pipBoundsState, + PhonePipMenuController pipMenuController, ShellExecutor mainExecutor, PipTransitionState pipTransitionState) { mContext = context; mPipBoundsState = pipBoundsState; + mPipMenuController = pipMenuController; mMainExecutor = mainExecutor; mPipTransitionState = pipTransitionState; @@ -189,9 +194,13 @@ public class PipScheduler { * Signals to Core to finish the PiP resize transition. * Note that we do not allow any actual WM Core changes at this point. * + * @param toBounds destination bounds used only for internal state updates - not sent to Core. * @param configAtEnd true if we are waiting for config updates at the end of the transition. */ - public void scheduleFinishResizePip(boolean configAtEnd) { + public void scheduleFinishResizePip(Rect toBounds, boolean configAtEnd) { + // Make updates to the internal state to reflect new bounds + onFinishingPipResize(toBounds); + SurfaceControl.Transaction tx = null; if (configAtEnd) { tx = new SurfaceControl.Transaction(); @@ -238,4 +247,23 @@ public class PipScheduler { tx.setMatrix(leash, transformTensor, mMatrixTmp); tx.apply(); } + + void setUpdateMovementBoundsRunnable(Runnable updateMovementBoundsRunnable) { + mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; + } + + private void maybeUpdateMovementBounds() { + if (mUpdateMovementBoundsRunnable != null) { + mUpdateMovementBoundsRunnable.run(); + } + } + + private void onFinishingPipResize(Rect newBounds) { + if (mPipBoundsState.getBounds().equals(newBounds)) { + return; + } + mPipBoundsState.setBounds(newBounds); + mPipMenuController.updateMenuLayout(newBounds); + maybeUpdateMovementBounds(); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java new file mode 100644 index 000000000000..7f168800fb29 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java @@ -0,0 +1,161 @@ +/* + * 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.pip2.phone; + +import static com.android.wm.shell.pip2.phone.PipTransition.ANIMATING_BOUNDS_CHANGE_DURATION; + +import android.app.ActivityManager; +import android.app.PictureInPictureParams; +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.SurfaceControl; + +import androidx.annotation.Nullable; + +import com.android.internal.util.Preconditions; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.pip.PipBoundsAlgorithm; +import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipUtils; +import com.android.wm.shell.pip2.animation.PipResizeAnimator; +import com.android.wm.shell.shared.annotations.ShellMainThread; + +/** + * A Task Listener implementation used only for CUJs and trigger paths that cannot be initiated via + * Transitions framework directly. + * Hence, it's the intention to keep the usage of this class for a very limited set of cases. + */ +public class PipTaskListener implements ShellTaskOrganizer.TaskListener, + PipTransitionState.PipTransitionStateChangedListener { + private static final int ASPECT_RATIO_CHANGE_DURATION = 250; + private static final String ANIMATING_ASPECT_RATIO_CHANGE = "animating_aspect_ratio_change"; + + private final Context mContext; + private final PipTransitionState mPipTransitionState; + private final PipScheduler mPipScheduler; + private final PipBoundsState mPipBoundsState; + private final PipBoundsAlgorithm mPipBoundsAlgorithm; + private final ShellExecutor mMainExecutor; + private final PictureInPictureParams mPictureInPictureParams = + new PictureInPictureParams.Builder().build(); + + private boolean mWaitingForAspectRatioChange = false; + + public PipTaskListener(Context context, + ShellTaskOrganizer shellTaskOrganizer, + PipTransitionState pipTransitionState, + PipScheduler pipScheduler, + PipBoundsState pipBoundsState, + PipBoundsAlgorithm pipBoundsAlgorithm, + @ShellMainThread ShellExecutor mainExecutor) { + mContext = context; + mPipTransitionState = pipTransitionState; + mPipScheduler = pipScheduler; + mPipBoundsState = pipBoundsState; + mPipBoundsAlgorithm = pipBoundsAlgorithm; + mMainExecutor = mainExecutor; + + mPipTransitionState.addPipTransitionStateChangedListener(this); + if (PipUtils.isPip2ExperimentEnabled()) { + mMainExecutor.execute(() -> { + shellTaskOrganizer.addListenerForType(this, + ShellTaskOrganizer.TASK_LISTENER_TYPE_PIP); + }); + } + } + + void setPictureInPictureParams(@Nullable PictureInPictureParams params) { + if (mPictureInPictureParams.equals(params)) { + return; + } + mPictureInPictureParams.copyOnlySet(params != null ? params + : new PictureInPictureParams.Builder().build()); + } + + @Override + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + PictureInPictureParams params = taskInfo.pictureInPictureParams; + if (mPictureInPictureParams.equals(params)) { + return; + } + setPictureInPictureParams(params); + float newAspectRatio = mPictureInPictureParams.getAspectRatioFloat(); + if (PipUtils.aspectRatioChanged(newAspectRatio, mPipBoundsState.getAspectRatio())) { + mPipTransitionState.setOnIdlePipTransitionStateRunnable(() -> { + onAspectRatioChanged(newAspectRatio); + }); + } + } + + private void onAspectRatioChanged(float ratio) { + mPipBoundsState.setAspectRatio(ratio); + + final Rect destinationBounds = mPipBoundsAlgorithm.getAdjustedDestinationBounds( + mPipBoundsState.getBounds(), mPipBoundsState.getAspectRatio()); + // Avoid scheduling a resize transition if destination bounds are unchanged, otherise + // we could end up with a no-op transition. + if (!destinationBounds.equals(mPipBoundsState.getBounds())) { + Bundle extra = new Bundle(); + extra.putBoolean(ANIMATING_ASPECT_RATIO_CHANGE, true); + mPipTransitionState.setState(PipTransitionState.SCHEDULED_BOUNDS_CHANGE, extra); + } + } + + @Override + public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, + @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) { + switch (newState) { + case PipTransitionState.SCHEDULED_BOUNDS_CHANGE: + mWaitingForAspectRatioChange = extra.getBoolean(ANIMATING_ASPECT_RATIO_CHANGE); + if (!mWaitingForAspectRatioChange) break; + + mPipScheduler.scheduleAnimateResizePip( + mPipBoundsAlgorithm.getAdjustedDestinationBounds( + mPipBoundsState.getBounds(), mPipBoundsState.getAspectRatio()), + false /* configAtEnd */, ASPECT_RATIO_CHANGE_DURATION); + break; + case PipTransitionState.CHANGING_PIP_BOUNDS: + final SurfaceControl.Transaction startTx = extra.getParcelable( + PipTransition.PIP_START_TX, SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishTx = extra.getParcelable( + PipTransition.PIP_FINISH_TX, SurfaceControl.Transaction.class); + final Rect destinationBounds = extra.getParcelable( + PipTransition.PIP_DESTINATION_BOUNDS, Rect.class); + final int duration = extra.getInt(ANIMATING_BOUNDS_CHANGE_DURATION, + PipTransition.BOUNDS_CHANGE_JUMPCUT_DURATION); + + Preconditions.checkNotNull(mPipTransitionState.mPinnedTaskLeash, + "Leash is null for bounds transition."); + + if (mWaitingForAspectRatioChange) { + PipResizeAnimator animator = new PipResizeAnimator(mContext, + mPipTransitionState.mPinnedTaskLeash, startTx, finishTx, + destinationBounds, + mPipBoundsState.getBounds(), destinationBounds, duration, + 0f /* delta */); + animator.setAnimationEndCallback(() -> { + mPipScheduler.scheduleFinishResizePip( + destinationBounds, false /* configAtEnd */); + }); + animator.start(); + } + break; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java index d75fa00b1fdd..029f001401c5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java @@ -206,7 +206,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha mMenuController.addListener(new PipMenuListener()); mGesture = new DefaultPipTouchGesture(); mMotionHelper = pipMotionHelper; - mMotionHelper.setUpdateMovementBoundsRunnable(this::updateMovementBounds); + mPipScheduler.setUpdateMovementBoundsRunnable(this::updateMovementBounds); mPipDismissTargetHandler = new PipDismissTargetHandler(context, pipUiEventLogger, mMotionHelper, mainExecutor); mTouchState = new PipTouchState(ViewConfiguration.get(context), @@ -219,8 +219,8 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha menuController::hideMenu, mainExecutor); mPipResizeGestureHandler = new PipResizeGestureHandler(context, pipBoundsAlgorithm, - pipBoundsState, mTouchState, mPipScheduler, mPipTransitionState, - this::updateMovementBounds, pipUiEventLogger, menuController, mainExecutor, + pipBoundsState, mTouchState, mPipScheduler, mPipTransitionState, pipUiEventLogger, + menuController, mainExecutor, mPipPerfHintController); mPipBoundsState.addOnAspectRatioChangedCallback(this::updateMinMaxSize); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index ed18712b283d..44baabdd5e2e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -50,10 +50,10 @@ import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUtils; -import com.android.wm.shell.pip.PipContentOverlay; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; import com.android.wm.shell.pip2.animation.PipEnterExitAnimator; +import com.android.wm.shell.shared.pip.PipContentOverlay; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; @@ -87,6 +87,7 @@ public class PipTransition extends PipTransitionController implements // private final Context mContext; + private final PipTaskListener mPipTaskListener; private final PipScheduler mPipScheduler; private final PipTransitionState mPipTransitionState; @@ -118,6 +119,7 @@ public class PipTransition extends PipTransitionController implements PipBoundsState pipBoundsState, PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm, + PipTaskListener pipTaskListener, PipScheduler pipScheduler, PipTransitionState pipTransitionState, PipUiStateChangeController pipUiStateChangeController) { @@ -125,6 +127,7 @@ public class PipTransition extends PipTransitionController implements pipBoundsAlgorithm); mContext = context; + mPipTaskListener = pipTaskListener; mPipScheduler = pipScheduler; mPipScheduler.setPipTransitionController(this); mPipTransitionState = pipTransitionState; @@ -510,6 +513,7 @@ public class PipTransition extends PipTransitionController implements // cache the original task token to check for multi-activity case later final ActivityManager.RunningTaskInfo pipTask = request.getPipTask(); PictureInPictureParams pipParams = pipTask.pictureInPictureParams; + mPipTaskListener.setPictureInPictureParams(pipParams); mPipBoundsState.setBoundsStateForEntry(pipTask.topActivity, pipTask.topActivityInfo, pipParams, mPipBoundsAlgorithm); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl index 4048c5b8feab..ebfd3571ae6d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl @@ -24,7 +24,7 @@ import android.os.Bundle; import android.view.IRecentsAnimationRunner; import com.android.wm.shell.recents.IRecentTasksListener; -import com.android.wm.shell.util.GroupedRecentTaskInfo; +import com.android.wm.shell.shared.GroupedRecentTaskInfo; /** * Interface that is exposed to remote callers to fetch recent tasks. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java index 77b8663861ab..8c5d1e7e069d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java @@ -19,8 +19,8 @@ package com.android.wm.shell.recents; import android.annotation.Nullable; import android.graphics.Color; +import com.android.wm.shell.shared.GroupedRecentTaskInfo; import com.android.wm.shell.shared.annotations.ExternalThread; -import com.android.wm.shell.util.GroupedRecentTaskInfo; import java.util.List; import java.util.concurrent.Executor; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index 2f0af8557538..39bea1bed447 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java @@ -51,16 +51,16 @@ import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.GroupedRecentTaskInfo; import com.android.wm.shell.shared.annotations.ExternalThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.desktopmode.DesktopModeFlags; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.shared.split.SplitBounds; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; -import com.android.wm.shell.util.GroupedRecentTaskInfo; -import com.android.wm.shell.util.SplitBounds; import java.io.PrintWriter; import java.util.ArrayList; 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 7a9eb1c582b7..c90da052dd72 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 @@ -30,7 +30,7 @@ import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION; -import static com.android.wm.shell.util.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS; +import static com.android.wm.shell.shared.split.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS; import android.annotation.Nullable; import android.annotation.SuppressLint; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 0b5c75104f65..95f864a775be 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -133,13 +133,13 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.TransitionUtil; +import com.android.wm.shell.shared.split.SplitBounds; import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.splitscreen.SplitScreen.StageType; import com.android.wm.shell.splitscreen.SplitScreenController.ExitReason; import com.android.wm.shell.transition.DefaultMixedHandler; import com.android.wm.shell.transition.Transitions; -import com.android.wm.shell.util.SplitBounds; import com.android.wm.shell.windowdecor.WindowDecorViewModel; import dalvik.annotation.optimization.NeverCompile; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index a9a4e1093392..4fc6c4489f2b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -18,8 +18,8 @@ package com.android.wm.shell.transition; import static android.app.ActivityOptions.ANIM_CLIP_REVEAL; import static android.app.ActivityOptions.ANIM_CUSTOM; -import static android.app.ActivityOptions.ANIM_FROM_STYLE; import static android.app.ActivityOptions.ANIM_NONE; +import static android.app.ActivityOptions.ANIM_FROM_STYLE; import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS; import static android.app.ActivityOptions.ANIM_SCALE_UP; import static android.app.ActivityOptions.ANIM_SCENE_TRANSITION; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/util/OWNERS deleted file mode 100644 index 482aaab6bc74..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/OWNERS +++ /dev/null @@ -1 +0,0 @@ -per-file KtProtolog.kt = file:platform/development:/tools/winscope/OWNERS 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 20a406f61ed3..1f95667f4e35 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 @@ -97,7 +97,6 @@ import com.android.wm.shell.common.DisplayLayout; 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.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition; @@ -107,6 +106,7 @@ import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.desktopmode.DesktopModeFlags; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreen.StageType; 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 8e87d0ff33c6..2bec3fa6418d 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 @@ -26,7 +26,7 @@ import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_UP; import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT; -import static com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON; +import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize; @@ -79,10 +79,10 @@ import com.android.wm.shell.common.DisplayLayout; 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.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.desktopmode.DesktopModeFlags; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; import com.android.wm.shell.windowdecor.viewholder.AppHandleViewHolder; 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 114c33114421..deef37874e79 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 @@ -318,7 +318,7 @@ class MaximizeMenu( rootView.setOnTouchListener { _, event -> if (event.actionMasked == ACTION_OUTSIDE) { onOutsideTouchListener?.invoke() - false + return@setOnTouchListener false } true } diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt index 8584b599a96c..3fb67cd522c0 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt @@ -24,14 +24,19 @@ import android.tools.flicker.assertors.assertions.AppWindowBecomesVisible import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd import android.tools.flicker.assertors.assertions.AppWindowHasSizeOfAtLeast import android.tools.flicker.assertors.assertions.AppWindowIsInvisibleAtEnd +import android.tools.flicker.assertors.assertions.AppWindowIsVisibleAlways +import android.tools.flicker.assertors.assertions.AppWindowMaintainsAspectRatioAlways import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd import android.tools.flicker.assertors.assertions.AppWindowOnTopAtStart +import android.tools.flicker.assertors.assertions.AppWindowRemainInsideDisplayBounds +import android.tools.flicker.assertors.assertions.AppWindowReturnsToStartBoundsAndPosition import android.tools.flicker.assertors.assertions.LauncherWindowReplacesAppAsTopWindow import android.tools.flicker.config.AssertionTemplates import android.tools.flicker.config.FlickerConfigEntry import android.tools.flicker.config.ScenarioId -import android.tools.flicker.config.desktopmode.Components +import android.tools.flicker.config.desktopmode.Components.DESKTOP_MODE_APP import android.tools.flicker.config.desktopmode.Components.DESKTOP_WALLPAPER +import android.tools.flicker.config.desktopmode.Components.NON_RESIZABLE_APP import android.tools.flicker.extractors.ITransitionMatcher import android.tools.flicker.extractors.ShellTransitionScenarioExtractor import android.tools.flicker.extractors.TaggedCujTransitionMatcher @@ -62,13 +67,11 @@ class DesktopModeFlickerScenarios { assertions = AssertionTemplates.COMMON_ASSERTIONS + listOf( - AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP), - AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP), - AppWindowHasDesktopModeInitialBoundsAtTheEnd( - Components.DESKTOP_MODE_APP - ), - AppWindowBecomesVisible(DESKTOP_WALLPAPER) - ) + AppLayerIsVisibleAlways(DESKTOP_MODE_APP), + AppWindowOnTopAtEnd(DESKTOP_MODE_APP), + AppWindowHasDesktopModeInitialBoundsAtTheEnd(DESKTOP_MODE_APP), + AppWindowBecomesVisible(DESKTOP_WALLPAPER) + ) .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), ) @@ -97,11 +100,10 @@ class DesktopModeFlickerScenarios { assertions = AssertionTemplates.COMMON_ASSERTIONS + listOf( - AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP), - AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP), - AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP), - ) - .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + AppWindowOnTopAtStart(DESKTOP_MODE_APP), + AppLayerIsVisibleAtStart(DESKTOP_MODE_APP), + AppLayerIsInvisibleAtEnd(DESKTOP_MODE_APP) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), ) val CLOSE_LAST_APP = @@ -125,10 +127,10 @@ class DesktopModeFlickerScenarios { assertions = AssertionTemplates.COMMON_ASSERTIONS + listOf( - AppWindowIsInvisibleAtEnd(Components.DESKTOP_MODE_APP), - LauncherWindowReplacesAppAsTopWindow(Components.DESKTOP_MODE_APP), - AppWindowIsInvisibleAtEnd(DESKTOP_WALLPAPER) - ) + AppWindowIsInvisibleAtEnd(DESKTOP_MODE_APP), + LauncherWindowReplacesAppAsTopWindow(DESKTOP_MODE_APP), + AppWindowIsInvisibleAtEnd(DESKTOP_WALLPAPER) + ) .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), ) @@ -156,9 +158,28 @@ class DesktopModeFlickerScenarios { ) .build(), assertions = - AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + - listOf(AppWindowHasSizeOfAtLeast(Components.DESKTOP_MODE_APP, 770, 700)) + AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + + listOf(AppWindowHasSizeOfAtLeast(DESKTOP_MODE_APP, 770, 700)) .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), ) + + val SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE = + FlickerConfigEntry( + scenarioId = ScenarioId("SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE"), + extractor = + TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DESKTOP_MODE_SNAP_RESIZE) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = false) + ) + .build(), + assertions = listOf( + AppWindowIsVisibleAlways(NON_RESIZABLE_APP), + AppWindowOnTopAtEnd(NON_RESIZABLE_APP), + AppWindowRemainInsideDisplayBounds(NON_RESIZABLE_APP), + AppWindowMaintainsAspectRatioAlways(NON_RESIZABLE_APP), + AppWindowReturnsToStartBoundsAndPosition(NON_RESIZABLE_APP) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) } } diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowLeftWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowLeftWithDrag.kt new file mode 100644 index 000000000000..582658f8cb11 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowLeftWithDrag.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE +import com.android.wm.shell.scenarios.SnapResizeAppWindowWithDrag +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Snap resize non-resizable app window by dragging it to the left edge of the screen. + * + * Assert that the app window keeps the same size and returns to its original pre-drag position. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class SnapResizeNonResizableAppWindowLeftWithDrag : + SnapResizeAppWindowWithDrag(toLeft = true, isResizable = false) { + @ExpectedScenarios(["SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE"]) + @Test + override fun snapResizeAppWindowWithDrag() = super.snapResizeAppWindowWithDrag() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT) + .use(SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowRightWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowRightWithDrag.kt new file mode 100644 index 000000000000..7205ec412c99 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/SnapResizeNonResizableAppWindowRightWithDrag.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE +import com.android.wm.shell.scenarios.SnapResizeAppWindowWithDrag +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Snap resize non-resizable app window by dragging it to the right edge of the screen. + * + * Assert that the app window keeps the same size and returns to its original pre-drag position. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class SnapResizeNonResizableAppWindowRightWithDrag : + SnapResizeAppWindowWithDrag(toLeft = false, isResizable = false) { + @ExpectedScenarios(["SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE"]) + @Test + override fun snapResizeAppWindowWithDrag() = super.snapResizeAppWindowWithDrag() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT) + .use(SNAP_RESIZE_WITH_DRAG_NON_RESIZABLE) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index 90e3f7fdb973..1e4b8b62a082 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -881,7 +881,7 @@ public class BackAnimationControllerTest extends ShellTestCase { RemoteAnimationTarget[] targets = new RemoteAnimationTarget[]{animationTarget}; if (mController.mBackAnimationAdapter != null) { mController.mBackAnimationAdapter.getRunner().onAnimationStart( - targets, null, null, mBackAnimationFinishedCallback); + targets, null /* prepareOpenTransition */, mBackAnimationFinishedCallback); mShellExecutor.flushAll(); } } 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 b39cf19a155a..d5287e742c2c 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 @@ -35,9 +35,12 @@ import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; import android.content.Context; import android.content.res.Configuration; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.view.InsetsSource; import android.view.InsetsState; @@ -90,6 +93,9 @@ public class CompatUIControllerTest extends ShellTestCase { public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private CompatUIController mController; private ShellInit mShellInit; @Mock @@ -122,7 +128,6 @@ public class CompatUIControllerTest extends ShellTestCase { private CompatUIConfiguration mCompatUIConfiguration; @Mock private CompatUIShellCommandHandler mCompatUIShellCommandHandler; - @Mock private AccessibilityManager mAccessibilityManager; @@ -132,6 +137,8 @@ public class CompatUIControllerTest extends ShellTestCase { @NonNull private CompatUIStatusManager mCompatUIStatusManager; + private boolean mInDesktopModePredicateResult; + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -157,7 +164,7 @@ public class CompatUIControllerTest extends ShellTestCase { mMockDisplayController, mMockDisplayInsetsController, mMockImeController, mMockSyncQueue, mMockExecutor, mMockTransitionsLazy, mDockStateReader, mCompatUIConfiguration, mCompatUIShellCommandHandler, mAccessibilityManager, - mCompatUIStatusManager) { + mCompatUIStatusManager, i -> mInDesktopModePredicateResult) { @Override CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { @@ -685,6 +692,7 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testLetterboxEduLayout_notCreatedWhenLetterboxEducationIsDisabled() { TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); taskInfo.appCompatTaskInfo.setLetterboxEducationEnabled(false); @@ -695,6 +703,34 @@ public class CompatUIControllerTest extends ShellTestCase { eq(mMockTaskListener)); } + @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) + @EnableFlags(Flags.FLAG_SKIP_COMPAT_UI_EDUCATION_IN_DESKTOP_MODE) + public void testUpdateActiveTaskInfo_removeAllComponentWhenInDesktopModeFlagEnabled() { + mInDesktopModePredicateResult = false; + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); + verify(mController, never()).removeLayouts(taskInfo.taskId); + + mInDesktopModePredicateResult = true; + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); + verify(mController).removeLayouts(taskInfo.taskId); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) + @DisableFlags(Flags.FLAG_SKIP_COMPAT_UI_EDUCATION_IN_DESKTOP_MODE) + public void testUpdateActiveTaskInfo_removeAllComponentWhenInDesktopModeFlagDisabled() { + mInDesktopModePredicateResult = false; + TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); + verify(mController, never()).removeLayouts(taskInfo.taskId); + + mInDesktopModePredicateResult = true; + mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); + verify(mController, never()).removeLayouts(taskInfo.taskId); + } + private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat) { return createTaskInfo(displayId, taskId, hasSizeCompat, /* isVisible */ false, /* isFocused */ false, /* isTopActivityTransparent */ false); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt index 518c00d377ad..db4e93de9541 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt @@ -18,11 +18,6 @@ package com.android.wm.shell.desktopmode import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.APP_FROM_OVERVIEW -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.KEYBOARD_SHORTCUT -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.TASK_DRAG -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.UNKNOWN import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG @@ -33,6 +28,11 @@ import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getEnterTransitionType import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getExitTransitionType +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_FROM_OVERVIEW +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.KEYBOARD_SHORTCUT +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.TASK_DRAG +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith 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 058a26a06f59..d2487209dcef 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 @@ -83,7 +83,6 @@ import com.android.wm.shell.common.LaunchAdjacentController 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.common.desktopmode.DesktopModeTransitionSource.UNKNOWN import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask @@ -94,6 +93,7 @@ import com.android.wm.shell.recents.RecentTasksController import com.android.wm.shell.recents.RecentsTransitionHandler import com.android.wm.shell.recents.RecentsTransitionStateListener import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN import com.android.wm.shell.shared.split.SplitScreenConstants import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.sysui.ShellCommandHandler diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index 16a234b9e2f2..5b028371be2b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -8,6 +8,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.app.WindowConfiguration.WindowingMode import android.graphics.PointF import android.os.IBinder +import android.os.SystemProperties import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.SurfaceControl @@ -16,6 +17,7 @@ import android.window.TransitionInfo import android.window.TransitionInfo.FLAG_IS_WALLPAPER import android.window.WindowContainerTransaction import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.internal.jank.InteractionJankMonitor import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase @@ -29,19 +31,24 @@ import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_END_DRAG import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP import com.android.wm.shell.windowdecor.MoveToDesktopAnimator import java.util.function.Supplier +import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mock +import org.mockito.MockitoSession import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyZeroInteractions import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness /** Tests of [DragToDesktopTransitionHandler]. */ @SmallTest @@ -61,10 +68,12 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { private lateinit var defaultHandler: DragToDesktopTransitionHandler private lateinit var springHandler: SpringDragToDesktopTransitionHandler + private lateinit var mockitoSession: MockitoSession @Before fun setUp() { - defaultHandler = DefaultDragToDesktopTransitionHandler( + defaultHandler = + DefaultDragToDesktopTransitionHandler( context, transitions, taskDisplayAreaOrganizer, @@ -72,7 +81,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { transactionSupplier, ) .apply { setSplitScreenController(splitScreenController) } - springHandler = SpringDragToDesktopTransitionHandler( + springHandler = + SpringDragToDesktopTransitionHandler( context, transitions, taskDisplayAreaOrganizer, @@ -80,6 +90,16 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { transactionSupplier, ) .apply { setSplitScreenController(splitScreenController) } + mockitoSession = + ExtendedMockito.mockitoSession() + .strictness(Strictness.LENIENT) + .mockStatic(SystemProperties::class.java) + .startMocking() + } + + @After + fun tearDown() { + mockitoSession.finishMocking() } @Test @@ -357,6 +377,77 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { verify(finishCallback).onTransitionFinished(null) } + @Test + fun propertyValue_returnsSystemPropertyValue() { + val name = "property_name" + val value = 10f + + whenever(SystemProperties.getInt(eq(systemPropertiesKey(name)), anyInt())) + .thenReturn(value.toInt()) + + assertEquals( + "Expects to return system properties stored value", + /* expected= */ value, + /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue(name) + ) + } + + @Test + fun propertyValue_withScale_returnsScaledSystemPropertyValue() { + val name = "property_name" + val value = 10f + val scale = 100f + + whenever(SystemProperties.getInt(eq(systemPropertiesKey(name)), anyInt())) + .thenReturn(value.toInt()) + + assertEquals( + "Expects to return scaled system properties stored value", + /* expected= */ value / scale, + /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue(name, scale = scale) + ) + } + + @Test + fun propertyValue_notSet_returnsDefaultValue() { + val name = "property_name" + val defaultValue = 50f + + whenever(SystemProperties.getInt(eq(systemPropertiesKey(name)), eq(defaultValue.toInt()))) + .thenReturn(defaultValue.toInt()) + + assertEquals( + "Expects to return the default value", + /* expected= */ defaultValue, + /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue( + name, + default = defaultValue + ) + ) + } + + @Test + fun propertyValue_withScaleNotSet_returnsDefaultValue() { + val name = "property_name" + val defaultValue = 0.5f + val scale = 100f + // Default value is multiplied when provided as a default value for [SystemProperties] + val scaledDefault = (defaultValue * scale).toInt() + + whenever(SystemProperties.getInt(eq(systemPropertiesKey(name)), eq(scaledDefault))) + .thenReturn(scaledDefault) + + assertEquals( + "Expects to return the default value", + /* expected= */ defaultValue, + /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue( + name, + default = defaultValue, + scale = scale + ) + ) + } + private fun startDrag( handler: DragToDesktopTransitionHandler, task: RunningTaskInfo = createTask(), @@ -462,4 +553,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { ) } } + + private fun systemPropertiesKey(name: String) = + "${SpringDragToDesktopTransitionHandler.SYSTEM_PROPERTIES_GROUP}.$name" } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java index e5157c974e2d..e0463b41ad20 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java @@ -48,7 +48,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.transition.Transitions; import org.junit.Before; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt index 6736593bba5b..0c3f98a324cd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedRecentTaskInfoTest.kt @@ -24,13 +24,13 @@ import android.window.IWindowContainerToken import android.window.WindowContainerToken import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.shared.GroupedRecentTaskInfo +import com.android.wm.shell.shared.GroupedRecentTaskInfo.CREATOR +import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_FREEFORM +import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SINGLE +import com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_SPLIT +import com.android.wm.shell.shared.split.SplitBounds import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50 -import com.android.wm.shell.util.GroupedRecentTaskInfo -import com.android.wm.shell.util.GroupedRecentTaskInfo.CREATOR -import com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_FREEFORM -import com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_SINGLE -import com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_SPLIT -import com.android.wm.shell.util.SplitBounds import com.google.common.truth.Correspondence import com.google.common.truth.Truth.assertThat import org.junit.Assert.assertThrows diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java index e1fe4e9054c0..a8d40db096dd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java @@ -68,13 +68,13 @@ import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; +import com.android.wm.shell.shared.GroupedRecentTaskInfo; import com.android.wm.shell.shared.ShellSharedConstants; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.shared.split.SplitBounds; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; -import com.android.wm.shell.util.GroupedRecentTaskInfo; -import com.android.wm.shell.util.SplitBounds; import org.junit.After; import org.junit.Before; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java index bfb760b6fc8c..248393cef9ae 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/SplitBoundsTest.java @@ -12,7 +12,7 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.util.SplitBounds; +import com.android.wm.shell.shared.split.SplitBounds; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index f7ac3e416938..da0aca7b3b0f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -79,12 +79,12 @@ import com.android.wm.shell.common.DisplayLayout 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.common.desktopmode.DesktopModeTransitionSource import com.android.wm.shell.desktopmode.DesktopTasksController import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition import com.android.wm.shell.desktopmode.DesktopTasksLimiter import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController diff --git a/libs/hwui/FeatureFlags.h b/libs/hwui/FeatureFlags.h index ac75c077b58f..c1c30f5379ab 100644 --- a/libs/hwui/FeatureFlags.h +++ b/libs/hwui/FeatureFlags.h @@ -49,6 +49,15 @@ inline bool letter_spacing_justification() { #endif // __ANDROID__ } +inline bool typeface_redesign() { +#ifdef __ANDROID__ + static bool flag = com_android_text_flags_typeface_redesign(); + return flag; +#else + return true; +#endif // __ANDROID__ +} + } // namespace text_feature } // namespace android diff --git a/libs/hwui/hwui/MinikinUtils.h b/libs/hwui/hwui/MinikinUtils.h index f8574ee50525..1510ce1378d8 100644 --- a/libs/hwui/hwui/MinikinUtils.h +++ b/libs/hwui/hwui/MinikinUtils.h @@ -27,6 +27,8 @@ #include <cutils/compiler.h> #include <log/log.h> #include <minikin/Layout.h> + +#include "FeatureFlags.h" #include "MinikinSkia.h" #include "Paint.h" #include "Typeface.h" @@ -71,27 +73,42 @@ public: static void forFontRun(const minikin::Layout& layout, Paint* paint, F& f) { float saveSkewX = paint->getSkFont().getSkewX(); bool savefakeBold = paint->getSkFont().isEmbolden(); - const minikin::MinikinFont* curFont = nullptr; - size_t start = 0; - size_t nGlyphs = layout.nGlyphs(); - for (size_t i = 0; i < nGlyphs; i++) { - const minikin::MinikinFont* nextFont = layout.typeface(i).get(); - if (i > 0 && nextFont != curFont) { + if (text_feature::typeface_redesign()) { + for (uint32_t runIdx = 0; runIdx < layout.getFontRunCount(); ++runIdx) { + uint32_t start = layout.getFontRunStart(runIdx); + uint32_t end = layout.getFontRunEnd(runIdx); + const minikin::FakedFont& fakedFont = layout.getFontRunFont(runIdx); + + std::shared_ptr<minikin::MinikinFont> font = fakedFont.typeface(); + SkFont* skfont = &paint->getSkFont(); + MinikinFontSkia::populateSkFont(skfont, font.get(), fakedFont.fakery); + f(start, end); + skfont->setSkewX(saveSkewX); + skfont->setEmbolden(savefakeBold); + } + } else { + const minikin::MinikinFont* curFont = nullptr; + size_t start = 0; + size_t nGlyphs = layout.nGlyphs(); + for (size_t i = 0; i < nGlyphs; i++) { + const minikin::MinikinFont* nextFont = layout.typeface(i).get(); + if (i > 0 && nextFont != curFont) { + SkFont* skfont = &paint->getSkFont(); + MinikinFontSkia::populateSkFont(skfont, curFont, layout.getFakery(start)); + f(start, i); + skfont->setSkewX(saveSkewX); + skfont->setEmbolden(savefakeBold); + start = i; + } + curFont = nextFont; + } + if (nGlyphs > start) { SkFont* skfont = &paint->getSkFont(); MinikinFontSkia::populateSkFont(skfont, curFont, layout.getFakery(start)); - f(start, i); + f(start, nGlyphs); skfont->setSkewX(saveSkewX); skfont->setEmbolden(savefakeBold); - start = i; } - curFont = nextFont; - } - if (nGlyphs > start) { - SkFont* skfont = &paint->getSkFont(); - MinikinFontSkia::populateSkFont(skfont, curFont, layout.getFakery(start)); - f(start, nGlyphs); - skfont->setSkewX(saveSkewX); - skfont->setEmbolden(savefakeBold); } } }; diff --git a/libs/hwui/tests/common/TestContext.cpp b/libs/hwui/tests/common/TestContext.cpp index fd596d998dfd..e427c97e41fa 100644 --- a/libs/hwui/tests/common/TestContext.cpp +++ b/libs/hwui/tests/common/TestContext.cpp @@ -16,6 +16,7 @@ #include "tests/common/TestContext.h" +#include <com_android_graphics_libgui_flags.h> #include <cutils/trace.h> namespace android { @@ -101,6 +102,14 @@ void TestContext::createWindowSurface() { } void TestContext::createOffscreenSurface() { +#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) + mConsumer = new BufferItemConsumer(GRALLOC_USAGE_HW_COMPOSER, 4); + const ui::Size& resolution = getActiveDisplayResolution(); + mConsumer->setDefaultBufferSize(resolution.getWidth(), resolution.getHeight()); + mSurface = mConsumer->getSurface(); + mSurface->setMaxDequeuedBufferCount(3); + mSurface->setAsyncMode(true); +#else sp<IGraphicBufferProducer> producer; sp<IGraphicBufferConsumer> consumer; BufferQueue::createBufferQueue(&producer, &consumer); @@ -110,6 +119,7 @@ void TestContext::createOffscreenSurface() { const ui::Size& resolution = getActiveDisplayResolution(); mConsumer->setDefaultBufferSize(resolution.getWidth(), resolution.getHeight()); mSurface = new Surface(producer); +#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) } void TestContext::waitForVsync() { @@ -144,4 +154,4 @@ void TestContext::waitForVsync() { } // namespace test } // namespace uirenderer -} // namespace android +} // namespace android
\ No newline at end of file diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index ca468fc1ff44..029e6f49b062 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -10128,6 +10128,24 @@ public class AudioManager { /** * @hide + * Blocks until permission updates have propagated through the audio system. + * Only useful in tests, where adoptShellPermissions can change the permission state of + * an app without the app being killed. + */ + @TestApi + @SuppressWarnings("UnflaggedApi") // @TestApi without associated feature. + public void permissionUpdateBarrier() { + final IAudioService service = getService(); + try { + service.permissionUpdateBarrier(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** + * @hide * Return the list of independent stream types for volume control. * A stream type is considered independent when the volume changes of that type do not * affect any other independent volume control stream type. diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index d20b7f090e92..e0c346100d03 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -101,6 +101,8 @@ interface IAudioService { oneway void portEvent(in int portId, in int event, in @nullable PersistableBundle extras); + void permissionUpdateBarrier(); + // Java-only methods below. void adjustStreamVolume(int streamType, int direction, int flags, String callingPackage); diff --git a/media/java/android/media/tv/tuner/Tuner.java b/media/java/android/media/tv/tuner/Tuner.java index 2c71ee01b3f1..d14275ff2fd6 100644 --- a/media/java/android/media/tv/tuner/Tuner.java +++ b/media/java/android/media/tv/tuner/Tuner.java @@ -18,6 +18,7 @@ package android.media.tv.tuner; import android.annotation.BytesLong; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; @@ -32,6 +33,7 @@ import android.hardware.tv.tuner.Constant64Bit; import android.hardware.tv.tuner.FrontendScanType; import android.media.MediaCodec; import android.media.tv.TvInputService; +import android.media.tv.flags.Flags; import android.media.tv.tuner.dvr.DvrPlayback; import android.media.tv.tuner.dvr.DvrRecorder; import android.media.tv.tuner.dvr.OnPlaybackStatusChangedListener; @@ -2529,6 +2531,50 @@ public class Tuner implements AutoCloseable { } /** + * Request a frontend by frontend type. + * + * <p> This API is used if the applications want to select a frontend with desired type when + * there are multiple frontends of the same type is there before {@link tune}. The applied + * frontend will be one of the not in-use frontend. If all frontends are in-use, this API will + * reclaim and apply the frontend owned by the lowest priority client if current client has + * higher priority. Otherwise, this API will not apply any frontend and return + * {@link #RESULT_UNAVAILABLE}. + * + * @param desiredFrontendType the Type of the desired fronted. Should be one of + * {@link android.media.tv.tuner.frontend.FrontendSettings.Type} + * @return result status of open operation. + */ + @Result + @FlaggedApi(Flags.FLAG_TUNER_W_APIS) + @RequiresPermission( + allOf = {"android.permission.TUNER_RESOURCE_ACCESS", "android.permission.ACCESS_TV_TUNER"}) + public int applyFrontendByType(@FrontendSettings.Type int desiredFrontendType) { + mFrontendLock.lock(); + try { + if (mFeOwnerTuner != null) { + Log.e(TAG, "Operation connot be done by sharee of tuner"); + return RESULT_INVALID_STATE; + } + if (mFrontendHandle != null) { + Log.e(TAG, "A frontend has been opened before"); + return RESULT_INVALID_STATE; + } + + mDesiredFrontendId = null; + mFrontendType = desiredFrontendType; + if (DEBUG) { + Log.d(TAG, "Applying frontend with type " + mFrontendType); + } + if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND, mFrontendLock)) { + return RESULT_UNAVAILABLE; + } + return RESULT_SUCCESS; + } finally { + mFrontendLock.unlock(); + } + } + + /** * Open a shared filter instance. * * @param context the context of the caller. diff --git a/media/jni/android_media_ImageReader.cpp b/media/jni/android_media_ImageReader.cpp index 371e3d2deda5..019b1e0de4d6 100644 --- a/media/jni/android_media_ImageReader.cpp +++ b/media/jni/android_media_ImageReader.cpp @@ -17,35 +17,31 @@ //#define LOG_NDEBUG 0 #define LOG_TAG "ImageReader_JNI" #define ATRACE_TAG ATRACE_TAG_CAMERA -#include "android_media_Utils.h" -#include <cutils/atomic.h> -#include <utils/Log.h> -#include <utils/misc.h> -#include <utils/List.h> -#include <utils/Trace.h> -#include <utils/String8.h> - -#include <cstdio> - -#include <gui/BufferItemConsumer.h> -#include <gui/Surface.h> - +#include <android/hardware_buffer_jni.h> #include <android_runtime/AndroidRuntime.h> -#include <android_runtime/android_view_Surface.h> #include <android_runtime/android_graphics_GraphicBuffer.h> #include <android_runtime/android_hardware_HardwareBuffer.h> +#include <android_runtime/android_view_Surface.h> +#include <com_android_graphics_libgui_flags.h> +#include <cutils/atomic.h> #include <grallocusage/GrallocUsageConversion.h> - -#include <private/android/AHardwareBufferHelpers.h> - +#include <gui/BufferItemConsumer.h> +#include <gui/Surface.h> +#include <inttypes.h> #include <jni.h> #include <nativehelper/JNIHelp.h> - +#include <private/android/AHardwareBufferHelpers.h> #include <stdint.h> -#include <inttypes.h> -#include <android/hardware_buffer_jni.h> - #include <ui/Rect.h> +#include <utils/List.h> +#include <utils/Log.h> +#include <utils/String8.h> +#include <utils/Trace.h> +#include <utils/misc.h> + +#include <cstdio> + +#include "android_media_Utils.h" #define ANDROID_MEDIA_IMAGEREADER_CTX_JNI_ID "mNativeContext" #define ANDROID_MEDIA_SURFACEIMAGE_BUFFER_JNI_ID "mNativeBuffer" @@ -393,18 +389,25 @@ static void ImageReader_init(JNIEnv* env, jobject thiz, jobject weakThiz, jint w } sp<JNIImageReaderContext> ctx(new JNIImageReaderContext(env, weakThiz, clazz, maxImages)); - sp<IGraphicBufferProducer> gbProducer; - sp<IGraphicBufferConsumer> gbConsumer; - BufferQueue::createBufferQueue(&gbProducer, &gbConsumer); - sp<BufferItemConsumer> bufferConsumer; String8 consumerName = String8::format("ImageReader-%dx%df%xm%d-%d-%d", width, height, nativeHalFormat, maxImages, getpid(), createProcessUniqueId()); uint64_t consumerUsage = android_hardware_HardwareBuffer_convertToGrallocUsageBits(ndkUsage); +#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) + sp<BufferItemConsumer> bufferConsumer = new BufferItemConsumer(consumerUsage, maxImages, + /*controlledByApp*/ true); + sp<IGraphicBufferProducer> gbProducer = + bufferConsumer->getSurface()->getIGraphicBufferProducer(); +#else + sp<IGraphicBufferProducer> gbProducer; + sp<IGraphicBufferConsumer> gbConsumer; + BufferQueue::createBufferQueue(&gbProducer, &gbConsumer); + sp<BufferItemConsumer> bufferConsumer; bufferConsumer = new BufferItemConsumer(gbConsumer, consumerUsage, maxImages, /*controlledByApp*/true); +#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) if (bufferConsumer == nullptr) { jniThrowExceptionFmt(env, "java/lang/RuntimeException", "Failed to allocate native buffer consumer for hal format 0x%x and usage 0x%x", @@ -413,7 +416,11 @@ static void ImageReader_init(JNIEnv* env, jobject thiz, jobject weakThiz, jint w } if (consumerUsage & GRALLOC_USAGE_PROTECTED) { +#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) + bufferConsumer->setConsumerIsProtected(true); +#else gbConsumer->setConsumerIsProtected(true); +#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) } ctx->setBufferConsumer(bufferConsumer); diff --git a/media/mca/filterfw/native/core/gl_env.cpp b/media/mca/filterfw/native/core/gl_env.cpp index 1bb82f88ba48..4637ccdfad15 100644 --- a/media/mca/filterfw/native/core/gl_env.cpp +++ b/media/mca/filterfw/native/core/gl_env.cpp @@ -15,21 +15,23 @@ */ // #define LOG_NDEBUG 0 -#include "base/logging.h" -#include "base/utilities.h" #include "core/gl_env.h" -#include "core/shader_program.h" -#include "core/vertex_frame.h" -#include "system/window.h" -#include <map> -#include <string> #include <EGL/eglext.h> - +#include <com_android_graphics_libgui_flags.h> #include <gui/BufferQueue.h> -#include <gui/Surface.h> #include <gui/GLConsumer.h> #include <gui/IGraphicBufferProducer.h> +#include <gui/Surface.h> + +#include <map> +#include <string> + +#include "base/logging.h" +#include "base/utilities.h" +#include "core/shader_program.h" +#include "core/vertex_frame.h" +#include "system/window.h" namespace android { namespace filterfw { @@ -165,12 +167,18 @@ bool GLEnv::InitWithNewContext() { } // Create dummy surface using a GLConsumer +#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) + surfaceTexture_ = new GLConsumer(0, GLConsumer::TEXTURE_EXTERNAL, /*useFenceSync=*/true, + /*isControlledByApp=*/false); + window_ = surfaceTexture_->getSurface(); +#else sp<IGraphicBufferProducer> producer; sp<IGraphicBufferConsumer> consumer; BufferQueue::createBufferQueue(&producer, &consumer); surfaceTexture_ = new GLConsumer(consumer, 0, GLConsumer::TEXTURE_EXTERNAL, true, false); window_ = new Surface(producer); +#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ) surfaces_[0] = SurfaceWindowPair(eglCreateWindowSurface(display(), config, window_.get(), NULL), NULL); if (CheckEGLError("eglCreateWindowSurface")) return false; diff --git a/media/tests/mediatestutils/Android.bp b/media/tests/mediatestutils/Android.bp index 88938e2f71d0..8c68f210ce3d 100644 --- a/media/tests/mediatestutils/Android.bp +++ b/media/tests/mediatestutils/Android.bp @@ -27,9 +27,11 @@ java_library { name: "mediatestutils", srcs: [ "java/com/android/media/mediatestutils/TestUtils.java", + "java/com/android/media/mediatestutils/PermissionUpdateBarrierRule.java", ], static_libs: [ "androidx.concurrent_concurrent-futures", + "androidx.test.runner", "guava", "mediatestutils_host", ], diff --git a/media/tests/mediatestutils/java/com/android/media/mediatestutils/PermissionUpdateBarrierRule.java b/media/tests/mediatestutils/java/com/android/media/mediatestutils/PermissionUpdateBarrierRule.java new file mode 100644 index 000000000000..c51b5dea9546 --- /dev/null +++ b/media/tests/mediatestutils/java/com/android/media/mediatestutils/PermissionUpdateBarrierRule.java @@ -0,0 +1,59 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.media.mediatestutils; + +import android.content.Context; +import android.media.AudioManager; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * Barrier to wait for permission updates to propagate to audioserver, to avoid flakiness when using + * {@code com.android.compatability.common.util.AdoptShellPermissionsRule}. Note, this rule should + * <b> always </b> be placed after the adopt permission rule. Don't use rule when changing + * permission state in {@code @Before}, since that executes after all rules. + */ +public class PermissionUpdateBarrierRule implements TestRule { + + private final Context mContext; + + /** + * @param context the context to use + */ + public PermissionUpdateBarrierRule(Context context) { + mContext = context; + } + + public PermissionUpdateBarrierRule() { + this(InstrumentationRegistry.getInstrumentation().getContext()); + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + mContext.getSystemService(AudioManager.class).permissionUpdateBarrier(); + base.evaluate(); + } + }; + } +} diff --git a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml index a0b34690696f..08155dd3e09a 100644 --- a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml +++ b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml @@ -133,7 +133,8 @@ <LinearLayout android:id="@+id/negative_multiple_devices_layout" android:layout_width="wrap_content" - android:layout_height="48dp" + android:layout_height="match_parent" + android:padding="6dp" android:gravity="center" android:visibility="gone"> diff --git a/packages/CompanionDeviceManager/res/values/styles.xml b/packages/CompanionDeviceManager/res/values/styles.xml index e8e24f492005..fe7cfc64603a 100644 --- a/packages/CompanionDeviceManager/res/values/styles.xml +++ b/packages/CompanionDeviceManager/res/values/styles.xml @@ -103,11 +103,10 @@ <style name="NegativeButtonMultipleDevices" parent="@android:style/Widget.Material.Button.Colored"> <item name="android:layout_width">wrap_content</item> - <item name="android:layout_height">36dp</item> + <item name="android:layout_height">match_parent</item> + <item name="android:minHeight">36dp</item>> <item name="android:textAllCaps">false</item> <item name="android:textSize">14sp</item> - <item name="android:paddingLeft">6dp</item> - <item name="android:paddingRight">6dp</item> <item name="android:background">@drawable/btn_negative_multiple_devices</item> <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item> </style> diff --git a/packages/EasterEgg/src/com/android/egg/paint/PaintActivity.java b/packages/EasterEgg/src/com/android/egg/paint/PaintActivity.java index ac47fbda09c6..391b16d38f0f 100644 --- a/packages/EasterEgg/src/com/android/egg/paint/PaintActivity.java +++ b/packages/EasterEgg/src/com/android/egg/paint/PaintActivity.java @@ -23,7 +23,6 @@ import static android.view.MotionEvent.ACTION_UP; import android.app.Activity; import android.content.res.Configuration; -import android.graphics.Bitmap; import android.graphics.Color; import android.os.Bundle; import android.view.MotionEvent; @@ -38,9 +37,7 @@ import android.widget.Magnifier; import com.android.egg.R; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.stream.IntStream; public class PaintActivity extends Activity { @@ -60,31 +57,28 @@ public class PaintActivity extends Activity { private View.OnClickListener buttonHandler = new View.OnClickListener() { @Override public void onClick(View view) { - switch (view.getId()) { - case R.id.btnBrush: - view.setSelected(true); - hideToolbar(colors); - toggleToolbar(brushes); - break; - case R.id.btnColor: - view.setSelected(true); - hideToolbar(brushes); - toggleToolbar(colors); - break; - case R.id.btnClear: - painting.clear(); - break; - case R.id.btnSample: - sampling = true; - view.setSelected(true); - break; - case R.id.btnZen: - painting.setZenMode(!painting.getZenMode()); - view.animate() - .setStartDelay(200) - .setInterpolator(new OvershootInterpolator()) - .rotation(painting.getZenMode() ? 0f : 90f); - break; + // With non final fields in the R class we can't switch on the + // id since the case values are no longer constants. + int viewId = view.getId(); + if (viewId == R.id.btnBrush) { + view.setSelected(true); + hideToolbar(colors); + toggleToolbar(brushes); + } else if (viewId == R.id.btnColor) { + view.setSelected(true); + hideToolbar(brushes); + toggleToolbar(colors); + } else if (viewId == R.id.btnClear) { + painting.clear(); + } else if (viewId == R.id.btnSample) { + sampling = true; + view.setSelected(true); + } else if (viewId == R.id.btnZen) { + painting.setZenMode(!painting.getZenMode()); + view.animate() + .setStartDelay(200) + .setInterpolator(new OvershootInterpolator()) + .rotation(painting.getZenMode() ? 0f : 90f); } } }; diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java index d36b55f8961a..9fa8fc3cc647 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java @@ -286,8 +286,7 @@ public class ZenMode implements Parcelable { /** * Returns the mode icon -- which can be either app-provided (via {@code addAutomaticZenRule}), - * user-chosen (via the icon picker in Settings), the app's launcher icon for implicit rules - * (in its monochrome variant, if available), or a default icon based on the mode type. + * user-chosen (via the icon picker in Settings), or a default icon based on the mode type. */ @NonNull public ListenableFuture<Drawable> getIcon(@NonNull Context context, @@ -300,23 +299,6 @@ public class ZenMode implements Parcelable { return iconLoader.getIcon(context, mRule); } - /** - * Returns an alternative mode icon. The difference with {@link #getIcon} is that it's the - * basic DND icon not only for Manual DND, but also for <em>implicit rules</em>. As such, it's - * suitable for places where showing the launcher icon of an app could be confusing, such as - * the status bar or lockscreen. - */ - @NonNull - public ListenableFuture<Drawable> getLockscreenIcon(@NonNull Context context, - @NonNull ZenIconLoader iconLoader) { - if (mKind == Kind.MANUAL_DND || mKind == Kind.IMPLICIT) { - return Futures.immediateFuture(requireNonNull( - context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp))); - } - - return iconLoader.getIcon(context, mRule); - } - @NonNull public ZenPolicy getPolicy() { switch (mRule.getInterruptionFilter()) { diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java index 6198d80cefe6..d71b337228f6 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java @@ -181,7 +181,7 @@ public class CreateUserDialogController { * admin status. */ public Dialog createDialog(Activity activity, - ActivityStarter activityStarter, boolean canCreateAdminUser, + @NonNull ActivityStarter activityStarter, boolean canCreateAdminUser, NewUserData successCallback, Runnable cancelCallback) { mActivity = activity; mCustomDialogHelper = new CustomDialogHelper(activity); diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java index 46f229035839..c4c4ed8e7b91 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java @@ -31,6 +31,7 @@ import android.view.WindowManager; import android.widget.EditText; import android.widget.ImageView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -126,9 +127,11 @@ public class EditUserInfoController { * @param activityStarter - ActivityStarter is called with appropriate intents and request * codes to take photo/choose photo/crop photo. */ - public Dialog createDialog(Activity activity, ActivityStarter activityStarter, - @Nullable Drawable oldUserIcon, String defaultUserName, - BiConsumer<String, Drawable> successCallback, Runnable cancelCallback) { + public @NonNull Dialog createDialog(@NonNull Activity activity, + @NonNull ActivityStarter activityStarter, @Nullable Drawable oldUserIcon, + @Nullable String defaultUserName, + @Nullable BiConsumer<String, Drawable> successCallback, + @Nullable Runnable cancelCallback) { LayoutInflater inflater = LayoutInflater.from(activity); View content = inflater.inflate(R.layout.edit_user_info_dialog_content, null); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/OWNERS b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/OWNERS new file mode 100644 index 000000000000..134a56ecb27e --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/OWNERS @@ -0,0 +1 @@ +include /packages/SettingsLib/src/com/android/settingslib/bluetooth/OWNERS diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java index f533e77bb33e..32216fadfb0d 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java @@ -322,41 +322,6 @@ public class ZenModeTest { verify(iconLoader).getIcon(any(), eq(IMPLICIT_ZEN_RULE)); } - @Test - public void getLockscreenIcon_normalMode_loadsIconNormally() { - ZenIconLoader iconLoader = mock(ZenIconLoader.class); - ZenMode mode = new ZenMode("id", ZEN_RULE, zenConfigRuleFor(ZEN_RULE, false)); - - ListenableFuture<Drawable> unused = mode.getLockscreenIcon( - RuntimeEnvironment.getApplication(), iconLoader); - - verify(iconLoader).getIcon(any(), eq(ZEN_RULE)); - } - - @Test - public void getLockscreenIcon_manualDnd_returnsFixedIcon() { - ZenIconLoader iconLoader = mock(ZenIconLoader.class); - - ListenableFuture<Drawable> future = TestModeBuilder.MANUAL_DND_INACTIVE.getLockscreenIcon( - RuntimeEnvironment.getApplication(), iconLoader); - - assertThat(future.isDone()).isTrue(); - verify(iconLoader, never()).getIcon(any(), any()); - } - - @Test - public void getLockscreenIcon_implicitMode_returnsFixedIcon() { - ZenIconLoader iconLoader = mock(ZenIconLoader.class); - ZenMode mode = new ZenMode(IMPLICIT_RULE_ID, IMPLICIT_ZEN_RULE, - zenConfigRuleFor(IMPLICIT_ZEN_RULE, false)); - - ListenableFuture<Drawable> future = mode.getLockscreenIcon( - RuntimeEnvironment.getApplication(), iconLoader); - - assertThat(future.isDone()).isTrue(); - verify(iconLoader, never()).getIcon(any(), any()); - } - private static void assertUnparceledIsEqualToOriginal(String type, ZenMode original) { Parcel parcel = Parcel.obtain(); try { diff --git a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig index 006e644b2ac7..62401a124ad1 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig +++ b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig @@ -81,3 +81,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "sync_local_overrides_removal_new_storage" + namespace: "core_experiments_team_internal" + description: "When DeviceConfig overrides are deleted, delete new storage overrides too." + bug: "361643653" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 0b364ac4a176..8a1d81be5e11 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1032,6 +1032,16 @@ flag { } flag { + name: "communal_edit_widgets_activity_finish_fix" + namespace: "systemui" + description: "finish edit widgets activity when stopping" + bug: "354725145" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "app_clips_backlinks" namespace: "systemui" description: "Enables Backlinks improvement feature in App Clips" @@ -1083,6 +1093,13 @@ flag { } flag { + name: "media_controls_posts_optimization" + namespace: "systemui" + description: "Ignore duplicate media notifications posted" + bug: "358645640" +} + +flag { namespace: "systemui" name: "enable_view_capture_tracing" description: "Enables view capture tracing in System UI." @@ -1103,16 +1120,6 @@ flag { } flag { - name: "glanceable_hub_back_gesture" - namespace: "systemui" - description: "Enables back gesture on the glanceable hub" - bug: "346331399" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "glanceable_hub_allow_keyguard_when_dreaming" namespace: "systemui" description: "Allows users to exit dream to keyguard with glanceable hub enabled" @@ -1367,3 +1374,13 @@ flag { } } +flag { + name: "media_load_metadata_via_media_data_loader" + namespace: "systemui" + description: "Use MediaDataLoader for loading media metadata with better threading" + bug: "358350077" + metadata { + purpose: PURPOSE_BUGFIX + } +} + diff --git a/packages/SystemUI/animation/build.gradle b/packages/SystemUI/animation/build.gradle index 939455fa44ac..16ba42f862f7 100644 --- a/packages/SystemUI/animation/build.gradle +++ b/packages/SystemUI/animation/build.gradle @@ -10,13 +10,6 @@ android { } } - compileSdk 33 - - defaultConfig { - minSdk 33 - targetSdk 33 - } - lintOptions { abortOnError false } @@ -24,10 +17,6 @@ android { tasks.withType(JavaCompile) { options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" } - kotlinOptions { - jvmTarget = '1.8' - freeCompilerArgs = ["-Xjvm-default=all"] - } } dependencies { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt index aeba67bd121c..5a4020d3b1fb 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt @@ -31,6 +31,7 @@ import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneActionsViewModel import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.ComposableScene @@ -56,7 +57,7 @@ constructor( private val actionsViewModelFactory: BouncerSceneActionsViewModel.Factory, private val contentViewModelFactory: BouncerSceneContentViewModel.Factory, private val dialogFactory: BouncerDialogFactory, -) : ComposableScene { +) : ExclusiveActivatable(), ComposableScene { override val key = Scenes.Bouncer private val actionsViewModel: BouncerSceneActionsViewModel by lazy { @@ -66,7 +67,7 @@ constructor( override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions - override suspend fun activate(): Nothing { + override suspend fun onActivated(): Nothing { actionsViewModel.activate() } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 872bef256f3a..ed1277666372 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -31,7 +31,6 @@ import androidx.compose.ui.semantics.disabled import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.android.compose.animation.scene.Back import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.ElementKey @@ -47,7 +46,6 @@ import com.android.compose.animation.scene.observableTransitionState import com.android.compose.animation.scene.transitions import com.android.compose.theme.LocalAndroidColorScheme import com.android.internal.R.attr.focusable -import com.android.systemui.Flags.glanceableHubBackGesture import com.android.systemui.communal.shared.model.CommunalBackgroundType import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.CommunalTransitionKeys @@ -198,15 +196,7 @@ fun CommunalContainer( Box(modifier = Modifier.fillMaxSize()) } - val userActions = - if (glanceableHubBackGesture()) { - mapOf( - Swipe(SwipeDirection.End) to CommunalScenes.Blank, - Back to CommunalScenes.Blank, - ) - } else { - mapOf(Swipe(SwipeDirection.End) to CommunalScenes.Blank) - } + val userActions = mapOf(Swipe(SwipeDirection.End) to CommunalScenes.Blank) scene(CommunalScenes.Communal, userActions = userActions) { CommunalScene( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 91a88bc7aa97..b0590e06d3bd 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -545,18 +545,35 @@ private fun ScrollOnUpdatedLiveContentEffect( ) { val coroutineScope = rememberCoroutineScope() val liveContentKeys = remember { mutableListOf<String>() } + var communalContentPending by remember { mutableStateOf(true) } LaunchedEffect(communalContent) { + // Do nothing until any communal content comes in + if (communalContentPending && communalContent.isEmpty()) { + return@LaunchedEffect + } + val prevLiveContentKeys = liveContentKeys.toList() + val newLiveContentKeys = communalContent.filter { it.isLiveContent() }.map { it.key } liveContentKeys.clear() - liveContentKeys.addAll(communalContent.filter { it.isLiveContent() }.map { it.key }) + liveContentKeys.addAll(newLiveContentKeys) - // Find the first updated content + // Do nothing on first communal content since we don't have a delta + if (communalContentPending) { + communalContentPending = false + return@LaunchedEffect + } + + // Do nothing if there is no new live content val indexOfFirstUpdatedContent = - liveContentKeys.indexOfFirst { !prevLiveContentKeys.contains(it) } + newLiveContentKeys.indexOfFirst { !prevLiveContentKeys.contains(it) } + if (indexOfFirstUpdatedContent < 0) { + return@LaunchedEffect + } - // Scroll if current position is behind the first updated content - if (indexOfFirstUpdatedContent in 0 until gridState.firstVisibleItemIndex) { + // Scroll if the live content is not visible + val lastVisibleItemIndex = gridState.layoutInfo.visibleItemsInfo.lastOrNull()?.index + if (lastVisibleItemIndex != null && indexOfFirstUpdatedContent > lastVisibleItemIndex) { // Launching with a scope to prevent the job from being canceled in the case of a // recomposition during scrolling coroutineScope.launch { gridState.animateScrollToItem(indexOfFirstUpdatedContent) } @@ -1154,7 +1171,7 @@ private fun WidgetContent( .then(selectableModifier) .thenIf(!viewModel.isEditMode && !model.inQuietMode) { Modifier.pointerInput(Unit) { - observeTaps { viewModel.onTapWidget(model.componentName, model.priority) } + observeTaps { viewModel.onTapWidget(model.componentName, model.rank) } } } .thenIf(!viewModel.isEditMode && model.inQuietMode) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt index 6750e41009c7..54ffcf475680 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt @@ -27,10 +27,12 @@ import com.android.systemui.communal.ui.view.layout.sections.CommunalAppWidgetSe import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.widgets.WidgetInteractionHandler import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.ComposableScene import com.android.systemui.statusbar.phone.SystemUIDialogFactory import javax.inject.Inject +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -44,7 +46,7 @@ constructor( private val dialogFactory: SystemUIDialogFactory, private val interactionHandler: WidgetInteractionHandler, private val widgetSection: CommunalAppWidgetSection, -) : ComposableScene { +) : ExclusiveActivatable(), ComposableScene { override val key = Scenes.Communal override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = @@ -55,6 +57,10 @@ constructor( ) .asStateFlow() + override suspend fun onActivated(): Nothing { + awaitCancellation() + } + @Composable override fun SceneScope.Content(modifier: Modifier) { CommunalHub( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt index 38a347465318..11373577eedb 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt @@ -34,11 +34,11 @@ fun rememberContentListState( return remember(communalContent) { ContentListState( communalContent, - { componentName, user, priority -> + { componentName, user, rank -> viewModel.onAddWidget( componentName, user, - priority, + rank, widgetConfigurator, ) }, @@ -56,10 +56,9 @@ fun rememberContentListState( class ContentListState internal constructor( communalContent: List<CommunalContentModel>, - private val onAddWidget: - (componentName: ComponentName, user: UserHandle, priority: Int) -> Unit, - private val onDeleteWidget: (id: Int, componentName: ComponentName, priority: Int) -> Unit, - private val onReorderWidgets: (widgetIdToPriorityMap: Map<Int, Int>) -> Unit, + private val onAddWidget: (componentName: ComponentName, user: UserHandle, rank: Int) -> Unit, + private val onDeleteWidget: (id: Int, componentName: ComponentName, rank: Int) -> Unit, + private val onReorderWidgets: (widgetIdToRankMap: Map<Int, Int>) -> Unit, ) { var list = communalContent.toMutableStateList() private set @@ -74,7 +73,7 @@ internal constructor( if (list[indexToRemove].isWidgetContent()) { val widget = list[indexToRemove] as CommunalContentModel.WidgetContent list.apply { removeAt(indexToRemove) } - onDeleteWidget(widget.appWidgetId, widget.componentName, widget.priority) + onDeleteWidget(widget.appWidgetId, widget.componentName, widget.rank) } } @@ -94,24 +93,24 @@ internal constructor( newItemUser: UserHandle? = null, newItemIndex: Int? = null ) { - // filters placeholder, but, maintains the indices of the widgets as if the placeholder was - // in the list. When persisted in DB, this leaves space for the new item (to be added) at - // the correct priority. - val widgetIdToPriorityMap: Map<Int, Int> = + // New widget added to the grid. Other widgets are shifted as needed at the database level. + if (newItemComponentName != null && newItemUser != null && newItemIndex != null) { + onAddWidget(newItemComponentName, newItemUser, /* rank= */ newItemIndex) + return + } + + // No new widget, only reorder existing widgets. + val widgetIdToRankMap: Map<Int, Int> = list .mapIndexedNotNull { index, item -> if (item is CommunalContentModel.WidgetContent) { - item.appWidgetId to list.size - index + item.appWidgetId to index } else { null } } .toMap() - // reorder and then add the new widget - onReorderWidgets(widgetIdToPriorityMap) - if (newItemComponentName != null && newItemUser != null && newItemIndex != null) { - onAddWidget(newItemComponentName, newItemUser, /* priority= */ list.size - newItemIndex) - } + onReorderWidgets(widgetIdToRankMap) } /** Returns true if the item at given index is editable. */ diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt index 0c293948dd3c..f2f7c872b71c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt @@ -193,7 +193,7 @@ internal class DragAndDropTargetState( val widgetExtra = event.maybeWidgetExtra() ?: return false val (componentName, user) = widgetExtra if (componentName != null && user != null) { - // Placeholder isn't removed yet to allow the setting the right priority for items + // Placeholder isn't removed yet to allow the setting the right rank for items // before adding in the new item. contentListState.onSaveList( newItemComponentName = componentName, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt index 7f059d766307..2029e9e7f139 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt @@ -25,6 +25,7 @@ import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneActionsViewModel +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.ComposableScene @@ -39,7 +40,7 @@ class LockscreenScene constructor( actionsViewModelFactory: LockscreenSceneActionsViewModel.Factory, private val lockscreenContent: Lazy<LockscreenContent>, -) : ComposableScene { +) : ExclusiveActivatable(), ComposableScene { override val key = Scenes.Lockscreen private val actionsViewModel: LockscreenSceneActionsViewModel by lazy { @@ -49,7 +50,7 @@ constructor( override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions - override suspend fun activate(): Nothing { + override suspend fun onActivated(): Nothing { actionsViewModel.activate() } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt index a3e07016ced1..3e73057ba09b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt @@ -76,6 +76,11 @@ constructor( viewModel .areNotificationsVisible(contentKey) .collectAsStateWithLifecycle(initialValue = false) + val isBypassEnabled by viewModel.isBypassEnabled.collectAsStateWithLifecycle() + + if (isBypassEnabled) { + with(notificationSection) { HeadsUpNotifications() } + } LockscreenLongPress( viewModel = viewModel.touchHandling, @@ -110,7 +115,7 @@ constructor( } ) } - if (isShadeLayoutWide) { + if (isShadeLayoutWide && !isBypassEnabled) { with(notificationSection) { Notifications( areNotificationsVisible = areNotificationsVisible, @@ -124,7 +129,7 @@ constructor( } } } - if (!isShadeLayoutWide) { + if (!isShadeLayoutWide && !isBypassEnabled) { with(notificationSection) { Notifications( areNotificationsVisible = areNotificationsVisible, @@ -175,7 +180,7 @@ constructor( }, modifier = Modifier.fillMaxSize(), ) { measurables, constraints -> - check(measurables.size == 6) + check(measurables.size == 6) { "Expected 6 measurables, got: ${measurables.size}" } val aboveLockIconMeasurable = measurables[0] val lockIconMeasurable = measurables[1] val belowLockIconMeasurable = measurables[2] diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index 6801cf27a64d..5f4dc6e77c63 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -20,7 +20,6 @@ import android.view.ViewGroup import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.Dp @@ -34,6 +33,7 @@ import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.notifications.ui.composable.ConstrainedNotificationStack +import com.android.systemui.notifications.ui.composable.SnoozeableHeadsUpNotificationSpace import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView @@ -79,6 +79,14 @@ constructor( ) } + @Composable + fun SceneScope.HeadsUpNotifications() { + SnoozeableHeadsUpNotificationSpace( + stackScrollView = stackScrollView.get(), + viewModel = rememberViewModel { viewModelFactory.create() }, + ) + } + /** * @param burnInParams params to make this view adaptive to burn-in, `null` to disable burn-in * adjustment diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt index 666e324c8d36..62213bd22cbd 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt @@ -28,6 +28,7 @@ import com.android.compose.animation.scene.UserActionResult import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.ui.composable.LockscreenContent +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneActionsViewModel import com.android.systemui.scene.session.ui.composable.SaveableSession @@ -61,7 +62,7 @@ constructor( private val shadeSession: SaveableSession, private val stackScrollView: Lazy<NotificationScrollView>, private val lockscreenContent: Lazy<Optional<LockscreenContent>>, -) : ComposableScene { +) : ExclusiveActivatable(), ComposableScene { override val key = Scenes.NotificationsShade @@ -72,7 +73,7 @@ constructor( override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions - override suspend fun activate(): Nothing { + override suspend fun onActivated(): Nothing { actionsViewModel.activate() } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt index fc4a8a5ee67c..192162475c9f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt @@ -121,6 +121,8 @@ private fun SceneScope.stateForQuickSettingsContent( ) } } + is TransitionState.Transition.OverlayTransition -> + TODO("b/359173565: Handle overlay transitions") } } @@ -212,7 +214,8 @@ private fun QuickSettingsContent( addView(view) } }, - // When the view changes (e.g. due to a theme change), this will be recomposed + // When the view changes (e.g. due to a theme change), this will be + // recomposed // if needed and the new view will be attached to the FrameLayout here. update = { qsSceneAdapter.setState(state()) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index d16ba0d6affb..f11f8bb94a52 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -81,6 +81,7 @@ import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.media.controls.ui.controller.MediaCarouselController @@ -129,7 +130,7 @@ constructor( private val statusBarIconController: StatusBarIconController, private val mediaCarouselController: MediaCarouselController, @Named(MediaModule.QS_PANEL) private val mediaHost: MediaHost, -) : ComposableScene { +) : ExclusiveActivatable(), ComposableScene { override val key = Scenes.QuickSettings private val actionsViewModel: QuickSettingsSceneActionsViewModel by lazy { @@ -139,7 +140,7 @@ constructor( override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions - override suspend fun activate(): Nothing { + override suspend fun onActivated(): Nothing { actionsViewModel.activate() } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt index fb7c42254caa..b25773b68471 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt @@ -43,6 +43,7 @@ import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.ui.composable.LockscreenContent +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.qs.panels.ui.compose.EditMode import com.android.systemui.qs.panels.ui.compose.TileGrid @@ -74,7 +75,7 @@ constructor( private val tintedIconManagerFactory: TintedIconManager.Factory, private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, private val statusBarIconController: StatusBarIconController, -) : ComposableScene { +) : ExclusiveActivatable(), ComposableScene { override val key = Scenes.QuickSettingsShade @@ -85,6 +86,10 @@ constructor( override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions + override suspend fun onActivated(): Nothing { + actionsViewModel.activate() + } + @Composable override fun SceneScope.Content( modifier: Modifier, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt index 3e221056c2db..d489d731b5fb 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt @@ -26,6 +26,7 @@ import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.animateSceneDpAsState import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.notifications.ui.composable.SnoozeableHeadsUpNotificationSpace import com.android.systemui.qs.ui.composable.QuickSettings @@ -50,7 +51,7 @@ constructor( private val notificationStackScrolLView: Lazy<NotificationScrollView>, private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, private val viewModelFactory: GoneSceneActionsViewModel.Factory, -) : ComposableScene { +) : ExclusiveActivatable(), ComposableScene { override val key = Scenes.Gone private val actionsViewModel: GoneSceneActionsViewModel by lazy { viewModelFactory.create() } @@ -58,7 +59,7 @@ constructor( override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions - override suspend fun activate(): Nothing { + override suspend fun onActivated(): Nothing { actionsViewModel.activate() } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 853dc6fb66cc..f8513a8c4dd4 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -82,6 +82,7 @@ import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.media.controls.ui.composable.MediaContentPicker @@ -160,7 +161,7 @@ constructor( private val mediaCarouselController: MediaCarouselController, @Named(QUICK_QS_PANEL) private val qqsMediaHost: MediaHost, @Named(QS_PANEL) private val qsMediaHost: MediaHost, -) : ComposableScene { +) : ExclusiveActivatable(), ComposableScene { override val key = Scenes.Shade @@ -168,7 +169,7 @@ constructor( actionsViewModelFactory.create() } - override suspend fun activate(): Nothing { + override suspend fun onActivated(): Nothing { actionsViewModel.activate() } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt index 2bc8c87978a8..b16673702b49 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt @@ -26,15 +26,15 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch internal fun CoroutineScope.animateContent( + layoutState: MutableSceneTransitionLayoutStateImpl, transition: TransitionState.Transition, oneOffAnimation: OneOffAnimation, targetProgress: Float, - startTransition: () -> Unit, - finishTransition: () -> Unit, + chain: Boolean = true, ) { // Start the transition. This will compute the TransformationSpec associated to [transition], // which we need to initialize the Animatable that will actually animate it. - startTransition() + layoutState.startTransition(transition, chain) // The transition now contains the transformation spec that we should use to instantiate the // Animatable. @@ -59,7 +59,7 @@ internal fun CoroutineScope.animateContent( try { animatable.animateTo(targetProgress, animationSpec, initialVelocity) } finally { - finishTransition() + layoutState.finishTransition(transition) } } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateOverlay.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateOverlay.kt new file mode 100644 index 000000000000..e020f14a9a02 --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateOverlay.kt @@ -0,0 +1,144 @@ +/* + * 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.compose.animation.scene + +import com.android.compose.animation.scene.content.state.TransitionState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job + +/** Trigger a one-off transition to show or hide an overlay. */ +internal fun CoroutineScope.showOrHideOverlay( + layoutState: MutableSceneTransitionLayoutStateImpl, + overlay: OverlayKey, + fromOrToScene: SceneKey, + isShowing: Boolean, + transitionKey: TransitionKey?, + replacedTransition: TransitionState.Transition.ShowOrHideOverlay?, + reversed: Boolean, +): TransitionState.Transition.ShowOrHideOverlay { + val targetProgress = if (reversed) 0f else 1f + val (fromContent, toContent) = + if (isShowing xor reversed) { + fromOrToScene to overlay + } else { + overlay to fromOrToScene + } + + val oneOffAnimation = OneOffAnimation() + val transition = + OneOffShowOrHideOverlayTransition( + overlay = overlay, + fromOrToScene = fromOrToScene, + fromContent = fromContent, + toContent = toContent, + isEffectivelyShown = isShowing, + key = transitionKey, + replacedTransition = replacedTransition, + oneOffAnimation = oneOffAnimation, + ) + + animateContent( + layoutState = layoutState, + transition = transition, + oneOffAnimation = oneOffAnimation, + targetProgress = targetProgress, + ) + + return transition +} + +/** Trigger a one-off transition to replace an overlay by another one. */ +internal fun CoroutineScope.replaceOverlay( + layoutState: MutableSceneTransitionLayoutStateImpl, + fromOverlay: OverlayKey, + toOverlay: OverlayKey, + transitionKey: TransitionKey?, + replacedTransition: TransitionState.Transition.ReplaceOverlay?, + reversed: Boolean, +): TransitionState.Transition.ReplaceOverlay { + val targetProgress = if (reversed) 0f else 1f + val effectivelyShownOverlay = if (reversed) fromOverlay else toOverlay + + val oneOffAnimation = OneOffAnimation() + val transition = + OneOffOverlayReplacingTransition( + fromOverlay = fromOverlay, + toOverlay = toOverlay, + effectivelyShownOverlay = effectivelyShownOverlay, + key = transitionKey, + replacedTransition = replacedTransition, + oneOffAnimation = oneOffAnimation, + ) + + animateContent( + layoutState = layoutState, + transition = transition, + oneOffAnimation = oneOffAnimation, + targetProgress = targetProgress, + ) + + return transition +} + +private class OneOffShowOrHideOverlayTransition( + overlay: OverlayKey, + fromOrToScene: SceneKey, + fromContent: ContentKey, + toContent: ContentKey, + override val isEffectivelyShown: Boolean, + override val key: TransitionKey?, + replacedTransition: TransitionState.Transition?, + private val oneOffAnimation: OneOffAnimation, +) : + TransitionState.Transition.ShowOrHideOverlay( + overlay, + fromOrToScene, + fromContent, + toContent, + replacedTransition, + ) { + override val progress: Float + get() = oneOffAnimation.progress + + override val progressVelocity: Float + get() = oneOffAnimation.progressVelocity + + override val isInitiatedByUserInput: Boolean = false + override val isUserInputOngoing: Boolean = false + + override fun finish(): Job = oneOffAnimation.finish() +} + +private class OneOffOverlayReplacingTransition( + fromOverlay: OverlayKey, + toOverlay: OverlayKey, + override val effectivelyShownOverlay: OverlayKey, + override val key: TransitionKey?, + replacedTransition: TransitionState.Transition?, + private val oneOffAnimation: OneOffAnimation, +) : TransitionState.Transition.ReplaceOverlay(fromOverlay, toOverlay, replacedTransition) { + override val progress: Float + get() = oneOffAnimation.progress + + override val progressVelocity: Float + get() = oneOffAnimation.progressVelocity + + override val isInitiatedByUserInput: Boolean = false + override val isUserInputOngoing: Boolean = false + + override fun finish(): Job = oneOffAnimation.finish() +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt index ea708a5637f0..4aa50b586c1b 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt @@ -393,7 +393,8 @@ private class AnimatedStateImpl<T, Delta>( transition: TransitionState.Transition?, ): T? { if (transition == null) { - return sharedValue[layoutImpl.state.transitionState.currentScene] + return sharedValue[content] + ?: sharedValue[layoutImpl.state.transitionState.currentScene] } val fromValue = sharedValue[transition.fromContent] @@ -411,7 +412,7 @@ private class AnimatedStateImpl<T, Delta>( if (canOverflow) transition.progress else transition.progress.fastCoerceIn(0f, 1f) } - overscrollSpec.scene == transition.toContent -> 1f + overscrollSpec.content == transition.toContent -> 1f else -> 0f } @@ -424,10 +425,12 @@ private class AnimatedStateImpl<T, Delta>( val targetValues = sharedValue.targetValues val transition = if (element != null) { - layoutImpl.elements[element]?.stateByContent?.let { sceneStates -> - layoutImpl.state.currentTransitions.fastLastOrNull { transition -> - transition.fromContent in sceneStates || transition.toContent in sceneStates - } + layoutImpl.elements[element]?.let { element -> + elementState( + layoutImpl.state.transitionStates, + isInContent = { it in element.stateByContent }, + ) + as? TransitionState.Transition } } else { layoutImpl.state.currentTransitions.fastLastOrNull { transition -> diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt index f2c2a3600366..abe079a4ab64 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt @@ -43,12 +43,15 @@ internal fun CoroutineScope.animateToScene( } return when (transitionState) { - is TransitionState.Idle -> { + is TransitionState.Idle, + is TransitionState.Transition.ShowOrHideOverlay, + is TransitionState.Transition.ReplaceOverlay -> { animateToScene( layoutState, target, transitionKey, isInitiatedByUserInput = false, + fromScene = transitionState.currentScene, replacedTransition = null, ) } @@ -163,11 +166,11 @@ private fun CoroutineScope.animateToScene( } animateContent( + layoutState = layoutState, transition = transition, oneOffAnimation = oneOffAnimation, targetProgress = targetProgress, - startTransition = { layoutState.startTransition(transition, chain) }, - finishTransition = { layoutState.finishTransition(transition) }, + chain = chain, ) return transition diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index 7dac2e4c4ada..9b1740dc700a 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -46,7 +46,7 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.round import androidx.compose.ui.util.fastCoerceIn -import androidx.compose.ui.util.fastLastOrNull +import androidx.compose.ui.util.fastForEachReversed import androidx.compose.ui.util.lerp import com.android.compose.animation.scene.content.Content import com.android.compose.animation.scene.content.state.TransitionState @@ -145,8 +145,9 @@ internal fun Modifier.element( // layout/drawing. // TODO(b/341072461): Revert this and read the current transitions in ElementNode directly once // we can ensure that SceneTransitionLayoutImpl will compose new contents first. - val currentTransitions = layoutImpl.state.currentTransitions - return then(ElementModifier(layoutImpl, currentTransitions, content, key)).testTag(key.testTag) + val currentTransitionStates = layoutImpl.state.transitionStates + return then(ElementModifier(layoutImpl, currentTransitionStates, content, key)) + .testTag(key.testTag) } /** @@ -155,20 +156,21 @@ internal fun Modifier.element( */ private data class ElementModifier( private val layoutImpl: SceneTransitionLayoutImpl, - private val currentTransitions: List<TransitionState.Transition>, + private val currentTransitionStates: List<TransitionState>, private val content: Content, private val key: ElementKey, ) : ModifierNodeElement<ElementNode>() { - override fun create(): ElementNode = ElementNode(layoutImpl, currentTransitions, content, key) + override fun create(): ElementNode = + ElementNode(layoutImpl, currentTransitionStates, content, key) override fun update(node: ElementNode) { - node.update(layoutImpl, currentTransitions, content, key) + node.update(layoutImpl, currentTransitionStates, content, key) } } internal class ElementNode( private var layoutImpl: SceneTransitionLayoutImpl, - private var currentTransitions: List<TransitionState.Transition>, + private var currentTransitionStates: List<TransitionState>, private var content: Content, private var key: ElementKey, ) : Modifier.Node(), DrawModifierNode, ApproachLayoutModifierNode, TraversableNode { @@ -226,12 +228,12 @@ internal class ElementNode( fun update( layoutImpl: SceneTransitionLayoutImpl, - currentTransitions: List<TransitionState.Transition>, + currentTransitionStates: List<TransitionState>, content: Content, key: ElementKey, ) { check(layoutImpl == this.layoutImpl && content == this.content) - this.currentTransitions = currentTransitions + this.currentTransitionStates = currentTransitionStates removeNodeFromContentState() @@ -287,31 +289,73 @@ internal class ElementNode( measurable: Measurable, constraints: Constraints, ): MeasureResult { - val transitions = currentTransitions - val transition = elementTransition(layoutImpl, element, transitions) + val elementState = elementState(layoutImpl, element, currentTransitionStates) + if (elementState == null) { + // If the element is not part of any transition, place it normally in its idle scene. + val currentState = currentTransitionStates.last() + val placeInThisContent = + elementContentWhenIdle( + layoutImpl, + currentState.currentScene, + currentState.currentOverlays, + isInContent = { it in element.stateByContent }, + ) == content.key + + return if (placeInThisContent) { + placeNormally(measurable, constraints) + } else { + doNotPlace(measurable, constraints) + } + } + + val transition = elementState as? TransitionState.Transition // If this element is not supposed to be laid out now, either because it is not part of any // ongoing transition or the other content of its transition is overscrolling, then lay out // the element normally and don't place it. - val overscrollScene = transition?.currentOverscrollSpec?.scene + val overscrollScene = transition?.currentOverscrollSpec?.content val isOtherSceneOverscrolling = overscrollScene != null && overscrollScene != content.key - val isNotPartOfAnyOngoingTransitions = transitions.isNotEmpty() && transition == null - if (isNotPartOfAnyOngoingTransitions || isOtherSceneOverscrolling) { - recursivelyClearPlacementValues() - stateInContent.lastSize = Element.SizeUnspecified - - val placeable = measurable.measure(constraints) - return layout(placeable.width, placeable.height) { /* Do not place */ } + if (isOtherSceneOverscrolling) { + return doNotPlace(measurable, constraints) } val placeable = measure(layoutImpl, element, transition, stateInContent, measurable, constraints) stateInContent.lastSize = placeable.size() - return layout(placeable.width, placeable.height) { place(transition, placeable) } + return layout(placeable.width, placeable.height) { place(elementState, placeable) } + } + + private fun ApproachMeasureScope.doNotPlace( + measurable: Measurable, + constraints: Constraints + ): MeasureResult { + recursivelyClearPlacementValues() + stateInContent.lastSize = Element.SizeUnspecified + + val placeable = measurable.measure(constraints) + return layout(placeable.width, placeable.height) { /* Do not place */ } + } + + private fun ApproachMeasureScope.placeNormally( + measurable: Measurable, + constraints: Constraints + ): MeasureResult { + val placeable = measurable.measure(constraints) + stateInContent.lastSize = placeable.size() + return layout(placeable.width, placeable.height) { + coordinates?.let { + with(layoutImpl.lookaheadScope) { + stateInContent.lastOffset = + lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero) + } + } + + placeable.place(0, 0) + } } private fun Placeable.PlacementScope.place( - transition: TransitionState.Transition?, + elementState: TransitionState, placeable: Placeable, ) { with(layoutImpl.lookaheadScope) { @@ -321,11 +365,12 @@ internal class ElementNode( coordinates ?: error("Element ${element.key} does not have any coordinates") // No need to place the element in this content if we don't want to draw it anyways. - if (!shouldPlaceElement(layoutImpl, content.key, element, transition)) { + if (!shouldPlaceElement(layoutImpl, content.key, element, elementState)) { recursivelyClearPlacementValues() return } + val transition = elementState as? TransitionState.Transition val currentOffset = lookaheadScopeCoordinates.localPositionOf(coords, Offset.Zero) val targetOffset = computeValue( @@ -391,11 +436,15 @@ internal class ElementNode( return@placeWithLayer } - val transition = elementTransition(layoutImpl, element, currentTransitions) - if (!shouldPlaceElement(layoutImpl, content.key, element, transition)) { + val elementState = elementState(layoutImpl, element, currentTransitionStates) + if ( + elementState == null || + !shouldPlaceElement(layoutImpl, content.key, element, elementState) + ) { return@placeWithLayer } + val transition = elementState as? TransitionState.Transition alpha = elementAlpha(layoutImpl, element, transition, stateInContent) compositingStrategy = CompositingStrategy.ModulateAlpha } @@ -425,7 +474,9 @@ internal class ElementNode( override fun ContentDrawScope.draw() { element.wasDrawnInAnyContent = true - val transition = elementTransition(layoutImpl, element, currentTransitions) + val transition = + elementState(layoutImpl, element, currentTransitionStates) + as? TransitionState.Transition val drawScale = getDrawScale(layoutImpl, element, transition, stateInContent) if (drawScale == Scale.Default) { drawContent() @@ -468,21 +519,15 @@ internal class ElementNode( } } -/** - * The transition that we should consider for [element]. This is the last transition where one of - * its contents contains the element. - */ -private fun elementTransition( +/** The [TransitionState] that we should consider for [element]. */ +private fun elementState( layoutImpl: SceneTransitionLayoutImpl, element: Element, - transitions: List<TransitionState.Transition>, -): TransitionState.Transition? { - val transition = - transitions.fastLastOrNull { transition -> - transition.fromContent in element.stateByContent || - transition.toContent in element.stateByContent - } + transitionStates: List<TransitionState>, +): TransitionState? { + val state = elementState(transitionStates, isInContent = { it in element.stateByContent }) + val transition = state as? TransitionState.Transition val previousTransition = element.lastTransition element.lastTransition = transition @@ -497,7 +542,66 @@ private fun elementTransition( } } - return transition + return state +} + +internal inline fun elementState( + transitionStates: List<TransitionState>, + isInContent: (ContentKey) -> Boolean, +): TransitionState? { + val lastState = transitionStates.last() + if (lastState is TransitionState.Idle) { + check(transitionStates.size == 1) + return lastState + } + + // Find the last transition with a content that contains the element. + transitionStates.fastForEachReversed { state -> + val transition = state as TransitionState.Transition + if (isInContent(transition.fromContent) || isInContent(transition.toContent)) { + return transition + } + } + + return null +} + +internal inline fun elementContentWhenIdle( + layoutImpl: SceneTransitionLayoutImpl, + idle: TransitionState.Idle, + isInContent: (ContentKey) -> Boolean, +): ContentKey { + val currentScene = idle.currentScene + val overlays = idle.currentOverlays + return elementContentWhenIdle(layoutImpl, currentScene, overlays, isInContent) +} + +private inline fun elementContentWhenIdle( + layoutImpl: SceneTransitionLayoutImpl, + currentScene: SceneKey, + overlays: Set<OverlayKey>, + isInContent: (ContentKey) -> Boolean, +): ContentKey { + if (overlays.isEmpty()) { + return currentScene + } + + // Find the overlay with highest zIndex that contains the element. + // TODO(b/353679003): Should we cache enabledOverlays into a List<> to avoid a lot of + // allocations here? + var currentOverlay: OverlayKey? = null + for (overlay in overlays) { + if ( + isInContent(overlay) && + (currentOverlay == null || + (layoutImpl.overlay(overlay).zIndex > + layoutImpl.overlay(currentOverlay).zIndex)) + ) { + currentOverlay = overlay + } + } + + return currentOverlay ?: currentScene } private fun prepareInterruption( @@ -693,12 +797,20 @@ private fun shouldPlaceElement( layoutImpl: SceneTransitionLayoutImpl, content: ContentKey, element: Element, - transition: TransitionState.Transition?, + elementState: TransitionState, ): Boolean { - // Always place the element if we are idle. - if (transition == null) { - return true - } + val transition = + when (elementState) { + is TransitionState.Idle -> { + return content == + elementContentWhenIdle( + layoutImpl, + elementState, + isInContent = { it in element.stateByContent }, + ) + } + is TransitionState.Transition -> elementState + } // Don't place the element in this content if this content is not part of the current element // transition. @@ -734,23 +846,19 @@ internal fun shouldPlaceOrComposeSharedElement( transition: TransitionState.Transition, ): Boolean { // If we are overscrolling, only place/compose the element in the overscrolling scene. - val overscrollScene = transition.currentOverscrollSpec?.scene + val overscrollScene = transition.currentOverscrollSpec?.content if (overscrollScene != null) { return content == overscrollScene } val scenePicker = element.contentPicker val pickedScene = - when (transition) { - is TransitionState.Transition.ChangeCurrentScene -> { - scenePicker.contentDuringTransition( - element = element, - transition = transition, - fromContentZIndex = layoutImpl.scene(transition.fromScene).zIndex, - toContentZIndex = layoutImpl.scene(transition.toScene).zIndex, - ) - } - } + scenePicker.contentDuringTransition( + element = element, + transition = transition, + fromContentZIndex = layoutImpl.content(transition.fromContent).zIndex, + toContentZIndex = layoutImpl.content(transition.toContent).zIndex, + ) return pickedScene == content } @@ -1077,7 +1185,7 @@ private inline fun <T> computeValue( val currentContent = currentContentState.content if (transition is TransitionState.HasOverscrollProperties) { val overscroll = transition.currentOverscrollSpec - if (overscroll?.scene == currentContent) { + if (overscroll?.content == currentContent) { val elementSpec = overscroll.transformationSpec.transformations(element.key, currentContent) val propertySpec = transformation(elementSpec) ?: return currentValue() @@ -1103,7 +1211,7 @@ private inline fun <T> computeValue( // TODO(b/290184746): Make sure that we don't overflow transformations associated to a // range. val directionSign = if (transition.isUpOrLeft) -1 else 1 - val isToContent = overscroll.scene == transition.toContent + val isToContent = overscroll.content == transition.toContent val linearProgress = transition.progress.let { if (isToContent) it - 1f else it } val progressConverter = overscroll.progressConverter diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt index acb436e4874b..3f8f5e742079 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt @@ -63,6 +63,18 @@ class SceneKey( } } +/** Key for an overlay. */ +class OverlayKey( + debugName: String, + identity: Any = Object(), +) : ContentKey(debugName, identity) { + override val testTag: String = "overlay:$debugName" + + override fun toString(): String { + return "OverlayKey(debugName=$debugName)" + } +} + /** Key for an element. */ open class ElementKey( debugName: String, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt index 63d51f9bbcf4..715222cfd9da 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.util.fastLastOrNull import com.android.compose.animation.scene.content.Content import com.android.compose.animation.scene.content.state.TransitionState @@ -58,6 +57,13 @@ internal fun MovableElement( modifier: Modifier, content: @Composable ElementScope<MovableElementContentScope>.() -> Unit, ) { + check(key.contentPicker.contents.contains(sceneOrOverlay.key)) { + val elementName = key.debugName + val contentName = sceneOrOverlay.key.debugName + "MovableElement $elementName was composed in content $contentName but the " + + "MovableElementKey($elementName).contentPicker.contents does not contain $contentName" + } + Box(modifier.element(layoutImpl, sceneOrOverlay, key)) { val contentScope = sceneOrOverlay.scope val boxScope = this @@ -153,13 +159,20 @@ private class MovableElementScopeImpl( // size* as its movable content, i.e. the same *size when idle*. During transitions, // this size will be used to interpolate the transition size, during the intermediate // layout pass. + // + // Important: Like in Modifier.element(), we read the transition states during + // composition then pass them to Layout to make sure that composition sees new states + // before layout and drawing. + val transitionStates = layoutImpl.state.transitionStates Layout { _, _ -> // No need to measure or place anything. val size = placeholderContentSize( - layoutImpl, - contentKey, - layoutImpl.elements.getValue(element), + layoutImpl = layoutImpl, + content = contentKey, + element = layoutImpl.elements.getValue(element), + elementKey = element, + transitionStates = transitionStates, ) layout(size.width, size.height) {} } @@ -172,28 +185,43 @@ private fun shouldComposeMovableElement( content: ContentKey, element: MovableElementKey, ): Boolean { - val transitions = layoutImpl.state.currentTransitions - if (transitions.isEmpty()) { - // If we are idle, there is only one [scene] that is composed so we can compose our - // movable content here. We still check that [scene] is equal to the current idle scene, to - // make sure we only compose it there. - return layoutImpl.state.transitionState.currentScene == content + return when ( + val elementState = movableElementState(element, layoutImpl.state.transitionStates) + ) { + null -> false + is TransitionState.Idle -> + movableElementContentWhenIdle(layoutImpl, element, elementState) == content + is TransitionState.Transition -> { + // During transitions, always compose movable elements in the scene picked by their + // content picker. + shouldPlaceOrComposeSharedElement( + layoutImpl, + content, + element, + elementState, + ) + } } +} - // The current transition for this element is the last transition in which either fromScene or - // toScene contains the element. - val contents = element.contentPicker.contents - val transition = - transitions.fastLastOrNull { transition -> - transition.fromContent in contents || transition.toContent in contents - } ?: return false +private fun movableElementState( + element: MovableElementKey, + transitionStates: List<TransitionState>, +): TransitionState? { + val content = element.contentPicker.contents + return elementState(transitionStates, isInContent = { content.contains(it) }) +} - // Always compose movable elements in the scene picked by their scene picker. - return shouldPlaceOrComposeSharedElement( +private fun movableElementContentWhenIdle( + layoutImpl: SceneTransitionLayoutImpl, + element: MovableElementKey, + elementState: TransitionState.Idle, +): ContentKey { + val contents = element.contentPicker.contents + return elementContentWhenIdle( layoutImpl, - content, - element, - transition, + elementState, + isInContent = { contents.contains(it) }, ) } @@ -205,6 +233,8 @@ private fun placeholderContentSize( layoutImpl: SceneTransitionLayoutImpl, content: ContentKey, element: Element, + elementKey: MovableElementKey, + transitionStates: List<TransitionState>, ): IntSize { // If the content of the movable element was already composed in this scene before, use that // target size. @@ -213,20 +243,21 @@ private fun placeholderContentSize( return targetValueInScene } - // This code is only run during transitions (otherwise the content would be composed and the - // placeholder would not), so it's ok to cast the state into a Transition directly. - val transition = - layoutImpl.state.transitionState as TransitionState.Transition.ChangeCurrentScene - - // If the content was already composed in the other scene, we use that target size assuming it - // doesn't change between scenes. - // TODO(b/317026105): Provide a way to give a hint size/content for cases where this is not - // true. - val otherScene = - if (transition.fromScene == content) transition.toScene else transition.fromScene - val targetValueInOtherScene = element.stateByContent[otherScene]?.targetSize - if (targetValueInOtherScene != null && targetValueInOtherScene != Element.SizeUnspecified) { - return targetValueInOtherScene + // If the element content was already composed in the other overlay/scene, we use that + // target size assuming it doesn't change between scenes. + // TODO(b/317026105): Provide a way to give a hint size/content for cases where this is + // not true. + val otherContent = + when (val state = movableElementState(elementKey, transitionStates)) { + null -> return IntSize.Zero + is TransitionState.Idle -> movableElementContentWhenIdle(layoutImpl, elementKey, state) + is TransitionState.Transition -> + if (state.fromContent == content) state.toContent else state.fromContent + } + + val targetValueInOtherContent = element.stateByContent[otherContent]?.targetSize + if (targetValueInOtherContent != null && targetValueInOtherContent != Element.SizeUnspecified) { + return targetValueInOtherContent } return IntSize.Zero diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt index 5071a7f744dc..236e202749b2 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt @@ -43,7 +43,9 @@ sealed interface ObservableTransitionState { fun currentScene(): Flow<SceneKey> { return when (this) { is Idle -> flowOf(currentScene) - is Transition -> currentScene + is Transition.ChangeCurrentScene -> currentScene + is Transition.ShowOrHideOverlay -> flowOf(currentScene) + is Transition.ReplaceOverlay -> flowOf(currentScene) } } @@ -51,10 +53,11 @@ sealed interface ObservableTransitionState { data class Idle(val currentScene: SceneKey) : ObservableTransitionState /** There is a transition animating between two scenes. */ - class Transition( - val fromScene: SceneKey, - val toScene: SceneKey, - val currentScene: Flow<SceneKey>, + sealed class Transition( + // TODO(b/353679003): Rename these to fromContent and toContent. + open val fromScene: ContentKey, + open val toScene: ContentKey, + val currentOverlays: Flow<Set<OverlayKey>>, val progress: Flow<Float>, /** @@ -76,10 +79,10 @@ sealed interface ObservableTransitionState { val isUserInputOngoing: Flow<Boolean>, /** Current progress of the preview part of the transition */ - val previewProgress: Flow<Float> = flowOf(0f), + val previewProgress: Flow<Float>, /** Whether the transition is currently in the preview stage or not */ - val isInPreviewStage: Flow<Boolean> = flowOf(false), + val isInPreviewStage: Flow<Boolean>, ) : ObservableTransitionState { override fun toString(): String = """Transition @@ -89,13 +92,109 @@ sealed interface ObservableTransitionState { | isUserInputOngoing=$isUserInputOngoing |)""" .trimMargin() + + /** A transition animating between [fromScene] and [toScene]. */ + class ChangeCurrentScene( + override val fromScene: SceneKey, + override val toScene: SceneKey, + val currentScene: Flow<SceneKey>, + currentOverlays: Flow<Set<OverlayKey>>, + progress: Flow<Float>, + isInitiatedByUserInput: Boolean, + isUserInputOngoing: Flow<Boolean>, + previewProgress: Flow<Float>, + isInPreviewStage: Flow<Boolean>, + ) : + Transition( + fromScene, + toScene, + currentOverlays, + progress, + isInitiatedByUserInput, + isUserInputOngoing, + previewProgress, + isInPreviewStage, + ) + + /** The [overlay] is either showing from [currentScene] or hiding into [currentScene]. */ + class ShowOrHideOverlay( + val overlay: OverlayKey, + fromContent: ContentKey, + toContent: ContentKey, + val currentScene: SceneKey, + currentOverlays: Flow<Set<OverlayKey>>, + progress: Flow<Float>, + isInitiatedByUserInput: Boolean, + isUserInputOngoing: Flow<Boolean>, + previewProgress: Flow<Float>, + isInPreviewStage: Flow<Boolean>, + ) : + Transition( + fromContent, + toContent, + currentOverlays, + progress, + isInitiatedByUserInput, + isUserInputOngoing, + previewProgress, + isInPreviewStage, + ) + + /** We are transitioning from [fromOverlay] to [toOverlay]. */ + class ReplaceOverlay( + val fromOverlay: OverlayKey, + val toOverlay: OverlayKey, + val currentScene: SceneKey, + currentOverlays: Flow<Set<OverlayKey>>, + progress: Flow<Float>, + isInitiatedByUserInput: Boolean, + isUserInputOngoing: Flow<Boolean>, + previewProgress: Flow<Float>, + isInPreviewStage: Flow<Boolean>, + ) : + Transition( + fromOverlay, + toOverlay, + currentOverlays, + progress, + isInitiatedByUserInput, + isUserInputOngoing, + previewProgress, + isInPreviewStage, + ) + + companion object { + operator fun invoke( + fromScene: SceneKey, + toScene: SceneKey, + currentScene: Flow<SceneKey>, + progress: Flow<Float>, + isInitiatedByUserInput: Boolean, + isUserInputOngoing: Flow<Boolean>, + previewProgress: Flow<Float> = flowOf(0f), + isInPreviewStage: Flow<Boolean> = flowOf(false), + currentOverlays: Flow<Set<OverlayKey>> = flowOf(emptySet()), + ): ChangeCurrentScene { + return ChangeCurrentScene( + fromScene, + toScene, + currentScene, + currentOverlays, + progress, + isInitiatedByUserInput, + isUserInputOngoing, + previewProgress, + isInPreviewStage, + ) + } + } } fun isIdle(scene: SceneKey?): Boolean { return this is Idle && (scene == null || this.currentScene == scene) } - fun isTransitioning(from: SceneKey? = null, to: SceneKey? = null): Boolean { + fun isTransitioning(from: ContentKey? = null, to: ContentKey? = null): Boolean { return this is Transition && (from == null || this.fromScene == from) && (to == null || this.toScene == to) @@ -112,15 +211,44 @@ fun SceneTransitionLayoutState.observableTransitionState(): Flow<ObservableTrans when (val state = transitionState) { is TransitionState.Idle -> ObservableTransitionState.Idle(state.currentScene) is TransitionState.Transition.ChangeCurrentScene -> { - ObservableTransitionState.Transition( + ObservableTransitionState.Transition.ChangeCurrentScene( fromScene = state.fromScene, toScene = state.toScene, currentScene = snapshotFlow { state.currentScene }, + currentOverlays = flowOf(state.currentOverlays), + progress = snapshotFlow { state.progress }, + isInitiatedByUserInput = state.isInitiatedByUserInput, + isUserInputOngoing = snapshotFlow { state.isUserInputOngoing }, + previewProgress = snapshotFlow { state.previewProgress }, + isInPreviewStage = snapshotFlow { state.isInPreviewStage }, + ) + } + is TransitionState.Transition.ShowOrHideOverlay -> { + check(state.fromOrToScene == state.currentScene) + ObservableTransitionState.Transition.ShowOrHideOverlay( + overlay = state.overlay, + fromContent = state.fromContent, + toContent = state.toContent, + currentScene = state.currentScene, + currentOverlays = snapshotFlow { state.currentOverlays }, + progress = snapshotFlow { state.progress }, + isInitiatedByUserInput = state.isInitiatedByUserInput, + isUserInputOngoing = snapshotFlow { state.isUserInputOngoing }, + previewProgress = snapshotFlow { state.previewProgress }, + isInPreviewStage = snapshotFlow { state.isInPreviewStage }, + ) + } + is TransitionState.Transition.ReplaceOverlay -> { + ObservableTransitionState.Transition.ReplaceOverlay( + fromOverlay = state.fromOverlay, + toOverlay = state.toOverlay, + currentScene = state.currentScene, + currentOverlays = snapshotFlow { state.currentOverlays }, progress = snapshotFlow { state.progress }, isInitiatedByUserInput = state.isInitiatedByUserInput, isUserInputOngoing = snapshotFlow { state.isUserInputOngoing }, previewProgress = snapshotFlow { state.previewProgress }, - isInPreviewStage = snapshotFlow { state.isInPreviewStage } + isInPreviewStage = snapshotFlow { state.isInPreviewStage }, ) } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 65a73676d398..aaa2546b1d4b 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -49,7 +49,7 @@ import androidx.compose.ui.unit.LayoutDirection * if any. * @param transitionInterceptionThreshold used during a scene transition. For the scene to be * intercepted, the progress value must be above the threshold, and below (1 - threshold). - * @param scenes the configuration of the different scenes of this layout. + * @param builder the configuration of the different scenes and overlays of this layout. */ @Composable fun SceneTransitionLayout( @@ -58,7 +58,7 @@ fun SceneTransitionLayout( swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, swipeDetector: SwipeDetector = DefaultSwipeDetector, @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0.05f, - scenes: SceneTransitionLayoutScope.() -> Unit, + builder: SceneTransitionLayoutScope.() -> Unit, ) { SceneTransitionLayoutForTesting( state, @@ -67,7 +67,7 @@ fun SceneTransitionLayout( swipeDetector, transitionInterceptionThreshold, onLayoutImpl = null, - scenes, + builder, ) } @@ -86,6 +86,31 @@ interface SceneTransitionLayoutScope { userActions: Map<UserAction, UserActionResult> = emptyMap(), content: @Composable ContentScope.() -> Unit, ) + + /** + * Add an overlay to this layout, identified by [key]. + * + * Overlays are displayed above scenes and can be toggled using + * [MutableSceneTransitionLayoutState.showOverlay] and + * [MutableSceneTransitionLayoutState.hideOverlay]. + * + * Overlays will have a maximum size that is the size of the layout without overlays, i.e. an + * overlay can be fillMaxSize() to match the layout size but it won't make the layout bigger. + * + * By default overlays are centered in their layout but they can be aligned differently using + * [alignment]. + * + * Important: overlays must be defined after all scenes. Overlay order along the z-axis follows + * call order. Calling overlay(A) followed by overlay(B) will mean that overlay B renders + * after/above overlay A. + */ + // TODO(b/353679003): Allow to specify user actions. When overlays are shown, the user actions + // of the top-most overlay in currentOverlays will be used. + fun overlay( + key: OverlayKey, + alignment: Alignment = Alignment.Center, + content: @Composable ContentScope.() -> Unit, + ) } /** @@ -239,7 +264,7 @@ interface ContentScope : BaseContentScope { /** * Animate some value at the content level. * - * @param value the value of this shared value in the current scene. + * @param value the value of this shared value in the current content. * @param key the key of this shared value. * @param type the [SharedValueType] of this animated value. * @param canOverflow whether this value can overflow past the values it is interpolated @@ -292,7 +317,7 @@ interface ElementScope<ContentScope> { /** * Animate some value associated to this element. * - * @param value the value of this shared value in the current scene. + * @param value the value of this shared value in the current content. * @param key the key of this shared value. * @param type the [SharedValueType] of this animated value. * @param canOverflow whether this value can overflow past the values it is interpolated @@ -509,7 +534,7 @@ internal fun SceneTransitionLayoutForTesting( swipeDetector: SwipeDetector = DefaultSwipeDetector, transitionInterceptionThreshold: Float = 0f, onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null, - scenes: SceneTransitionLayoutScope.() -> Unit, + builder: SceneTransitionLayoutScope.() -> Unit, ) { val density = LocalDensity.current val layoutDirection = LocalLayoutDirection.current @@ -521,7 +546,7 @@ internal fun SceneTransitionLayoutForTesting( layoutDirection = layoutDirection, swipeSourceDetector = swipeSourceDetector, transitionInterceptionThreshold = transitionInterceptionThreshold, - builder = scenes, + builder = builder, coroutineScope = coroutineScope, ) .also { onLayoutImpl?.invoke(it) } @@ -529,7 +554,7 @@ internal fun SceneTransitionLayoutForTesting( // TODO(b/317014852): Move this into the SideEffect {} again once STLImpl.scenes is not a // SnapshotStateMap anymore. - layoutImpl.updateScenes(scenes, layoutDirection) + layoutImpl.updateContents(builder, layoutDirection) SideEffect { if (state != layoutImpl.state) { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 392ff7ebb446..5f5141e1f153 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -18,10 +18,12 @@ package com.android.compose.animation.scene import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.key import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ApproachLayoutModifierNode @@ -36,7 +38,9 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachReversed +import androidx.compose.ui.zIndex import com.android.compose.animation.scene.content.Content +import com.android.compose.animation.scene.content.Overlay import com.android.compose.animation.scene.content.Scene import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.ui.util.lerp @@ -60,7 +64,17 @@ internal class SceneTransitionLayoutImpl( * * TODO(b/317014852): Make this a normal MutableMap instead. */ - internal val scenes = SnapshotStateMap<SceneKey, Scene>() + private val scenes = SnapshotStateMap<SceneKey, Scene>() + + /** + * The map of [Overlays]. + * + * Note: We lazily create this map to avoid instantiation an expensive SnapshotStateMap in the + * common case where there is no overlay in this layout. + */ + private var _overlays: MutableMap<OverlayKey, Overlay>? = null + private val overlays + get() = _overlays ?: SnapshotStateMap<OverlayKey, Overlay>().also { _overlays = it } /** * The map of [Element]s. @@ -119,7 +133,7 @@ internal class SceneTransitionLayoutImpl( private set init { - updateScenes(builder, layoutDirection) + updateContents(builder, layoutDirection) // DraggableHandlerImpl must wait for the scenes to be initialized, in order to access the // current scene (required for SwipeTransition). @@ -152,22 +166,32 @@ internal class SceneTransitionLayoutImpl( return scenes[key] ?: error("Scene $key is not configured") } + internal fun sceneOrNull(key: SceneKey): Scene? = scenes[key] + + internal fun overlay(key: OverlayKey): Overlay { + return overlays[key] ?: error("Overlay $key is not configured") + } + internal fun content(key: ContentKey): Content { return when (key) { is SceneKey -> scene(key) + is OverlayKey -> overlay(key) } } - internal fun updateScenes( + internal fun updateContents( builder: SceneTransitionLayoutScope.() -> Unit, layoutDirection: LayoutDirection, ) { - // Keep a reference of the current scenes. After processing [builder], the scenes that were - // not configured will be removed. + // Keep a reference of the current contents. After processing [builder], the contents that + // were not configured will be removed. val scenesToRemove = scenes.keys.toMutableSet() + val overlaysToRemove = + if (_overlays == null) mutableSetOf() else overlays.keys.toMutableSet() // The incrementing zIndex of each scene. var zIndex = 0f + var overlaysDefined = false object : SceneTransitionLayoutScope { override fun scene( @@ -175,6 +199,8 @@ internal class SceneTransitionLayoutImpl( userActions: Map<UserAction, UserActionResult>, content: @Composable ContentScope.() -> Unit, ) { + require(!overlaysDefined) { "all scenes must be defined before overlays" } + scenesToRemove.remove(key) val resolvedUserActions = @@ -199,10 +225,42 @@ internal class SceneTransitionLayoutImpl( zIndex++ } + + override fun overlay( + key: OverlayKey, + alignment: Alignment, + content: @Composable (ContentScope.() -> Unit) + ) { + overlaysDefined = true + overlaysToRemove.remove(key) + + val overlay = overlays[key] + if (overlay != null) { + // Update an existing overlay. + overlay.content = content + overlay.zIndex = zIndex + overlay.alignment = alignment + } else { + // New overlay. + overlays[key] = + Overlay( + key, + this@SceneTransitionLayoutImpl, + content, + // TODO(b/353679003): Allow to specify user actions + actions = emptyMap(), + zIndex, + alignment, + ) + } + + zIndex++ + } } .builder() scenesToRemove.forEach { scenes.remove(it) } + overlaysToRemove.forEach { overlays.remove(it) } } @Composable @@ -220,8 +278,8 @@ internal class SceneTransitionLayoutImpl( lookaheadScope = this BackHandler() - - scenesToCompose().fastForEach { scene -> key(scene.key) { scene.Content() } } + Scenes() + Overlays() } } } @@ -233,6 +291,11 @@ internal class SceneTransitionLayoutImpl( PredictiveBackHandler(state, coroutineScope, targetSceneForBack) } + @Composable + private fun Scenes() { + scenesToCompose().fastForEach { scene -> key(scene.key) { scene.Content() } } + } + private fun scenesToCompose(): List<Scene> { val transitions = state.currentTransitions return if (transitions.isEmpty()) { @@ -253,15 +316,74 @@ internal class SceneTransitionLayoutImpl( maybeAdd(transition.toScene) maybeAdd(transition.fromScene) } + is TransitionState.Transition.ShowOrHideOverlay -> + maybeAdd(transition.fromOrToScene) + is TransitionState.Transition.ReplaceOverlay -> {} } } + + // Make sure that the current scene is always composed. + maybeAdd(transitions.last().currentScene) + } + } + } + + @Composable + private fun BoxScope.Overlays() { + val overlaysOrderedByZIndex = overlaysToComposeOrderedByZIndex() + if (overlaysOrderedByZIndex.isEmpty()) { + return + } + + // We put the overlays inside a Box that is matching the layout size so that overlays are + // measured after all scenes and that their max size is the size of the layout without the + // overlays. + Box(Modifier.matchParentSize().zIndex(overlaysOrderedByZIndex.first().zIndex)) { + overlaysOrderedByZIndex.fastForEach { overlay -> + key(overlay.key) { overlay.Content(Modifier.align(overlay.alignment)) } } } } + private fun overlaysToComposeOrderedByZIndex(): List<Overlay> { + if (_overlays == null) return emptyList() + + val transitions = state.currentTransitions + return if (transitions.isEmpty()) { + state.transitionState.currentOverlays.map { overlay(it) } + } else { + buildList { + val visited = mutableSetOf<OverlayKey>() + fun maybeAdd(key: OverlayKey) { + if (visited.add(key)) { + add(overlay(key)) + } + } + + transitions.fastForEach { transition -> + when (transition) { + is TransitionState.Transition.ChangeCurrentScene -> {} + is TransitionState.Transition.ShowOrHideOverlay -> + maybeAdd(transition.overlay) + is TransitionState.Transition.ReplaceOverlay -> { + maybeAdd(transition.fromOverlay) + maybeAdd(transition.toOverlay) + } + } + } + + // Make sure that all current overlays are composed. + transitions.last().currentOverlays.forEach { maybeAdd(it) } + } + } + .sortedBy { it.zIndex } + } + internal fun setScenesTargetSizeForTest(size: IntSize) { scenes.values.forEach { it.targetSize = size } } + + internal fun overlaysOrNullForTest(): Map<OverlayKey, Overlay>? = _overlays } private data class LayoutElement(private val layoutImpl: SceneTransitionLayoutImpl) : @@ -310,7 +432,7 @@ private class LayoutNode(var layoutImpl: SceneTransitionLayoutImpl) : val progress = when { overscrollSpec == null -> transition.progress - overscrollSpec.scene == transition.toScene -> 1f + overscrollSpec.content == transition.toScene -> 1f else -> 0f } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index f37ded0547ea..0ac69124f7bc 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -39,6 +39,21 @@ import kotlinx.coroutines.CoroutineScope @Stable sealed interface SceneTransitionLayoutState { /** + * The current effective scene. If a new transition is triggered, it will start from this scene. + */ + val currentScene: SceneKey + + /** + * The current set of overlays. This represents the set of overlays that will be visible on + * screen once all [currentTransitions] are finished. + * + * @see MutableSceneTransitionLayoutState.showOverlay + * @see MutableSceneTransitionLayoutState.hideOverlay + * @see MutableSceneTransitionLayoutState.replaceOverlay + */ + val currentOverlays: Set<OverlayKey> + + /** * The current [TransitionState]. All values read here are backed by the Snapshot system. * * To observe those values outside of Compose/the Snapshot system, use @@ -66,12 +81,15 @@ sealed interface SceneTransitionLayoutState { /** * Whether we are transitioning. If [from] or [to] is empty, we will also check that they match - * the scenes we are animating from and/or to. + * the contents we are animating from and/or to. */ - fun isTransitioning(from: SceneKey? = null, to: SceneKey? = null): Boolean + fun isTransitioning(from: ContentKey? = null, to: ContentKey? = null): Boolean + + /** Whether we are transitioning from [content] to [other], or from [other] to [content]. */ + fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean - /** Whether we are transitioning from [scene] to [other], or from [other] to [scene]. */ - fun isTransitioningBetween(scene: SceneKey, other: SceneKey): Boolean + /** Whether we are transitioning from or to [content]. */ + fun isTransitioningFromOrTo(content: ContentKey): Boolean } /** A [SceneTransitionLayoutState] whose target scene can be imperatively set. */ @@ -110,7 +128,50 @@ sealed interface MutableSceneTransitionLayoutState : SceneTransitionLayoutState ): TransitionState.Transition? /** Immediately snap to the given [scene]. */ - fun snapToScene(scene: SceneKey) + fun snapToScene( + scene: SceneKey, + currentOverlays: Set<OverlayKey> = transitionState.currentOverlays, + ) + + /** + * Request to show [overlay] so that it animates in from [currentScene] and ends up being + * visible on screen. + * + * After this returns, this overlay will be included in [currentOverlays]. This does nothing if + * [overlay] is already in [currentOverlays]. + */ + fun showOverlay( + overlay: OverlayKey, + animationScope: CoroutineScope, + transitionKey: TransitionKey? = null, + ) + + /** + * Request to hide [overlay] so that it animates out to [currentScene] and ends up *not* being + * visible on screen. + * + * After this returns, this overlay will not be included in [currentOverlays]. This does nothing + * if [overlay] is not in [currentOverlays]. + */ + fun hideOverlay( + overlay: OverlayKey, + animationScope: CoroutineScope, + transitionKey: TransitionKey? = null, + ) + + /** + * Replace [from] by [to] so that [from] ends up not being visible on screen and [to] ends up + * being visible. + * + * This throws if [from] is not currently in [currentOverlays] or if [to] is already in + * [currentOverlays]. + */ + fun replaceOverlay( + from: OverlayKey, + to: OverlayKey, + animationScope: CoroutineScope, + transitionKey: TransitionKey? = null, + ) } /** @@ -128,6 +189,7 @@ sealed interface MutableSceneTransitionLayoutState : SceneTransitionLayoutState fun MutableSceneTransitionLayoutState( initialScene: SceneKey, transitions: SceneTransitions = SceneTransitions.Empty, + initialOverlays: Set<OverlayKey> = emptySet(), canChangeScene: (SceneKey) -> Boolean = { true }, stateLinks: List<StateLink> = emptyList(), enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED, @@ -135,6 +197,7 @@ fun MutableSceneTransitionLayoutState( return MutableSceneTransitionLayoutStateImpl( initialScene, transitions, + initialOverlays, canChangeScene, stateLinks, enableInterruptions, @@ -145,6 +208,7 @@ fun MutableSceneTransitionLayoutState( internal class MutableSceneTransitionLayoutStateImpl( initialScene: SceneKey, override var transitions: SceneTransitions = transitions {}, + initialOverlays: Set<OverlayKey> = emptySet(), internal val canChangeScene: (SceneKey) -> Boolean = { true }, private val stateLinks: List<StateLink> = emptyList(), @@ -158,13 +222,18 @@ internal class MutableSceneTransitionLayoutStateImpl( * 1. A list with a single [TransitionState.Idle] element, when we are idle. * 2. A list with one or more [TransitionState.Transition], when we are transitioning. */ - @VisibleForTesting internal var transitionStates: List<TransitionState> by - mutableStateOf(listOf(TransitionState.Idle(initialScene))) + mutableStateOf(listOf(TransitionState.Idle(initialScene, initialOverlays))) private set + override val currentScene: SceneKey + get() = transitionState.currentScene + + override val currentOverlays: Set<OverlayKey> + get() = transitionState.currentOverlays + override val transitionState: TransitionState - get() = transitionStates.last() + get() = transitionStates[transitionStates.lastIndex] override val currentTransitions: List<TransitionState.Transition> get() { @@ -194,14 +263,19 @@ internal class MutableSceneTransitionLayoutStateImpl( } } - override fun isTransitioning(from: SceneKey?, to: SceneKey?): Boolean { + override fun isTransitioning(from: ContentKey?, to: ContentKey?): Boolean { val transition = currentTransition ?: return false return transition.isTransitioning(from, to) } - override fun isTransitioningBetween(scene: SceneKey, other: SceneKey): Boolean { + override fun isTransitioningBetween(content: ContentKey, other: ContentKey): Boolean { + val transition = currentTransition ?: return false + return transition.isTransitioningBetween(content, other) + } + + override fun isTransitioningFromOrTo(content: ContentKey): Boolean { val transition = currentTransition ?: return false - return transition.isTransitioningBetween(scene, other) + return transition.isTransitioningFromOrTo(content) } override fun setTargetScene( @@ -227,30 +301,32 @@ internal class MutableSceneTransitionLayoutStateImpl( * * Important: you *must* call [finishTransition] once the transition is finished. */ - internal fun startTransition( - transition: TransitionState.Transition.ChangeCurrentScene, - chain: Boolean = true, - ) { + internal fun startTransition(transition: TransitionState.Transition, chain: Boolean = true) { checkThread() + // Set the current scene and overlays on the transition. + val currentState = transitionState + transition.currentSceneWhenTransitionStarted = currentState.currentScene + transition.currentOverlaysWhenTransitionStarted = currentState.currentOverlays + // Compute the [TransformationSpec] when the transition starts. - val fromScene = transition.fromScene - val toScene = transition.toScene + val fromContent = transition.fromContent + val toContent = transition.toContent val orientation = (transition as? TransitionState.HasOverscrollProperties)?.orientation // Update the transition specs. transition.transformationSpec = transitions - .transitionSpec(fromScene, toScene, key = transition.key) + .transitionSpec(fromContent, toContent, key = transition.key) .transformationSpec() transition.previewTransformationSpec = transitions - .transitionSpec(fromScene, toScene, key = transition.key) + .transitionSpec(fromContent, toContent, key = transition.key) .previewTransformationSpec() if (orientation != null) { transition.updateOverscrollSpecs( - fromSpec = transitions.overscrollSpec(fromScene, orientation), - toSpec = transitions.overscrollSpec(toScene, orientation), + fromSpec = transitions.overscrollSpec(fromContent, orientation), + toSpec = transitions.overscrollSpec(toContent, orientation), ) } else { transition.updateOverscrollSpecs(fromSpec = null, toSpec = null) @@ -331,31 +407,27 @@ internal class MutableSceneTransitionLayoutStateImpl( } private fun setupTransitionLinks(transition: TransitionState.Transition) { - when (transition) { - is TransitionState.Transition.ChangeCurrentScene -> { - stateLinks.fastForEach { stateLink -> - val matchingLinks = - stateLink.transitionLinks.fastFilter { it.isMatchingLink(transition) } - if (matchingLinks.isEmpty()) return@fastForEach - if (matchingLinks.size > 1) error("More than one link matched.") - - val targetCurrentScene = stateLink.target.transitionState.currentScene - val matchingLink = matchingLinks[0] - - if (!matchingLink.targetIsInValidState(targetCurrentScene)) return@fastForEach - - val linkedTransition = - LinkedTransition( - originalTransition = transition, - fromScene = targetCurrentScene, - toScene = matchingLink.targetTo, - key = matchingLink.targetTransitionKey, - ) - - stateLink.target.startTransition(linkedTransition) - transition.activeTransitionLinks[stateLink] = linkedTransition - } - } + stateLinks.fastForEach { stateLink -> + val matchingLinks = + stateLink.transitionLinks.fastFilter { it.isMatchingLink(transition) } + if (matchingLinks.isEmpty()) return@fastForEach + if (matchingLinks.size > 1) error("More than one link matched.") + + val targetCurrentScene = stateLink.target.transitionState.currentScene + val matchingLink = matchingLinks[0] + + if (!matchingLink.targetIsInValidState(targetCurrentScene)) return@fastForEach + + val linkedTransition = + LinkedTransition( + originalTransition = transition, + fromScene = targetCurrentScene, + toScene = matchingLink.targetTo, + key = matchingLink.targetTransitionKey, + ) + + stateLink.target.startTransition(linkedTransition) + transition.activeTransitionLinks[stateLink] = linkedTransition } } @@ -408,23 +480,28 @@ internal class MutableSceneTransitionLayoutStateImpl( // If all transitions are finished, we are idle. if (i == nStates) { check(finishedTransitions.isEmpty()) - this.transitionStates = listOf(TransitionState.Idle(lastTransition.currentScene)) + this.transitionStates = + listOf( + TransitionState.Idle( + lastTransition.currentScene, + lastTransition.currentOverlays, + ) + ) } else if (i > 0) { this.transitionStates = transitionStates.subList(fromIndex = i, toIndex = nStates) } } - override fun snapToScene(scene: SceneKey) { + override fun snapToScene(scene: SceneKey, currentOverlays: Set<OverlayKey>) { checkThread() // Force finish all transitions. while (currentTransitions.isNotEmpty()) { - val transition = transitionStates[0] as TransitionState.Transition - finishTransition(transition) + finishTransition(transitionStates[0] as TransitionState.Transition) } check(transitionStates.size == 1) - transitionStates = listOf(TransitionState.Idle(scene)) + transitionStates = listOf(TransitionState.Idle(scene, currentOverlays)) } private fun finishActiveTransitionLinks(transition: TransitionState.Transition) { @@ -466,6 +543,137 @@ internal class MutableSceneTransitionLayoutStateImpl( false } } + + override fun showOverlay( + overlay: OverlayKey, + animationScope: CoroutineScope, + transitionKey: TransitionKey? + ) { + checkThread() + + // Overlay is already shown, do nothing. + val currentState = transitionState + if (overlay in currentState.currentOverlays) { + return + } + + val fromScene = currentState.currentScene + fun animate( + replacedTransition: TransitionState.Transition.ShowOrHideOverlay? = null, + reversed: Boolean = false, + ) { + animationScope.showOrHideOverlay( + layoutState = this@MutableSceneTransitionLayoutStateImpl, + overlay = overlay, + fromOrToScene = fromScene, + isShowing = true, + transitionKey = transitionKey, + replacedTransition = replacedTransition, + reversed = reversed, + ) + } + + if ( + currentState is TransitionState.Transition.ShowOrHideOverlay && + currentState.overlay == overlay && + currentState.fromOrToScene == fromScene + ) { + animate( + replacedTransition = currentState, + reversed = overlay == currentState.fromContent + ) + } else { + animate() + } + } + + override fun hideOverlay( + overlay: OverlayKey, + animationScope: CoroutineScope, + transitionKey: TransitionKey? + ) { + checkThread() + + // Overlay is not shown, do nothing. + val currentState = transitionState + if (!currentState.currentOverlays.contains(overlay)) { + return + } + + val toScene = currentState.currentScene + fun animate( + replacedTransition: TransitionState.Transition.ShowOrHideOverlay? = null, + reversed: Boolean = false, + ) { + animationScope.showOrHideOverlay( + layoutState = this@MutableSceneTransitionLayoutStateImpl, + overlay = overlay, + fromOrToScene = toScene, + isShowing = false, + transitionKey = transitionKey, + replacedTransition = replacedTransition, + reversed = reversed, + ) + } + + if ( + currentState is TransitionState.Transition.ShowOrHideOverlay && + currentState.overlay == overlay && + currentState.fromOrToScene == toScene + ) { + animate(replacedTransition = currentState, reversed = overlay == currentState.toContent) + } else { + animate() + } + } + + override fun replaceOverlay( + from: OverlayKey, + to: OverlayKey, + animationScope: CoroutineScope, + transitionKey: TransitionKey? + ) { + checkThread() + + val currentState = transitionState + require(from != to) { + "replaceOverlay must be called with different overlays (from = to = ${from.debugName})" + } + require(from in currentState.currentOverlays) { + "Overlay ${from.debugName} is not shown so it can't be replaced by ${to.debugName}" + } + require(to !in currentState.currentOverlays) { + "Overlay ${to.debugName} is already shown so it can't replace ${from.debugName}" + } + + fun animate( + replacedTransition: TransitionState.Transition.ReplaceOverlay? = null, + reversed: Boolean = false, + ) { + animationScope.replaceOverlay( + layoutState = this@MutableSceneTransitionLayoutStateImpl, + fromOverlay = if (reversed) to else from, + toOverlay = if (reversed) from else to, + transitionKey = transitionKey, + replacedTransition = replacedTransition, + reversed = reversed, + ) + } + + if (currentState is TransitionState.Transition.ReplaceOverlay) { + if (currentState.fromOverlay == from && currentState.toOverlay == to) { + animate(replacedTransition = currentState, reversed = false) + return + } + + if (currentState.fromOverlay == to && currentState.toOverlay == from) { + animate(replacedTransition = currentState, reversed = true) + return + } + } + + animate() + } } private const val TAG = "SceneTransitionLayoutState" diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt index d35d95685d22..cefcff75f13a 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt @@ -49,16 +49,16 @@ internal constructor( ) { private val transitionCache = mutableMapOf< - SceneKey, - MutableMap<SceneKey, MutableMap<TransitionKey?, TransitionSpecImpl>> + ContentKey, + MutableMap<ContentKey, MutableMap<TransitionKey?, TransitionSpecImpl>> >() private val overscrollCache = - mutableMapOf<SceneKey, MutableMap<Orientation, OverscrollSpecImpl?>>() + mutableMapOf<ContentKey, MutableMap<Orientation, OverscrollSpecImpl?>>() internal fun transitionSpec( - from: SceneKey, - to: SceneKey, + from: ContentKey, + to: ContentKey, key: TransitionKey?, ): TransitionSpecImpl { return transitionCache @@ -67,7 +67,11 @@ internal constructor( .getOrPut(key) { findSpec(from, to, key) } } - private fun findSpec(from: SceneKey, to: SceneKey, key: TransitionKey?): TransitionSpecImpl { + private fun findSpec( + from: ContentKey, + to: ContentKey, + key: TransitionKey? + ): TransitionSpecImpl { val spec = transition(from, to, key) { it.from == from && it.to == to } if (spec != null) { return spec @@ -93,8 +97,8 @@ internal constructor( } private fun transition( - from: SceneKey, - to: SceneKey, + from: ContentKey, + to: ContentKey, key: TransitionKey?, filter: (TransitionSpecImpl) -> Boolean, ): TransitionSpecImpl? { @@ -110,16 +114,16 @@ internal constructor( return match } - private fun defaultTransition(from: SceneKey, to: SceneKey) = + private fun defaultTransition(from: ContentKey, to: ContentKey) = TransitionSpecImpl(key = null, from, to, null, null, TransformationSpec.EmptyProvider) - internal fun overscrollSpec(scene: SceneKey, orientation: Orientation): OverscrollSpecImpl? = + internal fun overscrollSpec(scene: ContentKey, orientation: Orientation): OverscrollSpecImpl? = overscrollCache .getOrPut(scene) { mutableMapOf() } - .getOrPut(orientation) { overscroll(scene, orientation) { it.scene == scene } } + .getOrPut(orientation) { overscroll(scene, orientation) { it.content == scene } } private fun overscroll( - scene: SceneKey, + scene: ContentKey, orientation: Orientation, filter: (OverscrollSpecImpl) -> Boolean, ): OverscrollSpecImpl? { @@ -264,10 +268,10 @@ internal class TransitionSpecImpl( previewTransformationSpec?.invoke() } -/** The definition of the overscroll behavior of the [scene]. */ +/** The definition of the overscroll behavior of the [content]. */ interface OverscrollSpec { /** The scene we are over scrolling. */ - val scene: SceneKey + val content: ContentKey /** The orientation of this [OverscrollSpec]. */ val orientation: Orientation @@ -288,7 +292,7 @@ interface OverscrollSpec { } internal class OverscrollSpecImpl( - override val scene: SceneKey, + override val content: ContentKey, override val orientation: Orientation, override val transformationSpec: TransformationSpecImpl, override val progressConverter: ProgressConverter?, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index 5cc194d32424..2b5953c586db 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -523,8 +523,8 @@ fun interface ProgressConverter { fun convert(progress: Float): Float companion object { - /** Keeps scrolling linearly */ - val Default = linear() + /** Starts linearly with some resistance and slowly approaches to 0.2f */ + val Default = tanh(maxProgress = 0.2f, tilt = 3f) /** * The scroll stays linear, with [factor] you can control how much resistance there is. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt index 523e5bdd7203..18e356f71768 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt @@ -107,7 +107,7 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { ): OverscrollSpec { val spec = OverscrollSpecImpl( - scene = scene, + content = scene, orientation = orientation, transformationSpec = TransformationSpecImpl( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt index 0f668044112e..9851b32c42c4 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt @@ -35,7 +35,7 @@ internal class ElementStateScopeImpl( } override fun SceneKey.targetSize(): IntSize? { - return layoutImpl.scenes[this]?.targetSize.takeIf { it != IntSize.Zero } + return layoutImpl.sceneOrNull(this)?.targetSize.takeIf { it != IntSize.Zero } } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt index 6f608cbc1d7f..6bc1754150fe 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt @@ -96,6 +96,8 @@ internal sealed class Content( .approachLayout( isMeasurementApproachInProgress = { layoutImpl.state.isTransitioning() } ) { measurable, constraints -> + // TODO(b/353679003): Use the ModifierNode API to set this *before* the approach + // pass is started. targetSize = lookaheadSize val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { placeable.place(0, 0) } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Overlay.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Overlay.kt new file mode 100644 index 000000000000..ccec9e834385 --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Overlay.kt @@ -0,0 +1,46 @@ +/* + * 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.compose.animation.scene.content + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import com.android.compose.animation.scene.ContentScope +import com.android.compose.animation.scene.OverlayKey +import com.android.compose.animation.scene.SceneTransitionLayoutImpl +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult + +/** An overlay defined in a [SceneTransitionLayout]. */ +@Stable +internal class Overlay( + override val key: OverlayKey, + layoutImpl: SceneTransitionLayoutImpl, + content: @Composable ContentScope.() -> Unit, + actions: Map<UserAction.Resolved, UserActionResult>, + zIndex: Float, + alignment: Alignment, +) : Content(key, layoutImpl, content, actions, zIndex) { + var alignment by mutableStateOf(alignment) + + override fun toString(): String { + return "Overlay(key=$key)" + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt index 22df34b34b97..fdb019f5a604 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt @@ -21,7 +21,11 @@ import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.spring import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import com.android.compose.animation.scene.ContentKey +import com.android.compose.animation.scene.MutableSceneTransitionLayoutState +import com.android.compose.animation.scene.OverlayKey import com.android.compose.animation.scene.OverscrollScope import com.android.compose.animation.scene.OverscrollSpecImpl import com.android.compose.animation.scene.ProgressVisibilityThreshold @@ -49,9 +53,20 @@ sealed interface TransitionState { */ val currentScene: SceneKey + /** + * The current set of overlays. This represents the set of overlays that will be visible on + * screen once all transitions are finished. + * + * @see MutableSceneTransitionLayoutState.showOverlay + * @see MutableSceneTransitionLayoutState.hideOverlay + * @see MutableSceneTransitionLayoutState.replaceOverlay + */ + val currentOverlays: Set<OverlayKey> + /** The scene [currentScene] is idle. */ data class Idle( override val currentScene: SceneKey, + override val currentOverlays: Set<OverlayKey> = emptySet(), ) : TransitionState sealed class Transition( @@ -69,7 +84,125 @@ sealed interface TransitionState { /** The transition that `this` transition is replacing, if any. */ replacedTransition: Transition? = null, - ) : Transition(fromScene, toScene, replacedTransition) + ) : Transition(fromScene, toScene, replacedTransition) { + final override val currentOverlays: Set<OverlayKey> + get() { + // The set of overlays does not change in a [ChangeCurrentScene] transition. + return currentOverlaysWhenTransitionStarted + } + } + + /** + * A transition that is animating one or more overlays and for which [currentOverlays] will + * change over the course of the transition. + */ + sealed class OverlayTransition( + fromContent: ContentKey, + toContent: ContentKey, + replacedTransition: Transition?, + ) : Transition(fromContent, toContent, replacedTransition) { + final override val currentScene: SceneKey + get() { + // The current scene does not change during overlay transitions. + return currentSceneWhenTransitionStarted + } + + // Note: We use deriveStateOf() so that the computed set is cached and reused when the + // inputs of the computations don't change, to avoid recomputing and allocating a new + // set every time currentOverlays is called (which is every frame and for each element). + final override val currentOverlays: Set<OverlayKey> by derivedStateOf { + computeCurrentOverlays() + } + + protected abstract fun computeCurrentOverlays(): Set<OverlayKey> + } + + /** The [overlay] is either showing from [fromOrToScene] or hiding into [fromOrToScene]. */ + abstract class ShowOrHideOverlay( + val overlay: OverlayKey, + val fromOrToScene: SceneKey, + fromContent: ContentKey, + toContent: ContentKey, + replacedTransition: Transition? = null, + ) : OverlayTransition(fromContent, toContent, replacedTransition) { + /** + * Whether [overlay] is effectively shown. For instance, this will be `false` when + * starting a swipe transition to show [overlay] and will be `true` only once the swipe + * transition is committed. + */ + protected abstract val isEffectivelyShown: Boolean + + init { + check( + (fromContent == fromOrToScene && toContent == overlay) || + (fromContent == overlay && toContent == fromOrToScene) + ) + } + + final override fun computeCurrentOverlays(): Set<OverlayKey> { + return if (isEffectivelyShown) { + currentOverlaysWhenTransitionStarted + overlay + } else { + currentOverlaysWhenTransitionStarted - overlay + } + } + } + + /** We are transitioning from [fromOverlay] to [toOverlay]. */ + abstract class ReplaceOverlay( + val fromOverlay: OverlayKey, + val toOverlay: OverlayKey, + replacedTransition: Transition? = null, + ) : + OverlayTransition( + fromContent = fromOverlay, + toContent = toOverlay, + replacedTransition, + ) { + /** + * The current effective overlay, either [fromOverlay] or [toOverlay]. For instance, + * this will be [fromOverlay] when starting a swipe transition that replaces + * [fromOverlay] by [toOverlay] and will [toOverlay] once the swipe transition is + * committed. + */ + protected abstract val effectivelyShownOverlay: OverlayKey + + init { + check(fromOverlay != toOverlay) + } + + final override fun computeCurrentOverlays(): Set<OverlayKey> { + return when (effectivelyShownOverlay) { + fromOverlay -> + computeCurrentOverlays(include = fromOverlay, exclude = toOverlay) + toOverlay -> computeCurrentOverlays(include = toOverlay, exclude = fromOverlay) + else -> + error( + "effectivelyShownOverlay=$effectivelyShownOverlay, should be " + + "equal to fromOverlay=$fromOverlay or toOverlay=$toOverlay" + ) + } + } + + private fun computeCurrentOverlays( + include: OverlayKey, + exclude: OverlayKey + ): Set<OverlayKey> { + return buildSet { + addAll(currentOverlaysWhenTransitionStarted) + remove(exclude) + add(include) + } + } + } + + /** + * The current scene and overlays observed right when this transition started. These are set + * when this transition is started in + * [com.android.compose.animation.scene.MutableSceneTransitionLayoutStateImpl.startTransition]. + */ + internal lateinit var currentSceneWhenTransitionStarted: SceneKey + internal lateinit var currentOverlaysWhenTransitionStarted: Set<OverlayKey> /** * The key of this transition. This should usually be null, but it can be specified to use a @@ -163,6 +296,11 @@ sealed interface TransitionState { isTransitioning(from = other, to = content) } + /** Whether we are transitioning from or to [content]. */ + fun isTransitioningFromOrTo(content: ContentKey): Boolean { + return fromContent == content || toContent == content + } + /** * Force this transition to finish and animate to an [Idle] state. * diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt index 89b004046475..59ddb1354073 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.Job /** A linked transition which is driven by a [originalTransition]. */ internal class LinkedTransition( - private val originalTransition: TransitionState.Transition.ChangeCurrentScene, + private val originalTransition: TransitionState.Transition, fromScene: SceneKey, toScene: SceneKey, override val key: TransitionKey? = null, @@ -32,8 +32,8 @@ internal class LinkedTransition( override val currentScene: SceneKey get() { return when (originalTransition.currentScene) { - originalTransition.fromScene -> fromScene - originalTransition.toScene -> toScene + originalTransition.fromContent -> fromScene + originalTransition.toContent -> toScene else -> error("Original currentScene is neither FromScene nor ToScene") } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt index c29bf212ec9c..c830ca4fa7c0 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt @@ -16,6 +16,7 @@ package com.android.compose.animation.scene.transition.link +import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateImpl import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneTransitionLayoutState @@ -35,8 +36,8 @@ class StateLink(target: SceneTransitionLayoutState, val transitionLinks: List<Tr * target to `SceneA` from any current scene. */ class TransitionLink( - val sourceFrom: SceneKey?, - val sourceTo: SceneKey?, + val sourceFrom: ContentKey?, + val sourceTo: ContentKey?, val targetFrom: SceneKey?, val targetTo: SceneKey, val targetTransitionKey: TransitionKey? = null, @@ -50,15 +51,15 @@ class StateLink(target: SceneTransitionLayoutState, val transitionLinks: List<Tr } internal fun isMatchingLink( - transition: TransitionState.Transition.ChangeCurrentScene, + transition: TransitionState.Transition, ): Boolean { - return (sourceFrom == null || sourceFrom == transition.fromScene) && - (sourceTo == null || sourceTo == transition.toScene) + return (sourceFrom == null || sourceFrom == transition.fromContent) && + (sourceTo == null || sourceTo == transition.toContent) } - internal fun targetIsInValidState(targetCurrentScene: SceneKey): Boolean { - return (targetFrom == null || targetFrom == targetCurrentScene) && - targetTo != targetCurrentScene + internal fun targetIsInValidState(targetCurrentContent: ContentKey): Boolean { + return (targetFrom == null || targetFrom == targetCurrentContent) && + targetTo != targetCurrentContent } } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt index 01895c91a399..8ebb42aa24f8 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt @@ -168,10 +168,7 @@ class AnimatedSharedAsStateTest { assertThat(lastValueInTo).isEqualTo(expectedValues) } - after { - assertThat(lastValueInFrom).isEqualTo(toValues) - assertThat(lastValueInTo).isEqualTo(toValues) - } + after { assertThat(lastValueInTo).isEqualTo(toValues) } } } @@ -229,10 +226,7 @@ class AnimatedSharedAsStateTest { assertThat(lastValueInTo).isEqualTo(lerp(fromValues, toValues, fraction = 0.75f)) } - after { - assertThat(lastValueInFrom).isEqualTo(toValues) - assertThat(lastValueInTo).isEqualTo(toValues) - } + after { assertThat(lastValueInTo).isEqualTo(toValues) } } } @@ -288,10 +282,7 @@ class AnimatedSharedAsStateTest { assertThat(lastValueInTo).isEqualTo(expectedValues) } - after { - assertThat(lastValueInFrom).isEqualTo(toValues) - assertThat(lastValueInTo).isEqualTo(toValues) - } + after { assertThat(lastValueInTo).isEqualTo(toValues) } } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index 72a16b7fbd6f..7d8e898e9ab2 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -65,19 +65,19 @@ class DraggableHandlerTest { var layoutDirection = LayoutDirection.Rtl set(value) { field = value - layoutImpl.updateScenes(scenesBuilder, layoutDirection) + layoutImpl.updateContents(scenesBuilder, layoutDirection) } var mutableUserActionsA = mapOf(Swipe.Up to SceneB, Swipe.Down to SceneC) set(value) { field = value - layoutImpl.updateScenes(scenesBuilder, layoutDirection) + layoutImpl.updateContents(scenesBuilder, layoutDirection) } var mutableUserActionsB = mapOf(Swipe.Up to SceneC, Swipe.Down to SceneA) set(value) { field = value - layoutImpl.updateScenes(scenesBuilder, layoutDirection) + layoutImpl.updateContents(scenesBuilder, layoutDirection) } private val scenesBuilder: SceneTransitionLayoutScope.() -> Unit = { @@ -1020,7 +1020,7 @@ class DraggableHandlerTest { // We scrolled down, under scene C there is nothing, so we can use the overscroll spec assertThat(layoutState.currentTransition?.currentOverscrollSpec).isNotNull() - assertThat(layoutState.currentTransition?.currentOverscrollSpec?.scene).isEqualTo(SceneC) + assertThat(layoutState.currentTransition?.currentOverscrollSpec?.content).isEqualTo(SceneC) val transition = layoutState.currentTransition assertThat(transition).isNotNull() assertThat(transition!!.progress).isEqualTo(-0.1f) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 682fe95c66a5..770c0f8dbb8f 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -724,6 +724,7 @@ class ElementTest { layoutHeight = layoutHeight, sceneTransitions = { overscroll(SceneB, Orientation.Vertical) { + progressConverter = ProgressConverter.linear() // On overscroll 100% -> Foo should translate by overscrollTranslateY translate(TestElements.Foo, y = overscrollTranslateY) } @@ -780,6 +781,7 @@ class ElementTest { transitions = transitions { overscroll(SceneB, Orientation.Vertical) { + progressConverter = ProgressConverter.linear() translate(TestElements.Foo, y = overscrollTranslateY) } } @@ -921,6 +923,7 @@ class ElementTest { layoutHeight = layoutHeight, sceneTransitions = { overscroll(SceneB, Orientation.Vertical) { + progressConverter = ProgressConverter.linear() // On overscroll 100% -> Foo should translate by layoutHeight translate(TestElements.Foo, y = { absoluteDistance }) } @@ -1015,7 +1018,7 @@ class ElementTest { layoutHeight = layoutHeight, sceneTransitions = { // Overscroll progress will be linear (by default) - defaultOverscrollProgressConverter = ProgressConverter { it } + defaultOverscrollProgressConverter = ProgressConverter.linear() overscroll(SceneB, Orientation.Vertical) { // This override the defaultOverscrollProgressConverter @@ -1125,6 +1128,7 @@ class ElementTest { ) overscroll(SceneB, Orientation.Vertical) { + progressConverter = ProgressConverter.linear() // On overscroll 100% -> Foo should translate by layoutHeight translate(TestElements.Foo, y = { absoluteDistance }) } @@ -1861,6 +1865,7 @@ class ElementTest { SceneA, transitions { overscroll(SceneB, Orientation.Vertical) { + progressConverter = ProgressConverter.linear() translate(TestElements.Foo, y = 15.dp) } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt index b7f50fd8d685..a549d0355a26 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt @@ -106,7 +106,7 @@ class MovableElementTest { rule .onNode( hasText("count: 3") and - hasParent(isElement(TestElements.Foo, scene = SceneA)) + hasParent(isElement(TestElements.Foo, content = SceneA)) ) .assertExists() .assertIsNotDisplayed() @@ -114,7 +114,7 @@ class MovableElementTest { rule .onNode( hasText("count: 0") and - hasParent(isElement(TestElements.Foo, scene = SceneB)) + hasParent(isElement(TestElements.Foo, content = SceneB)) ) .assertIsDisplayed() .assertSizeIsEqualTo(75.dp, 75.dp) @@ -213,7 +213,7 @@ class MovableElementTest { rule .onNode( hasText("count: 3") and - hasParent(isElement(TestElements.Foo, scene = SceneA)) + hasParent(isElement(TestElements.Foo, content = SceneA)) ) .assertIsDisplayed() .assertSizeIsEqualTo(75.dp, 75.dp) @@ -234,7 +234,7 @@ class MovableElementTest { rule .onNode( hasText("count: 3") and - hasParent(isElement(TestElements.Foo, scene = SceneB)) + hasParent(isElement(TestElements.Foo, content = SceneB)) ) .assertIsDisplayed() @@ -324,7 +324,7 @@ class MovableElementTest { fun movableElementScopeExtendsBoxScope() { val key = MovableElementKey("Foo", contents = setOf(SceneA)) rule.setContent { - TestContentScope { + TestContentScope(currentScene = SceneA) { MovableElement(key, Modifier.size(200.dp)) { content { Box(Modifier.testTag("bottomEnd").align(Alignment.BottomEnd)) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt new file mode 100644 index 000000000000..85db418f6020 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt @@ -0,0 +1,527 @@ +/* + * 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.compose.animation.scene + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.assertPositionInRootIsEqualTo +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.TestOverlays.OverlayA +import com.android.compose.animation.scene.TestOverlays.OverlayB +import com.android.compose.animation.scene.TestScenes.SceneA +import com.android.compose.test.assertSizeIsEqualTo +import kotlinx.coroutines.CoroutineScope +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class OverlayTest { + @get:Rule val rule = createComposeRule() + + @Composable + private fun ContentScope.Foo(width: Dp = 100.dp, height: Dp = 100.dp) { + Box(Modifier.element(TestElements.Foo).size(width, height)) + } + + @Test + fun showThenHideOverlay() { + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + lateinit var coroutineScope: CoroutineScope + rule.setContent { + coroutineScope = rememberCoroutineScope() + SceneTransitionLayout(state, Modifier.size(200.dp)) { + scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } } + overlay(OverlayA) { Foo() } + } + } + + // Initial state: overlay A is not shown, so Foo is displayed at the top left in scene A. + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertIsDisplayed() + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(0.dp, 0.dp) + rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() + + // Show overlay A: Foo is now centered on screen and placed in overlay A. It is not placed + // in scene A. + rule.runOnUiThread { state.showOverlay(OverlayA, coroutineScope) } + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertExists() + .assertIsNotDisplayed() + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(50.dp, 50.dp) + + // Hide overlay A: back to initial state, top-left in scene A. + rule.runOnUiThread { state.hideOverlay(OverlayA, coroutineScope) } + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertIsDisplayed() + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(0.dp, 0.dp) + rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() + } + + @Test + fun multipleOverlays() { + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + lateinit var coroutineScope: CoroutineScope + rule.setContent { + coroutineScope = rememberCoroutineScope() + SceneTransitionLayout(state, Modifier.size(200.dp)) { + scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } } + overlay(OverlayA) { Foo() } + overlay(OverlayB) { Foo() } + } + } + + // Initial state. + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertIsDisplayed() + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(0.dp, 0.dp) + rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() + rule.onNode(isElement(TestElements.Foo, content = OverlayB)).assertDoesNotExist() + + // Show overlay A. + rule.runOnUiThread { state.showOverlay(OverlayA, coroutineScope) } + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertExists() + .assertIsNotDisplayed() + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(50.dp, 50.dp) + rule.onNode(isElement(TestElements.Foo, content = OverlayB)).assertDoesNotExist() + + // Replace overlay A by overlay B. + rule.runOnUiThread { state.replaceOverlay(OverlayA, OverlayB, coroutineScope) } + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertExists() + .assertIsNotDisplayed() + rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() + rule + .onNode(isElement(TestElements.Foo, content = OverlayB)) + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(50.dp, 50.dp) + + // Show overlay A: Foo is still placed in B because it has a higher zIndex, but it now + // exists in A as well. + rule.runOnUiThread { state.showOverlay(OverlayA, coroutineScope) } + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertExists() + .assertIsNotDisplayed() + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertExists() + .assertIsNotDisplayed() + rule + .onNode(isElement(TestElements.Foo, content = OverlayB)) + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(50.dp, 50.dp) + + // Hide overlay B. + rule.runOnUiThread { state.hideOverlay(OverlayB, coroutineScope) } + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertExists() + .assertIsNotDisplayed() + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(50.dp, 50.dp) + rule.onNode(isElement(TestElements.Foo, content = OverlayB)).assertDoesNotExist() + + // Hide overlay A. + rule.runOnUiThread { state.hideOverlay(OverlayA, coroutineScope) } + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertIsDisplayed() + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(0.dp, 0.dp) + rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() + rule.onNode(isElement(TestElements.Foo, content = OverlayB)).assertDoesNotExist() + } + + @Test + fun movableElement() { + val key = MovableElementKey("MovableBar", contents = setOf(SceneA, OverlayA, OverlayB)) + val elementChildTag = "elementChildTag" + + fun elementChild(content: ContentKey) = hasTestTag(elementChildTag) and inContent(content) + + @Composable + fun ContentScope.MovableBar() { + MovableElement(key, Modifier) { + content { Box(Modifier.testTag(elementChildTag).size(100.dp)) } + } + } + + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + lateinit var coroutineScope: CoroutineScope + rule.setContent { + coroutineScope = rememberCoroutineScope() + SceneTransitionLayout(state, Modifier.size(200.dp)) { + scene(SceneA) { Box(Modifier.fillMaxSize()) { MovableBar() } } + overlay(OverlayA) { MovableBar() } + overlay(OverlayB) { MovableBar() } + } + } + + // Initial state. + rule + .onNode(elementChild(content = SceneA)) + .assertIsDisplayed() + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(0.dp, 0.dp) + rule.onNode(elementChild(content = OverlayA)).assertDoesNotExist() + rule.onNode(elementChild(content = OverlayB)).assertDoesNotExist() + + // Show overlay A: movable element child only exists (is only composed) in overlay A. + rule.runOnUiThread { state.showOverlay(OverlayA, coroutineScope) } + rule.onNode(elementChild(content = SceneA)).assertDoesNotExist() + rule + .onNode(elementChild(content = OverlayA)) + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(50.dp, 50.dp) + rule.onNode(elementChild(content = OverlayB)).assertDoesNotExist() + + // Replace overlay A by overlay B: element child is only in overlay B. + rule.runOnUiThread { state.replaceOverlay(OverlayA, OverlayB, coroutineScope) } + rule.onNode(elementChild(content = SceneA)).assertDoesNotExist() + rule.onNode(elementChild(content = OverlayA)).assertDoesNotExist() + rule + .onNode(elementChild(content = OverlayB)) + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(50.dp, 50.dp) + + // Show overlay A: element child still only exists in overlay B because it has a higher + // zIndex. + rule.runOnUiThread { state.showOverlay(OverlayA, coroutineScope) } + rule.onNode(elementChild(content = SceneA)).assertDoesNotExist() + rule.onNode(elementChild(content = OverlayA)).assertDoesNotExist() + rule + .onNode(elementChild(content = OverlayB)) + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(50.dp, 50.dp) + + // Hide overlay B: element child is in overlay A. + rule.runOnUiThread { state.hideOverlay(OverlayB, coroutineScope) } + rule.onNode(elementChild(content = SceneA)).assertDoesNotExist() + rule + .onNode(elementChild(content = OverlayA)) + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(50.dp, 50.dp) + rule.onNode(elementChild(content = OverlayB)).assertDoesNotExist() + + // Hide overlay A: element child is in scene A. + rule.runOnUiThread { state.hideOverlay(OverlayA, coroutineScope) } + rule + .onNode(elementChild(content = SceneA)) + .assertIsDisplayed() + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(0.dp, 0.dp) + rule.onNode(elementChild(content = OverlayA)).assertDoesNotExist() + rule.onNode(elementChild(content = OverlayB)).assertDoesNotExist() + } + + @Test + fun overlayAlignment() { + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutState(SceneA, initialOverlays = setOf(OverlayA)) + } + var alignment by mutableStateOf(Alignment.Center) + rule.setContent { + SceneTransitionLayout(state, Modifier.size(200.dp)) { + scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } } + overlay(OverlayA, alignment) { Foo() } + } + } + + // Initial state: 100x100dp centered in 200x200dp layout. + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(50.dp, 50.dp) + + // BottomStart. + alignment = Alignment.BottomStart + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(0.dp, 100.dp) + + // TopEnd. + alignment = Alignment.TopEnd + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertSizeIsEqualTo(100.dp) + .assertPositionInRootIsEqualTo(100.dp, 0.dp) + } + + @Test + fun overlayMaxSizeIsCurrentSceneSize() { + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutState(SceneA, initialOverlays = setOf(OverlayA)) + } + + val contentTag = "overlayContent" + rule.setContent { + SceneTransitionLayout(state) { + scene(SceneA) { Box(Modifier.size(100.dp)) { Foo() } } + overlay(OverlayA) { Box(Modifier.testTag(contentTag).fillMaxSize()) } + } + } + + // Max overlay size is the size of the layout without overlays, not the (max) possible size + // of the layout. + rule.onNodeWithTag(contentTag).assertSizeIsEqualTo(100.dp) + } + + @Test + fun showAnimation() { + rule.testShowOverlayTransition( + fromSceneContent = { + Box(Modifier.size(width = 180.dp, height = 120.dp)) { + Foo(width = 60.dp, height = 40.dp) + } + }, + overlayContent = { Foo(width = 100.dp, height = 80.dp) }, + transition = { + // 4 frames of animation + spec = tween(4 * 16, easing = LinearEasing) + }, + ) { + // Foo moves from (0,0) with a size of 60x40dp to centered (in a 180x120dp Box) with a + // size of 100x80dp, so at (40,20). + before { + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertSizeIsEqualTo(60.dp, 40.dp) + .assertPositionInRootIsEqualTo(0.dp, 0.dp) + rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() + } + + at(16) { + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertExists() + .assertIsNotDisplayed() + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertSizeIsEqualTo(70.dp, 50.dp) + .assertPositionInRootIsEqualTo(10.dp, 5.dp) + } + + at(32) { + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertExists() + .assertIsNotDisplayed() + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertSizeIsEqualTo(80.dp, 60.dp) + .assertPositionInRootIsEqualTo(20.dp, 10.dp) + } + + at(48) { + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertExists() + .assertIsNotDisplayed() + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertSizeIsEqualTo(90.dp, 70.dp) + .assertPositionInRootIsEqualTo(30.dp, 15.dp) + } + + after { + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertExists() + .assertIsNotDisplayed() + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertSizeIsEqualTo(100.dp, 80.dp) + .assertPositionInRootIsEqualTo(40.dp, 20.dp) + } + } + } + + @Test + fun hideAnimation() { + rule.testHideOverlayTransition( + toSceneContent = { + Box(Modifier.size(width = 180.dp, height = 120.dp)) { + Foo(width = 60.dp, height = 40.dp) + } + }, + overlayContent = { Foo(width = 100.dp, height = 80.dp) }, + transition = { + // 4 frames of animation + spec = tween(4 * 16, easing = LinearEasing) + }, + ) { + // Foo moves from centered (in a 180x120dp Box) with a size of 100x80dp, so at (40,20), + // to (0,0) with a size of 60x40dp. + before { + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertExists() + .assertIsNotDisplayed() + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertSizeIsEqualTo(100.dp, 80.dp) + .assertPositionInRootIsEqualTo(40.dp, 20.dp) + } + + at(16) { + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertExists() + .assertIsNotDisplayed() + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertSizeIsEqualTo(90.dp, 70.dp) + .assertPositionInRootIsEqualTo(30.dp, 15.dp) + } + + at(32) { + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertExists() + .assertIsNotDisplayed() + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertSizeIsEqualTo(80.dp, 60.dp) + .assertPositionInRootIsEqualTo(20.dp, 10.dp) + } + + at(48) { + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertExists() + .assertIsNotDisplayed() + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertSizeIsEqualTo(70.dp, 50.dp) + .assertPositionInRootIsEqualTo(10.dp, 5.dp) + } + + after { + rule + .onNode(isElement(TestElements.Foo, content = SceneA)) + .assertSizeIsEqualTo(60.dp, 40.dp) + .assertPositionInRootIsEqualTo(0.dp, 0.dp) + rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() + } + } + } + + @Test + fun replaceAnimation() { + rule.testReplaceOverlayTransition( + currentSceneContent = { Box(Modifier.size(width = 180.dp, height = 120.dp)) }, + fromContent = { Foo(width = 60.dp, height = 40.dp) }, + fromAlignment = Alignment.TopStart, + toContent = { Foo(width = 100.dp, height = 80.dp) }, + transition = { + // 4 frames of animation + spec = tween(4 * 16, easing = LinearEasing) + }, + ) { + // Foo moves from (0,0) with a size of 60x40dp to centered (in a 180x120dp Box) with a + // size of 100x80dp, so at (40,20). + before { + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertSizeIsEqualTo(60.dp, 40.dp) + .assertPositionInRootIsEqualTo(0.dp, 0.dp) + rule.onNode(isElement(TestElements.Foo, content = OverlayB)).assertDoesNotExist() + } + + at(16) { + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertExists() + .assertIsNotDisplayed() + rule + .onNode(isElement(TestElements.Foo, content = OverlayB)) + .assertSizeIsEqualTo(70.dp, 50.dp) + .assertPositionInRootIsEqualTo(10.dp, 5.dp) + } + + at(32) { + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertExists() + .assertIsNotDisplayed() + rule + .onNode(isElement(TestElements.Foo, content = OverlayB)) + .assertSizeIsEqualTo(80.dp, 60.dp) + .assertPositionInRootIsEqualTo(20.dp, 10.dp) + } + + at(48) { + rule + .onNode(isElement(TestElements.Foo, content = OverlayA)) + .assertExists() + .assertIsNotDisplayed() + rule + .onNode(isElement(TestElements.Foo, content = OverlayB)) + .assertSizeIsEqualTo(90.dp, 70.dp) + .assertPositionInRootIsEqualTo(30.dp, 15.dp) + } + + after { + rule.onNode(isElement(TestElements.Foo, content = OverlayA)).assertDoesNotExist() + rule + .onNode(isElement(TestElements.Foo, content = OverlayB)) + .assertSizeIsEqualTo(100.dp, 80.dp) + .assertPositionInRootIsEqualTo(40.dp, 20.dp) + } + } + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt index 3422a8e47a3d..69f2cbace276 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt @@ -495,7 +495,7 @@ class SceneTransitionLayoutStateTest { // overscroll for SceneB is defined progress.value = 1.1f val overscrollSpec = assertThat(transition).hasOverscrollSpec() - assertThat(overscrollSpec.scene).isEqualTo(SceneB) + assertThat(overscrollSpec.content).isEqualTo(SceneB) } @Test @@ -516,7 +516,7 @@ class SceneTransitionLayoutStateTest { // overscroll for SceneA is defined progress.value = -0.1f val overscrollSpec = assertThat(transition).hasOverscrollSpec() - assertThat(overscrollSpec.scene).isEqualTo(SceneA) + assertThat(overscrollSpec.content).isEqualTo(SceneA) // scroll from SceneA to SceneB progress.value = 0.5f diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt index e97c27e5a034..b8e13dab913b 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt @@ -500,4 +500,19 @@ class SceneTransitionLayoutTest { assertThat(keyInB).isEqualTo(SceneB) assertThat(keyInC).isEqualTo(SceneC) } + + @Test + fun overlaysMapIsNotAllocatedWhenNoOverlayIsDefined() { + lateinit var layoutImpl: SceneTransitionLayoutImpl + rule.setContent { + SceneTransitionLayoutForTesting( + remember { MutableSceneTransitionLayoutState(SceneA) }, + onLayoutImpl = { layoutImpl = it }, + ) { + scene(SceneA) { Box(Modifier.fillMaxSize()) } + } + } + + assertThat(layoutImpl.overlaysOrNullForTest()).isNull() + } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt index 06799bcda0ef..e48cd81765cb 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt @@ -614,6 +614,7 @@ class SwipeToSceneTest { from(SceneA, to = SceneB) { distance = FixedDistance(swipeDistance) } overscroll(SceneB, Orientation.Vertical) { + progressConverter = ProgressConverter.linear() translate(TestElements.Foo, x = { 20.dp.toPx() }, y = { 30.dp.toPx() }) } } diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt index 00adefb150c1..5cccfb1b319f 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt @@ -26,9 +26,9 @@ import androidx.compose.ui.Modifier @Composable fun TestContentScope( modifier: Modifier = Modifier, + currentScene: SceneKey = remember { SceneKey("current") }, content: @Composable ContentScope.() -> Unit, ) { - val currentScene = remember { SceneKey("current") } val state = remember { MutableSceneTransitionLayoutState(currentScene) } SceneTransitionLayout(state, modifier) { scene(currentScene, content = content) } } diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt index 6d063a0418d6..22450d32ea62 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt @@ -20,11 +20,16 @@ import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.hasTestTag -/** A [SemanticsMatcher] that matches [element], optionally restricted to scene [scene]. */ -fun isElement(element: ElementKey, scene: SceneKey? = null): SemanticsMatcher { - return if (scene == null) { +/** A [SemanticsMatcher] that matches [element], optionally restricted to content [content]. */ +fun isElement(element: ElementKey, content: ContentKey? = null): SemanticsMatcher { + return if (content == null) { hasTestTag(element.testTag) } else { - hasTestTag(element.testTag) and hasAnyAncestor(hasTestTag(scene.testTag)) + hasTestTag(element.testTag) and inContent(content) } } + +/** A [SemanticsMatcher] that matches anything inside [content]. */ +fun inContent(content: ContentKey): SemanticsMatcher { + return hasAnyAncestor(hasTestTag(content.testTag)) +} diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt index 7f26b9855a9a..1ebd3d98471b 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt @@ -16,9 +16,12 @@ package com.android.compose.animation.scene +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.SemanticsNode import androidx.compose.ui.test.SemanticsNodeInteraction @@ -115,6 +118,97 @@ fun ComposeContentTestRule.testTransition( ) } +/** Test the transition when showing [overlay] from [fromScene]. */ +fun ComposeContentTestRule.testShowOverlayTransition( + fromSceneContent: @Composable ContentScope.() -> Unit, + overlayContent: @Composable ContentScope.() -> Unit, + transition: TransitionBuilder.() -> Unit, + fromScene: SceneKey = TestScenes.SceneA, + overlay: OverlayKey = TestOverlays.OverlayA, + builder: TransitionTestBuilder.() -> Unit, +) { + testTransition( + state = + runOnUiThread { + MutableSceneTransitionLayoutState( + fromScene, + transitions = transitions { from(fromScene, overlay, builder = transition) }, + ) + }, + transitionLayout = { state -> + SceneTransitionLayout(state) { + scene(fromScene) { fromSceneContent() } + overlay(overlay) { overlayContent() } + } + }, + changeState = { state -> state.showOverlay(overlay, animationScope = this) }, + builder = builder, + ) +} + +/** Test the transition when hiding [overlay] to [toScene]. */ +fun ComposeContentTestRule.testHideOverlayTransition( + toSceneContent: @Composable ContentScope.() -> Unit, + overlayContent: @Composable ContentScope.() -> Unit, + transition: TransitionBuilder.() -> Unit, + toScene: SceneKey = TestScenes.SceneA, + overlay: OverlayKey = TestOverlays.OverlayA, + builder: TransitionTestBuilder.() -> Unit, +) { + testTransition( + state = + runOnUiThread { + MutableSceneTransitionLayoutState( + toScene, + initialOverlays = setOf(overlay), + transitions = transitions { from(overlay, toScene, builder = transition) }, + ) + }, + transitionLayout = { state -> + SceneTransitionLayout(state) { + scene(toScene) { toSceneContent() } + overlay(overlay) { overlayContent() } + } + }, + changeState = { state -> state.hideOverlay(overlay, animationScope = this) }, + builder = builder, + ) +} + +/** Test the transition when replace [from] to [to]. */ +fun ComposeContentTestRule.testReplaceOverlayTransition( + fromContent: @Composable ContentScope.() -> Unit, + toContent: @Composable ContentScope.() -> Unit, + transition: TransitionBuilder.() -> Unit, + currentSceneContent: @Composable ContentScope.() -> Unit = { Box(Modifier.fillMaxSize()) }, + fromAlignment: Alignment = Alignment.Center, + toAlignment: Alignment = Alignment.Center, + from: OverlayKey = TestOverlays.OverlayA, + to: OverlayKey = TestOverlays.OverlayB, + currentScene: SceneKey = TestScenes.SceneA, + builder: TransitionTestBuilder.() -> Unit, +) { + testTransition( + state = + runOnUiThread { + MutableSceneTransitionLayoutState( + currentScene, + initialOverlays = setOf(from), + transitions = transitions { from(from, to, builder = transition) }, + ) + }, + transitionLayout = { state -> + SceneTransitionLayout(state) { + scene(currentScene) { currentSceneContent() } + overlay(from, fromAlignment) { fromContent() } + overlay(to, toAlignment) { toContent() } + } + }, + changeState = { state -> state.replaceOverlay(from, to, animationScope = this) }, + builder = builder, + ) +} + data class TransitionRecordingSpec( val recordBefore: Boolean = true, val recordAfter: Boolean = true, @@ -188,6 +282,21 @@ fun ComposeContentTestRule.testTransition( "(${currentScene.debugName})" } + testTransition( + state = state, + changeState = { state -> state.setTargetScene(to, coroutineScope = this) }, + transitionLayout = transitionLayout, + builder = builder, + ) +} + +/** Test the transition from [state] to [to]. */ +fun ComposeContentTestRule.testTransition( + state: MutableSceneTransitionLayoutState, + changeState: CoroutineScope.(MutableSceneTransitionLayoutState) -> Unit, + transitionLayout: @Composable (state: MutableSceneTransitionLayoutState) -> Unit, + builder: TransitionTestBuilder.() -> Unit, +) { val test = transitionTest(builder) val assertionScope = object : TransitionTestAssertionScope { @@ -213,7 +322,7 @@ fun ComposeContentTestRule.testTransition( mainClock.autoAdvance = false // Change the current scene. - runOnUiThread { state.setTargetScene(to, coroutineScope) } + runOnUiThread { coroutineScope.changeState(state) } waitForIdle() mainClock.advanceTimeByFrame() waitForIdle() diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestValues.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestValues.kt index b83705aa64fc..f39dd676fb6e 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestValues.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestValues.kt @@ -21,7 +21,7 @@ import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween -/** Scenes keys that can be reused by tests. */ +/** Scene keys that can be reused by tests. */ object TestScenes { val SceneA = SceneKey("SceneA") val SceneB = SceneKey("SceneB") @@ -29,6 +29,12 @@ object TestScenes { val SceneD = SceneKey("SceneD") } +/** Overlay keys that can be reused by tests. */ +object TestOverlays { + val OverlayA = OverlayKey("OverlayA") + val OverlayB = OverlayKey("OverlayB") +} + /** Element keys that can be reused by tests. */ object TestElements { val Foo = ElementKey("Foo") diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/OWNERS b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/OWNERS new file mode 100644 index 000000000000..f6f98e934dde --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/OWNERS @@ -0,0 +1 @@ +include /packages/SystemUI/src/com/android/systemui/keyguard/OWNERS diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityLoggerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/AccessibilityLoggerTest.kt index 1ce21e77f7f3..1ce21e77f7f3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityLoggerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/AccessibilityLoggerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/DisplayIdIndexSupplierTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/DisplayIdIndexSupplierTest.java index cb8cfc2f5dd6..cb8cfc2f5dd6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/DisplayIdIndexSupplierTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/DisplayIdIndexSupplierTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java index c74d340ee325..c74d340ee325 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationGestureDetectorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationGestureDetectorTest.java index 7b06dd65e7b3..7b06dd65e7b3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationGestureDetectorTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationGestureDetectorTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java index 1ceac78af1a2..1ceac78af1a2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java index 3cd3fefb8ef0..3cd3fefb8ef0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java index 8f9b7c8cbc45..8f9b7c8cbc45 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/ModeSwitchesControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/ModeSwitchesControllerTest.java index e1e515eb31f5..e1e515eb31f5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/ModeSwitchesControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/ModeSwitchesControllerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MotionEventHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MotionEventHelper.java index 550e77d63c3b..550e77d63c3b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MotionEventHelper.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MotionEventHelper.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/SystemActionsTest.java index 53b98d52e9d1..53b98d52e9d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/SystemActionsTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/TestableWindowManager.java index 859517839388..859517839388 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/TestableWindowManager.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java index 25696bffdd66..25696bffdd66 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryTest.kt index c4a92bf18283..c4a92bf18283 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogDelegateTest.kt index b80836d80e12..b80836d80e12 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogDelegateTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManagerTest.kt index 1386092ef93e..1386092ef93e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManagerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogReceiverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogReceiverTest.kt index ebe7500300c8..ebe7500300c8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogReceiverTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/extradim/ExtraDimDialogReceiverTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AnnotationLinkSpanTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/AnnotationLinkSpanTest.java index 5b2afe7443dd..5b2afe7443dd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AnnotationLinkSpanTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/AnnotationLinkSpanTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java index d7acaaf796f8..d7acaaf796f8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuEduTooltipViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuEduTooltipViewTest.java index b59773700ed5..b59773700ed5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuEduTooltipViewTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuEduTooltipViewTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java index 24f3a29e64ee..24f3a29e64ee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java index 4373c880d999..4373c880d999 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java index fcdeff9ab683..fcdeff9ab683 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/PositionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/PositionTest.java index 8fb71faf71a9..8fb71faf71a9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/PositionTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/PositionTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesCheckerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesCheckerTest.java index 7320bbff843e..7320bbff843e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesCheckerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesCheckerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java index 09aa286874b9..09aa286874b9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapterTest.java index 9359adf96f80..9359adf96f80 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapterTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsControllerTest.java index 2ac5d105ba99..2ac5d105ba99 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsControllerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParserTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParserTest.java index 17ce1ddee87a..17ce1ddee87a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParserTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParserTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/TestUtils.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java index 8399fa85bfb1..8399fa85bfb1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/TestUtils.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/statusbar/ui/AmbientStatusBarViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/statusbar/ui/AmbientStatusBarViewControllerTest.java index 201ed00acae3..43db5a70849f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/statusbar/ui/AmbientStatusBarViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/statusbar/ui/AmbientStatusBarViewControllerTest.java @@ -148,6 +148,7 @@ public class AmbientStatusBarViewControllerTest extends SysuiTestCase { mKosmos.getWifiInteractor(), mKosmos.getCommunalSceneInteractor(), mLogBuffer); + mController.onInit(); } @Test @@ -517,6 +518,15 @@ public class AmbientStatusBarViewControllerTest extends SysuiTestCase { verify(mDreamOverlayStateController).setDreamOverlayStatusBarVisible(false); } + @Test + public void testStatusBarWindowStateControllerListenerLifecycle() { + ArgumentCaptor<StatusBarWindowStateListener> listenerCaptor = + ArgumentCaptor.forClass(StatusBarWindowStateListener.class); + verify(mStatusBarWindowStateController).addListener(listenerCaptor.capture()); + mController.destroy(); + verify(mStatusBarWindowStateController).removeListener(eq(listenerCaptor.getValue())); + } + private StatusBarWindowStateListener updateStatusBarWindowState(boolean show) { when(mStatusBarWindowStateController.windowIsShowing()).thenReturn(show); final ArgumentCaptor<StatusBarWindowStateListener> diff --git a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/InputSessionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/InputSessionTest.java index 8fca557c7832..8fca557c7832 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/InputSessionTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/InputSessionTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/FontVariationUtilsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt index b0f81c012cca..b0f81c012cca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/FontVariationUtilsTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt index e492c63d095c..e492c63d095c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt index 4809d0e4838f..4809d0e4838f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/back/BackTransformationTest.kt index d898d1cc0f8e..d898d1cc0f8e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/back/BackTransformationTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt index 9548e297e7c5..9548e297e7c5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/assist/ui/DisplayUtilsTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/assist/ui/DisplayUtilsTest.java index 828d36741aeb..828d36741aeb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/assist/ui/DisplayUtilsTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/assist/ui/DisplayUtilsTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt index cbad133ba4f0..cbad133ba4f0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt index 99d36003dfef..99d36003dfef 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java index 7a4bbfe9a580..7a4bbfe9a580 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatterySpecsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/battery/BatterySpecsTest.kt index cac0b664ab79..cac0b664ab79 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatterySpecsTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/battery/BatterySpecsTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java index 197cb843ba5f..197cb843ba5f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt index 9b0e58d63952..9b0e58d63952 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt index baef620ad556..baef620ad556 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt index 13306becf6d2..13306becf6d2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegateTest.kt index 921ff098753e..921ff098753e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegateTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsUtilsTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsUtilsTest.java index d26ccbcd12e0..d26ccbcd12e0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsUtilsTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsUtilsTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt index d2150471744e..d2150471744e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepositoryTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/DisplayStateRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/data/repository/DisplayStateRepositoryTest.kt index d9b71619992f..d9b71619992f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/DisplayStateRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/data/repository/DisplayStateRepositoryTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt index 9c114054bcfb..9c114054bcfb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt index 0209ab803368..0209ab803368 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt index ff5a419faf35..ff5a419faf35 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt index 22971bcf799e..22971bcf799e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt index 5d2d20ce88e9..5d2d20ce88e9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractorImplTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractorImplTest.kt index f40b6b046187..f40b6b046187 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractorImplTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt index 13f2c7212e36..13f2c7212e36 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt index f9bedc93e193..f9bedc93e193 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/udfps/BoundingBoxOverlapDetectorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt index 33ddbf1989b3..33ddbf1989b3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt index 3863b3ccdaee..3863b3ccdaee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/udfps/NormalizedTouchDataTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt index a4653e736745..a4653e736745 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt index 7fa165c19f60..7fa165c19f60 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DefaultUdfpsTouchOverlayViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/DefaultUdfpsTouchOverlayViewModelTest.kt index 0d01472b45c7..0d01472b45c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DefaultUdfpsTouchOverlayViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/DefaultUdfpsTouchOverlayViewModelTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelTest.kt index 77ddd3183b00..77ddd3183b00 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthStateTest.kt index 3eb2ff301212..3eb2ff301212 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthStateTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptHistoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptHistoryImplTest.kt index 81132d72f86c..81132d72f86c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptHistoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptHistoryImplTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt index 0db7b62b8ef1..0db7b62b8ef1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnInteractorTest.kt index ac5ceb8ed266..ac5ceb8ed266 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnInteractorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt index 969e26a8d884..969e26a8d884 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorTest.kt index f06b105a9e26..f06b105a9e26 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt index 5ff46346b386..5ff46346b386 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt index 15a4a40c99ac..65c9b72a3665 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt @@ -39,7 +39,6 @@ import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepositor import com.android.systemui.bouncer.shared.model.BouncerMessageModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.domain.interactor.deviceEntryBiometricsAllowedInteractor -import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor import com.android.systemui.flags.SystemPropertiesHelper import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository @@ -114,8 +113,6 @@ class BouncerMessageInteractorTest : SysuiTestCase() { systemPropertiesHelper = systemPropertiesHelper, primaryBouncerInteractor = kosmos.primaryBouncerInteractor, facePropertyRepository = kosmos.fakeFacePropertyRepository, - deviceEntryFingerprintAuthInteractor = kosmos.deviceEntryFingerprintAuthInteractor, - faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository, securityModel = securityModel, deviceEntryBiometricsAllowedInteractor = kosmos.deviceEntryBiometricsAllowedInteractor, @@ -217,7 +214,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { fun resetMessageBackToDefault_faceAuthRestarts() = testScope.runTest { init() - verify(updateMonitor).registerCallback(keyguardUpdateMonitorCaptor.capture()) + captureKeyguardUpdateMonitorCallback() val bouncerMessage by collectLastValue(underTest.bouncerMessage) underTest.setFaceAcquisitionMessage("not empty") @@ -240,7 +237,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { fun faceRestartDoesNotResetFingerprintMessage() = testScope.runTest { init() - verify(updateMonitor).registerCallback(keyguardUpdateMonitorCaptor.capture()) + captureKeyguardUpdateMonitorCallback() val bouncerMessage by collectLastValue(underTest.bouncerMessage) underTest.setFingerprintAcquisitionMessage("not empty") @@ -337,6 +334,32 @@ class BouncerMessageInteractorTest : SysuiTestCase() { } @Test + fun faceLockoutThenFaceFailure_doesNotUpdateMessage() = + testScope.runTest { + init() + captureKeyguardUpdateMonitorCallback() + val bouncerMessage by collectLastValue(underTest.bouncerMessage) + + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true) + runCurrent() + + assertThat(primaryResMessage(bouncerMessage)) + .isEqualTo("Unlock with PIN or fingerprint") + assertThat(secondaryResMessage(bouncerMessage)) + .isEqualTo("Can’t unlock with face. Too many attempts.") + + // WHEN face failure comes in during lockout + keyguardUpdateMonitorCaptor.value.onBiometricAuthFailed(BiometricSourceType.FACE) + + // THEN lockout message does NOT update to face failure message + assertThat(primaryResMessage(bouncerMessage)) + .isEqualTo("Unlock with PIN or fingerprint") + assertThat(secondaryResMessage(bouncerMessage)) + .isEqualTo("Can’t unlock with face. Too many attempts.") + } + + @Test fun onFaceLockoutStateChange_whenFaceIsNotEnrolled_isANoop() = testScope.runTest { init() @@ -629,6 +652,10 @@ class BouncerMessageInteractorTest : SysuiTestCase() { } } + private fun captureKeyguardUpdateMonitorCallback() { + verify(updateMonitor).registerCallback(keyguardUpdateMonitorCaptor.capture()) + } + companion object { private const val PRIMARY_USER_ID = 0 private val PRIMARY_USER = diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt index 923687b9375d..923687b9375d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/helper/BouncerSceneLayoutTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastSenderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/broadcast/BroadcastSenderTest.kt index 1e9f8558d73c..1e9f8558d73c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastSenderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/broadcast/BroadcastSenderTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/broadcast/PendingRemovalStoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/broadcast/PendingRemovalStoreTest.kt index c693ecc7252f..c693ecc7252f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/broadcast/PendingRemovalStoreTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/broadcast/PendingRemovalStoreTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/camera/CameraIntentsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/camera/CameraIntentsTest.kt index 34940246a7cb..34940246a7cb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/camera/CameraIntentsTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/camera/CameraIntentsTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java index ec8cc4d493d0..ec8cc4d493d0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/ClassifierTest.java index 4da151c3ca04..4da151c3ca04 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/ClassifierTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/DiagonalClassifierTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/DiagonalClassifierTest.java index 8e1be4160498..8e1be4160498 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/DiagonalClassifierTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/DiagonalClassifierTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/DoubleTapClassifierTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/DoubleTapClassifierTest.java index 9289867cbfe2..9289867cbfe2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/DoubleTapClassifierTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/DoubleTapClassifierTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingA11yDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/FalsingA11yDelegateTest.kt index 8e4bec3f2e50..8e4bec3f2e50 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingA11yDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/FalsingA11yDelegateTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java index 5d0bfd7d3d87..5d0bfd7d3d87 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/HistoryTrackerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/HistoryTrackerTest.java index 8e19a1f84d72..8e19a1f84d72 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/HistoryTrackerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/HistoryTrackerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/ProximityClassifierTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/ProximityClassifierTest.java index f965a11c2aa9..f965a11c2aa9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/ProximityClassifierTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/ProximityClassifierTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/SingleTapClassifierTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/SingleTapClassifierTest.java index 65e90888ecb2..65e90888ecb2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/SingleTapClassifierTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/SingleTapClassifierTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/TimeLimitedInputEventBufferTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/TimeLimitedInputEventBufferTest.java index 9a27f386d519..9a27f386d519 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/TimeLimitedInputEventBufferTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/TimeLimitedInputEventBufferTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/TypeClassifierTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/TypeClassifierTest.java index 80c44e2537ec..80c44e2537ec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/TypeClassifierTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/TypeClassifierTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardImageLoaderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ClipboardImageLoaderTest.kt index 791f1f2e1f26..791f1f2e1f26 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardImageLoaderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ClipboardImageLoaderTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtilsTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtilsTest.java index 63e43d777a78..63e43d777a78 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtilsTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtilsTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java index ea6cb3b6d178..ea6cb3b6d178 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/colorextraction/SysuiColorExtractorTests.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/colorextraction/SysuiColorExtractorTests.java index 882bcab5fab6..882bcab5fab6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/colorextraction/SysuiColorExtractorTests.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/colorextraction/SysuiColorExtractorTests.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/coroutine/CoroutineResultTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/coroutine/CoroutineResultTest.kt index de07cda21e75..de07cda21e75 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/common/coroutine/CoroutineResultTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/coroutine/CoroutineResultTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt index 5556b04c2d20..4c908dd895c7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt @@ -140,6 +140,41 @@ class PackageInstallerMonitorTest : SysuiTestCase() { } @Test + fun installSessions_ignoreNullPackageNameSessions() = + testScope.runTest { + val nullPackageSession = + SessionInfo().apply { + sessionId = 1 + appPackageName = null + appIcon = icon1 + } + val wellFormedSession = + SessionInfo().apply { + sessionId = 2 + appPackageName = "pkg_name" + appIcon = icon2 + } + + defaultSessions = listOf(nullPackageSession, wellFormedSession) + + whenever(packageInstaller.allSessions).thenReturn(defaultSessions) + whenever(packageInstaller.getSessionInfo(1)).thenReturn(nullPackageSession) + whenever(packageInstaller.getSessionInfo(2)).thenReturn(wellFormedSession) + + val packageInstallerMonitor = + PackageInstallerMonitor( + handler, + kosmos.applicationCoroutineScope, + logcatLogBuffer("PackageInstallerRepositoryImplTest"), + packageInstaller, + ) + + val sessions by + testScope.collectLastValue(packageInstallerMonitor.installSessionsForPrimaryUser) + assertThat(sessions?.size).isEqualTo(1) + } + + @Test fun installSessions_newSessionsAreAdded() = testScope.runTest { val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser) @@ -177,7 +212,7 @@ class PackageInstallerMonitorTest : SysuiTestCase() { } // Session 1 finished successfully - callback.onFinished(1, /* success = */ true) + callback.onFinished(1, /* success= */ true) runCurrent() // Verify flow updated with session 1 removed diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt index a308c8ee38ca..a308c8ee38ca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractorTest.kt index 72e0726dedb0..72e0726dedb0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt index bb400f274fbe..bb400f274fbe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java index cecb5251b6e2..cecb5251b6e2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt index 983a43561590..edc8c837bf78 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt @@ -69,19 +69,19 @@ class CommunalBackupUtilsTest : SysuiTestCase() { FakeWidgetMetadata( widgetId = 11, componentName = "com.android.fakePackage1/fakeWidget1", - rank = 3, + rank = 0, userSerialNumber = 0, ), FakeWidgetMetadata( widgetId = 12, componentName = "com.android.fakePackage2/fakeWidget2", - rank = 2, + rank = 1, userSerialNumber = 0, ), FakeWidgetMetadata( widgetId = 13, componentName = "com.android.fakePackage3/fakeWidget3", - rank = 1, + rank = 2, userSerialNumber = 10, ), ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt index d6705085eafd..d4d966ad2ef7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt @@ -67,11 +67,11 @@ class CommunalWidgetDaoTest : SysuiTestCase() { @Test fun addWidget_readValueInDb() = testScope.runTest { - val (widgetId, provider, priority, userSerialNumber) = widgetInfo1 + val (widgetId, provider, rank, userSerialNumber) = widgetInfo1 communalWidgetDao.addWidget( widgetId = widgetId, provider = provider, - priority = priority, + rank = rank, userSerialNumber = userSerialNumber, ) val entry = communalWidgetDao.getWidgetByIdNow(id = 1) @@ -81,11 +81,11 @@ class CommunalWidgetDaoTest : SysuiTestCase() { @Test fun deleteWidget_notInDb_returnsFalse() = testScope.runTest { - val (widgetId, provider, priority, userSerialNumber) = widgetInfo1 + val (widgetId, provider, rank, userSerialNumber) = widgetInfo1 communalWidgetDao.addWidget( widgetId = widgetId, provider = provider, - priority = priority, + rank = rank, userSerialNumber = userSerialNumber, ) assertThat(communalWidgetDao.deleteWidgetById(widgetId = 123)).isFalse() @@ -97,11 +97,11 @@ class CommunalWidgetDaoTest : SysuiTestCase() { val widgetsToAdd = listOf(widgetInfo1, widgetInfo2) val widgets = collectLastValue(communalWidgetDao.getWidgets()) widgetsToAdd.forEach { - val (widgetId, provider, priority, userSerialNumber) = it + val (widgetId, provider, rank, userSerialNumber) = it communalWidgetDao.addWidget( widgetId = widgetId, provider = provider, - priority = priority, + rank = rank, userSerialNumber = userSerialNumber ) } @@ -115,17 +115,48 @@ class CommunalWidgetDaoTest : SysuiTestCase() { } @Test + fun addWidget_rankNotSpecified_widgetAddedAtTheEnd(): Unit = + testScope.runTest { + val widgets by collectLastValue(communalWidgetDao.getWidgets()) + + // Verify database is empty + assertThat(widgets).isEmpty() + + // Add widgets one by one without specifying rank + val widgetsToAdd = listOf(widgetInfo1, widgetInfo2, widgetInfo3) + widgetsToAdd.forEach { + val (widgetId, provider, _, userSerialNumber) = it + communalWidgetDao.addWidget( + widgetId = widgetId, + provider = provider, + userSerialNumber = userSerialNumber + ) + } + + // Verify new each widget is added at the end + assertThat(widgets) + .containsExactly( + communalItemRankEntry1, + communalWidgetItemEntry1, + communalItemRankEntry2, + communalWidgetItemEntry2, + communalItemRankEntry3, + communalWidgetItemEntry3, + ) + } + + @Test fun deleteWidget_emitsActiveWidgetsInDb() = testScope.runTest { val widgetsToAdd = listOf(widgetInfo1, widgetInfo2) val widgets = collectLastValue(communalWidgetDao.getWidgets()) widgetsToAdd.forEach { - val (widgetId, provider, priority, userSerialNumber) = it + val (widgetId, provider, rank, userSerialNumber) = it communalWidgetDao.addWidget( widgetId = widgetId, provider = provider, - priority = priority, + rank = rank, userSerialNumber = userSerialNumber, ) } @@ -148,32 +179,32 @@ class CommunalWidgetDaoTest : SysuiTestCase() { val widgets = collectLastValue(communalWidgetDao.getWidgets()) widgetsToAdd.forEach { - val (widgetId, provider, priority, userSerialNumber) = it + val (widgetId, provider, rank, userSerialNumber) = it communalWidgetDao.addWidget( widgetId = widgetId, provider = provider, - priority = priority, + rank = rank, userSerialNumber = userSerialNumber, ) } assertThat(widgets()) .containsExactly( - communalItemRankEntry2, - communalWidgetItemEntry2, communalItemRankEntry1, communalWidgetItemEntry1, + communalItemRankEntry2, + communalWidgetItemEntry2, ) .inOrder() - // swapped priorities - val widgetIdsToPriorityMap = mapOf(widgetInfo1.widgetId to 2, widgetInfo2.widgetId to 1) - communalWidgetDao.updateWidgetOrder(widgetIdsToPriorityMap) + // swapped ranks + val widgetIdsToRankMap = mapOf(widgetInfo1.widgetId to 1, widgetInfo2.widgetId to 0) + communalWidgetDao.updateWidgetOrder(widgetIdsToRankMap) assertThat(widgets()) .containsExactly( - communalItemRankEntry1.copy(rank = 2), + communalItemRankEntry2.copy(rank = 0), + communalWidgetItemEntry2, + communalItemRankEntry1.copy(rank = 1), communalWidgetItemEntry1, - communalItemRankEntry2.copy(rank = 1), - communalWidgetItemEntry2 ) .inOrder() } @@ -181,53 +212,56 @@ class CommunalWidgetDaoTest : SysuiTestCase() { @Test fun addNewWidgetWithReorder_emitsWidgetsInNewOrder() = testScope.runTest { - val existingWidgets = listOf(widgetInfo1, widgetInfo2) + val existingWidgets = listOf(widgetInfo1, widgetInfo2, widgetInfo3) val widgets = collectLastValue(communalWidgetDao.getWidgets()) existingWidgets.forEach { - val (widgetId, provider, priority, userSerialNumber) = it + val (widgetId, provider, rank, userSerialNumber) = it communalWidgetDao.addWidget( widgetId = widgetId, provider = provider, - priority = priority, + rank = rank, userSerialNumber = userSerialNumber, ) } assertThat(widgets()) .containsExactly( - communalItemRankEntry2, - communalWidgetItemEntry2, communalItemRankEntry1, communalWidgetItemEntry1, - ) - .inOrder() - - // map with no item in the middle at index 1 - val widgetIdsToIndexMap = mapOf(widgetInfo1.widgetId to 1, widgetInfo2.widgetId to 3) - communalWidgetDao.updateWidgetOrder(widgetIdsToIndexMap) - assertThat(widgets()) - .containsExactly( - communalItemRankEntry2.copy(rank = 3), + communalItemRankEntry2, communalWidgetItemEntry2, - communalItemRankEntry1.copy(rank = 1), - communalWidgetItemEntry1, + communalItemRankEntry3, + communalWidgetItemEntry3, ) .inOrder() - // add the new middle item that we left space for. + + // add a new widget at rank 1. communalWidgetDao.addWidget( - widgetId = widgetInfo3.widgetId, - provider = widgetInfo3.provider, - priority = 2, - userSerialNumber = widgetInfo3.userSerialNumber, + widgetId = 4, + provider = ComponentName("pk_name", "cls_name_4"), + rank = 1, + userSerialNumber = 0, ) + + val newRankEntry = CommunalItemRank(uid = 4L, rank = 1) + val newWidgetEntry = + CommunalWidgetItem( + uid = 4L, + widgetId = 4, + componentName = "pk_name/cls_name_4", + itemId = 4L, + userSerialNumber = 0, + ) assertThat(widgets()) .containsExactly( - communalItemRankEntry2.copy(rank = 3), + communalItemRankEntry1.copy(rank = 0), + communalWidgetItemEntry1, + newRankEntry, + newWidgetEntry, + communalItemRankEntry2.copy(rank = 2), communalWidgetItemEntry2, - communalItemRankEntry3.copy(rank = 2), + communalItemRankEntry3.copy(rank = 3), communalWidgetItemEntry3, - communalItemRankEntry1.copy(rank = 1), - communalWidgetItemEntry1, ) .inOrder() } @@ -261,11 +295,11 @@ class CommunalWidgetDaoTest : SysuiTestCase() { assertThat(widgets).containsExactlyEntriesIn(expected) } - private fun addWidget(metadata: FakeWidgetMetadata, priority: Int? = null) { + private fun addWidget(metadata: FakeWidgetMetadata, rank: Int? = null) { communalWidgetDao.addWidget( widgetId = metadata.widgetId, provider = metadata.provider, - priority = priority ?: metadata.priority, + rank = rank ?: metadata.rank, userSerialNumber = metadata.userSerialNumber, ) } @@ -273,7 +307,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() { data class FakeWidgetMetadata( val widgetId: Int, val provider: ComponentName, - val priority: Int, + val rank: Int, val userSerialNumber: Int, ) @@ -282,26 +316,26 @@ class CommunalWidgetDaoTest : SysuiTestCase() { FakeWidgetMetadata( widgetId = 1, provider = ComponentName("pk_name", "cls_name_1"), - priority = 1, + rank = 0, userSerialNumber = 0, ) val widgetInfo2 = FakeWidgetMetadata( widgetId = 2, provider = ComponentName("pk_name", "cls_name_2"), - priority = 2, + rank = 1, userSerialNumber = 0, ) val widgetInfo3 = FakeWidgetMetadata( widgetId = 3, provider = ComponentName("pk_name", "cls_name_3"), - priority = 3, + rank = 2, userSerialNumber = 10, ) - val communalItemRankEntry1 = CommunalItemRank(uid = 1L, rank = widgetInfo1.priority) - val communalItemRankEntry2 = CommunalItemRank(uid = 2L, rank = widgetInfo2.priority) - val communalItemRankEntry3 = CommunalItemRank(uid = 3L, rank = widgetInfo3.priority) + val communalItemRankEntry1 = CommunalItemRank(uid = 1L, rank = widgetInfo1.rank) + val communalItemRankEntry2 = CommunalItemRank(uid = 2L, rank = widgetInfo2.rank) + val communalItemRankEntry3 = CommunalItemRank(uid = 3L, rank = widgetInfo3.rank) val communalWidgetItemEntry1 = CommunalWidgetItem( uid = 1L, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt index ad2c42fd1f09..eba395bdb5a3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt @@ -115,21 +115,21 @@ class DefaultWidgetPopulationTest : SysuiTestCase() { .addWidget( widgetId = 0, componentName = defaultWidgets[0], - priority = 3, + rank = 0, userSerialNumber = 0, ) verify(communalWidgetDao) .addWidget( widgetId = 1, componentName = defaultWidgets[1], - priority = 2, + rank = 1, userSerialNumber = 0, ) verify(communalWidgetDao) .addWidget( widgetId = 2, componentName = defaultWidgets[2], - priority = 1, + rank = 2, userSerialNumber = 0, ) } @@ -150,7 +150,7 @@ class DefaultWidgetPopulationTest : SysuiTestCase() { .addWidget( widgetId = anyInt(), componentName = any(), - priority = anyInt(), + rank = anyInt(), userSerialNumber = anyInt(), ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt index ca81838bcdaa..980a5ec8c494 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt @@ -154,7 +154,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { CommunalWidgetContentModel.Available( appWidgetId = communalWidgetItemEntry.widgetId, providerInfo = providerInfoA, - priority = communalItemRankEntry.rank, + rank = communalItemRankEntry.rank, ) ) @@ -190,12 +190,12 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { CommunalWidgetContentModel.Available( appWidgetId = 1, providerInfo = providerInfoA, - priority = 1, + rank = 1, ), CommunalWidgetContentModel.Available( appWidgetId = 2, providerInfo = providerInfoB, - priority = 2, + rank = 2, ), ) } @@ -225,12 +225,12 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { CommunalWidgetContentModel.Available( appWidgetId = 1, providerInfo = providerInfoA, - priority = 1, + rank = 1, ), CommunalWidgetContentModel.Available( appWidgetId = 2, providerInfo = providerInfoB, - priority = 2, + rank = 2, ), ) @@ -248,12 +248,12 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { appWidgetId = 1, // Verify that provider info updated providerInfo = providerInfoC, - priority = 1, + rank = 1, ), CommunalWidgetContentModel.Available( appWidgetId = 2, providerInfo = providerInfoB, - priority = 2, + rank = 2, ), ) } @@ -263,7 +263,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { testScope.runTest { val provider = ComponentName("pkg_name", "cls_name") val id = 1 - val priority = 1 + val rank = 1 whenever(communalWidgetHost.getAppWidgetInfo(id)) .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION) whenever( @@ -273,12 +273,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { ) ) .thenReturn(id) - underTest.addWidget(provider, mainUser, priority, kosmos.widgetConfiguratorSuccess) + underTest.addWidget(provider, mainUser, rank, kosmos.widgetConfiguratorSuccess) runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser) - verify(communalWidgetDao) - .addWidget(id, provider, priority, testUserSerialNumber(mainUser)) + verify(communalWidgetDao).addWidget(id, provider, rank, testUserSerialNumber(mainUser)) // Verify backup requested verify(backupManager).dataChanged() @@ -289,7 +288,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { testScope.runTest { val provider = ComponentName("pkg_name", "cls_name") val id = 1 - val priority = 1 + val rank = 1 whenever(communalWidgetHost.getAppWidgetInfo(id)) .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION) whenever( @@ -299,7 +298,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { ) ) .thenReturn(id) - underTest.addWidget(provider, mainUser, priority, kosmos.widgetConfiguratorFail) + underTest.addWidget(provider, mainUser, rank, kosmos.widgetConfiguratorFail) runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser) @@ -316,7 +315,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { testScope.runTest { val provider = ComponentName("pkg_name", "cls_name") val id = 1 - val priority = 1 + val rank = 1 whenever(communalWidgetHost.getAppWidgetInfo(id)) .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION) whenever( @@ -326,7 +325,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { ) ) .thenReturn(id) - underTest.addWidget(provider, mainUser, priority) { + underTest.addWidget(provider, mainUser, rank) { throw IllegalStateException("some error") } runCurrent() @@ -345,7 +344,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { testScope.runTest { val provider = ComponentName("pkg_name", "cls_name") val id = 1 - val priority = 1 + val rank = 1 whenever(communalWidgetHost.getAppWidgetInfo(id)) .thenReturn(PROVIDER_INFO_CONFIGURATION_OPTIONAL) whenever( @@ -355,12 +354,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { ) ) .thenReturn(id) - underTest.addWidget(provider, mainUser, priority, kosmos.widgetConfiguratorFail) + underTest.addWidget(provider, mainUser, rank, kosmos.widgetConfiguratorFail) runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser) - verify(communalWidgetDao) - .addWidget(id, provider, priority, testUserSerialNumber(mainUser)) + verify(communalWidgetDao).addWidget(id, provider, rank, testUserSerialNumber(mainUser)) // Verify backup requested verify(backupManager).dataChanged() @@ -399,11 +397,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun reorderWidgets_queryDb() = testScope.runTest { - val widgetIdToPriorityMap = mapOf(104 to 1, 103 to 2, 101 to 3) - underTest.updateWidgetOrder(widgetIdToPriorityMap) + val widgetIdToRankMap = mapOf(104 to 1, 103 to 2, 101 to 3) + underTest.updateWidgetOrder(widgetIdToRankMap) runCurrent() - verify(communalWidgetDao).updateWidgetOrder(widgetIdToPriorityMap) + verify(communalWidgetDao).updateWidgetOrder(widgetIdToRankMap) // Verify backup requested verify(backupManager).dataChanged() @@ -691,11 +689,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { CommunalWidgetContentModel.Available( appWidgetId = 1, providerInfo = providerInfoA, - priority = 1, + rank = 1, ), CommunalWidgetContentModel.Pending( appWidgetId = 2, - priority = 2, + rank = 2, componentName = ComponentName("pk_2", "cls_2"), icon = fakeIcon, user = mainUser, @@ -730,7 +728,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { .containsExactly( CommunalWidgetContentModel.Pending( appWidgetId = 1, - priority = 1, + rank = 1, componentName = ComponentName("pk_1", "cls_1"), icon = fakeIcon, user = mainUser, @@ -750,7 +748,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { CommunalWidgetContentModel.Available( appWidgetId = 1, providerInfo = providerInfoA, - priority = 1, + rank = 1, ), ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 864795b062be..1d03ced19c72 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -791,14 +791,6 @@ class CommunalInteractorTest : SysuiTestCase() { } @Test - fun showWidgetEditor_withPreselectedKey_startsActivity() = - testScope.runTest { - val widgetKey = CommunalContentModel.KEY.widget(123) - underTest.showWidgetEditor(preselectedKey = widgetKey) - verify(editWidgetsActivityStarter).startActivity(widgetKey) - } - - @Test fun showWidgetEditor_openWidgetPickerOnStart_startsActivity() = testScope.runTest { underTest.showWidgetEditor(shouldOpenWidgetPickerOnStart = true) @@ -1082,6 +1074,16 @@ class CommunalInteractorTest : SysuiTestCase() { assertThat(disclaimerDismissed).isFalse() } + @Test + fun settingSelectedKey_flowUpdated() { + testScope.runTest { + val key = "test" + val selectedKey by collectLastValue(underTest.selectedKey) + underTest.setSelectedKey(key) + assertThat(selectedKey).isEqualTo(key) + } + } + private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) { whenever(kosmos.devicePolicyManager.getKeyguardDisabledFeatures(nullable(), eq(user.id))) .thenReturn(disabledFlags) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt index ed7e9107240e..dfb75cae6ecd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt @@ -22,6 +22,7 @@ import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.communal.data.repository.communalSceneRepository +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor.OnSceneAboutToChangeListener import com.android.systemui.communal.domain.model.CommunalTransitionProgressModel import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.EditModeState @@ -36,6 +37,11 @@ import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) @@ -58,6 +64,36 @@ class CommunalSceneInteractorTest : SysuiTestCase() { } @Test + fun changeScene_callsSceneStateProcessor() = + testScope.runTest { + val callback: OnSceneAboutToChangeListener = mock() + underTest.registerSceneStateProcessor(callback) + + val currentScene by collectLastValue(underTest.currentScene) + assertThat(currentScene).isEqualTo(CommunalScenes.Blank) + verify(callback, never()).onSceneAboutToChange(any(), anyOrNull()) + + underTest.changeScene(CommunalScenes.Communal, "test") + assertThat(currentScene).isEqualTo(CommunalScenes.Communal) + verify(callback).onSceneAboutToChange(CommunalScenes.Communal, null) + } + + @Test + fun changeScene_doesNotCallSceneStateProcessorForDuplicateState() = + testScope.runTest { + val callback: OnSceneAboutToChangeListener = mock() + underTest.registerSceneStateProcessor(callback) + + val currentScene by collectLastValue(underTest.currentScene) + assertThat(currentScene).isEqualTo(CommunalScenes.Blank) + + underTest.changeScene(CommunalScenes.Blank, "test") + assertThat(currentScene).isEqualTo(CommunalScenes.Blank) + + verify(callback, never()).onSceneAboutToChange(any(), anyOrNull()) + } + + @Test fun snapToScene() = testScope.runTest { val currentScene by collectLastValue(underTest.currentScene) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt index 7e28e19d0ee0..0bfcd242828d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt @@ -36,11 +36,15 @@ import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.repository.fakeUserRepository import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class CommunalTutorialInteractorTest : SysuiTestCase() { @@ -50,14 +54,14 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { private lateinit var underTest: CommunalTutorialInteractor private lateinit var keyguardRepository: FakeKeyguardRepository private lateinit var communalTutorialRepository: FakeCommunalTutorialRepository - private lateinit var communalInteractor: CommunalInteractor + private lateinit var communalSceneInteractor: CommunalSceneInteractor private lateinit var userRepository: FakeUserRepository @Before fun setUp() { keyguardRepository = kosmos.fakeKeyguardRepository communalTutorialRepository = kosmos.fakeCommunalTutorialRepository - communalInteractor = kosmos.communalInteractor + communalSceneInteractor = kosmos.communalSceneInteractor userRepository = kosmos.fakeUserRepository kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) @@ -158,7 +162,7 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { kosmos.setCommunalAvailable(true) communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED) - communalInteractor.changeScene(CommunalScenes.Blank, "test") + communalSceneInteractor.changeScene(CommunalScenes.Blank, "test") assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_NOT_STARTED) } @@ -171,7 +175,7 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { goToCommunal() communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED) - communalInteractor.changeScene(CommunalScenes.Blank, "test") + communalSceneInteractor.changeScene(CommunalScenes.Blank, "test") assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_COMPLETED) } @@ -184,13 +188,14 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { goToCommunal() communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) - communalInteractor.changeScene(CommunalScenes.Blank, "test") + communalSceneInteractor.changeScene(CommunalScenes.Blank, "test") assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_COMPLETED) } - private suspend fun goToCommunal() { + private suspend fun TestScope.goToCommunal() { kosmos.setCommunalAvailable(true) - communalInteractor.changeScene(CommunalScenes.Communal, "test") + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + runCurrent() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index 57ce9de2d057..82181788e1be 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -150,8 +150,8 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) // Widgets available. - widgetRepository.addWidget(appWidgetId = 0, priority = 30) - widgetRepository.addWidget(appWidgetId = 1, priority = 20) + widgetRepository.addWidget(appWidgetId = 0, rank = 30) + widgetRepository.addWidget(appWidgetId = 1, rank = 20) // Smartspace available. smartspaceRepository.setTimers( @@ -212,8 +212,8 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) // Widgets available. - widgetRepository.addWidget(appWidgetId = 0, priority = 30) - widgetRepository.addWidget(appWidgetId = 1, priority = 20) + widgetRepository.addWidget(appWidgetId = 0, rank = 30) + widgetRepository.addWidget(appWidgetId = 1, rank = 20) val communalContent by collectLastValue(underTest.communalContent) @@ -227,7 +227,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { underTest.onDeleteWidget( id = 0, componentName = ComponentName("test_package", "test_class"), - priority = 30, + rank = 30, ) // Only one widget and CTA tile remain. diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index cc945d63e15f..f6f5bc038209 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -101,7 +101,6 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.spy -import org.mockito.kotlin.times import org.mockito.kotlin.whenever import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters @@ -160,24 +159,27 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { communalInteractor = spy(kosmos.communalInteractor) - underTest = - CommunalViewModel( - kosmos.testDispatcher, - testScope, - kosmos.testScope.backgroundScope, - context.resources, - kosmos.keyguardTransitionInteractor, - kosmos.keyguardInteractor, - mock<KeyguardIndicationController>(), - kosmos.communalSceneInteractor, - communalInteractor, - kosmos.communalSettingsInteractor, - kosmos.communalTutorialInteractor, - kosmos.shadeInteractor, - mediaHost, - logcatLogBuffer("CommunalViewModelTest"), - metricsLogger, - ) + underTest = createViewModel() + } + + private fun createViewModel(): CommunalViewModel { + return CommunalViewModel( + kosmos.testDispatcher, + testScope, + kosmos.testScope.backgroundScope, + context.resources, + kosmos.keyguardTransitionInteractor, + kosmos.keyguardInteractor, + mock<KeyguardIndicationController>(), + kosmos.communalSceneInteractor, + communalInteractor, + kosmos.communalSettingsInteractor, + kosmos.communalTutorialInteractor, + kosmos.shadeInteractor, + mediaHost, + logcatLogBuffer("CommunalViewModelTest"), + metricsLogger, + ) } @Test @@ -213,8 +215,8 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) // Widgets available. - widgetRepository.addWidget(appWidgetId = 0, priority = 30) - widgetRepository.addWidget(appWidgetId = 1, priority = 20) + widgetRepository.addWidget(appWidgetId = 0, rank = 30) + widgetRepository.addWidget(appWidgetId = 1, rank = 20) // Smartspace available. smartspaceRepository.setTimers( @@ -303,7 +305,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { testScope.runTest { tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) - widgetRepository.addWidget(appWidgetId = 1, priority = 1) + widgetRepository.addWidget(appWidgetId = 1, rank = 1) mediaRepository.mediaInactive() smartspaceRepository.setTimers(emptyList()) @@ -660,8 +662,8 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { ) // Widgets available - widgetRepository.addWidget(appWidgetId = 0, priority = 30) - widgetRepository.addWidget(appWidgetId = 1, priority = 20) + widgetRepository.addWidget(appWidgetId = 0, rank = 30) + widgetRepository.addWidget(appWidgetId = 1, rank = 20) // Then hub shows widgets and the CTA tile assertThat(communalContent).hasSize(3) @@ -716,8 +718,8 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { ) // And widgets available - widgetRepository.addWidget(appWidgetId = 0, priority = 30) - widgetRepository.addWidget(appWidgetId = 1, priority = 20) + widgetRepository.addWidget(appWidgetId = 0, rank = 30) + widgetRepository.addWidget(appWidgetId = 1, rank = 20) // Then emits widgets and the CTA tile assertThat(communalContent).hasSize(3) @@ -770,7 +772,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun onTapWidget_logEvent() { - underTest.onTapWidget(ComponentName("test_pkg", "test_cls"), priority = 10) + underTest.onTapWidget(ComponentName("test_pkg", "test_cls"), rank = 10) verify(metricsLogger).logTapWidget("test_pkg/test_cls", rank = 10) } @@ -785,6 +787,21 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(touchAvailable).isTrue() } + @Test + fun selectedKey_changeAffectsAllInstances() = + testScope.runTest { + val model1 = createViewModel() + val selectedKey1 by collectLastValue(model1.selectedKey) + val model2 = createViewModel() + val selectedKey2 by collectLastValue(model2.selectedKey) + + val key = "test" + model1.setSelectedKey(key) + + assertThat(selectedKey1).isEqualTo(key) + assertThat(selectedKey2).isEqualTo(key) + } + private suspend fun setIsMainUser(isMainUser: Boolean) { val user = if (isMainUser) MAIN_USER_INFO else SECONDARY_USER_INFO with(userRepository) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/EditWidgetsActivityControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/EditWidgetsActivityControllerTest.kt new file mode 100644 index 000000000000..3ba86254d2f4 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/EditWidgetsActivityControllerTest.kt @@ -0,0 +1,143 @@ +/* + * 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.communal.widgets + +import android.app.Activity +import android.app.Application.ActivityLifecycleCallbacks +import android.os.Bundle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +class EditWidgetsActivityControllerTest : SysuiTestCase() { + @Test + fun activityLifecycle_finishedWhenNotWaitingForResult() { + val activity = mock<Activity>() + val controller = EditWidgetsActivity.ActivityControllerImpl(activity) + + val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() + verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) + + controller.setActivityFullyVisible(true) + callbackCapture.lastValue.onActivityStopped(activity) + + verify(activity).finish() + } + + @Test + fun activityLifecycle_notFinishedWhenOnStartCalledAfterOnStop() { + val activity = mock<Activity>() + + val controller = EditWidgetsActivity.ActivityControllerImpl(activity) + + val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() + verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) + + controller.setActivityFullyVisible(false) + callbackCapture.lastValue.onActivityStopped(activity) + callbackCapture.lastValue.onActivityStarted(activity) + + verify(activity, never()).finish() + } + + @Test + fun activityLifecycle_notFinishedDuringConfigurationChange() { + val activity = mock<Activity>() + + val controller = EditWidgetsActivity.ActivityControllerImpl(activity) + + val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() + verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) + + controller.setActivityFullyVisible(true) + whenever(activity.isChangingConfigurations).thenReturn(true) + callbackCapture.lastValue.onActivityStopped(activity) + callbackCapture.lastValue.onActivityStarted(activity) + + verify(activity, never()).finish() + } + + @Test + fun activityLifecycle_notFinishedWhenWaitingForResult() { + val activity = mock<Activity>() + val controller = EditWidgetsActivity.ActivityControllerImpl(activity) + + val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() + verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) + + controller.onWaitingForResult(true) + callbackCapture.lastValue.onActivityStopped(activity) + + verify(activity, never()).finish() + } + + @Test + fun activityLifecycle_finishedAfterResultReturned() { + val activity = mock<Activity>() + val controller = EditWidgetsActivity.ActivityControllerImpl(activity) + + val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() + verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) + + controller.onWaitingForResult(true) + controller.onWaitingForResult(false) + controller.setActivityFullyVisible(true) + callbackCapture.lastValue.onActivityStopped(activity) + + verify(activity).finish() + } + + @Test + fun activityLifecycle_statePreservedThroughInstanceSave() { + val activity = mock<Activity>() + val bundle = Bundle(1) + + run { + val controller = EditWidgetsActivity.ActivityControllerImpl(activity) + val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() + verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) + + controller.onWaitingForResult(true) + callbackCapture.lastValue.onActivitySaveInstanceState(activity, bundle) + } + + clearInvocations(activity) + + run { + val controller = EditWidgetsActivity.ActivityControllerImpl(activity) + val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() + verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) + + callbackCapture.lastValue.onActivityCreated(activity, bundle) + callbackCapture.lastValue.onActivityStopped(activity) + + verify(activity, never()).finish() + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarterTest.kt index 5b629b91eb45..48b42d551d60 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarterTest.kt @@ -21,7 +21,6 @@ import android.content.Intent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.communal.widgets.EditWidgetsActivity.Companion.EXTRA_PRESELECTED_KEY import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.ActivityStarter import com.android.systemui.testKosmos @@ -62,7 +61,7 @@ class EditWidgetsActivityStarterTest : SysuiTestCase() { fun activityLaunch_intentIsWellFormed() { with(kosmos) { testScope.runTest { - underTest.startActivity(TEST_PRESELECTED_KEY, shouldOpenWidgetPickerOnStart = true) + underTest.startActivity(shouldOpenWidgetPickerOnStart = true) val captor = argumentCaptor<Intent>() verify(activityStarter) @@ -71,8 +70,6 @@ class EditWidgetsActivityStarterTest : SysuiTestCase() { assertThat(captor.lastValue.flags and Intent.FLAG_ACTIVITY_NEW_TASK).isNotEqualTo(0) assertThat(captor.lastValue.flags and Intent.FLAG_ACTIVITY_CLEAR_TASK) .isNotEqualTo(0) - assertThat(captor.lastValue.extras?.getString(EXTRA_PRESELECTED_KEY)) - .isEqualTo(TEST_PRESELECTED_KEY) assertThat( captor.lastValue.extras?.getBoolean( EditWidgetsActivity.EXTRA_OPEN_WIDGET_PICKER_ON_START @@ -80,7 +77,7 @@ class EditWidgetsActivityStarterTest : SysuiTestCase() { ) .isEqualTo(true) - underTest.startActivity(TEST_PRESELECTED_KEY, shouldOpenWidgetPickerOnStart = false) + underTest.startActivity(shouldOpenWidgetPickerOnStart = false) verify(activityStarter, times(2)) .startActivityDismissingKeyguard(captor.capture(), eq(true), eq(true), any()) @@ -88,8 +85,6 @@ class EditWidgetsActivityStarterTest : SysuiTestCase() { assertThat(captor.lastValue.flags and Intent.FLAG_ACTIVITY_NEW_TASK).isNotEqualTo(0) assertThat(captor.lastValue.flags and Intent.FLAG_ACTIVITY_CLEAR_TASK) .isNotEqualTo(0) - assertThat(captor.lastValue.extras?.getString(EXTRA_PRESELECTED_KEY)) - .isEqualTo(TEST_PRESELECTED_KEY) assertThat( captor.lastValue.extras?.getBoolean( EditWidgetsActivity.EXTRA_OPEN_WIDGET_PICKER_ON_START @@ -99,8 +94,4 @@ class EditWidgetsActivityStarterTest : SysuiTestCase() { } } } - - companion object { - const val TEST_PRESELECTED_KEY = "test-key" - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java index ed214749d6a7..ed214749d6a7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationHostViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/ComplicationHostViewControllerTest.java index dd3f991e60b7..dd3f991e60b7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationHostViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/ComplicationHostViewControllerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationLayoutEngineTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/ComplicationLayoutEngineTest.java index 383e0fab73ff..383e0fab73ff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationLayoutEngineTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/ComplicationLayoutEngineTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationLayoutParamsTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/ComplicationLayoutParamsTest.java index 12cb8a61e0d8..12cb8a61e0d8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationLayoutParamsTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/ComplicationLayoutParamsTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationTypesUpdaterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/ComplicationTypesUpdaterTest.java index d728517e2000..d728517e2000 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationTypesUpdaterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/ComplicationTypesUpdaterTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationUtilsTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/ComplicationUtilsTest.java index 1e802337a9e7..1e802337a9e7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationUtilsTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/ComplicationUtilsTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java index 98b119ae75c4..98b119ae75c4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/DreamClockTimeComplicationTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/DreamClockTimeComplicationTest.java index 22ab4994f026..22ab4994f026 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/complication/DreamClockTimeComplicationTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/DreamClockTimeComplicationTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/DreamHomeControlsComplicationTest.java index c2173c43ad45..c2173c43ad45 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/complication/DreamHomeControlsComplicationTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/DreamHomeControlsComplicationTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/DreamMediaEntryComplicationTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/DreamMediaEntryComplicationTest.java index 3a856a05ac02..3a856a05ac02 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/complication/DreamMediaEntryComplicationTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/DreamMediaEntryComplicationTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/SmartSpaceComplicationTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/SmartSpaceComplicationTest.java index 6c354ef0966c..6c354ef0966c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/complication/SmartSpaceComplicationTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/SmartSpaceComplicationTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/CustomIconCacheTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/CustomIconCacheTest.kt index 28e0cffc4f78..28e0cffc4f78 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/CustomIconCacheTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/CustomIconCacheTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt index 928514657257..928514657257 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapperTest.kt index afa5ceccb6cb..afa5ceccb6cb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapperTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt index f9c2c6b791f1..f9c2c6b791f1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsTileResourceConfigurationImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/controller/ControlsTileResourceConfigurationImplTest.kt index e04ce45592b1..e04ce45592b1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsTileResourceConfigurationImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/controller/ControlsTileResourceConfigurationImplTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/PackageUpdateMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/controller/PackageUpdateMonitorTest.kt index 282ea5ccb5c2..282ea5ccb5c2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/PackageUpdateMonitorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/controller/PackageUpdateMonitorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ServiceWrapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/controller/ServiceWrapperTest.kt index b5c6c538ec9e..b5c6c538ec9e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ServiceWrapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/controller/ServiceWrapperTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/StatefulControlSubscriberTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/controller/StatefulControlSubscriberTest.kt index 7d197f75b5f9..7d197f75b5f9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/StatefulControlSubscriberTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/controller/StatefulControlSubscriberTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt index 844cc1f1f8fa..844cc1f1f8fa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/dagger/ControlsComponentTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/AllModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/management/AllModelTest.kt index 5528f6523111..5528f6523111 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/AllModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/management/AllModelTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/management/AppAdapterTest.kt index 56c7c854b69d..56c7c854b69d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/AppAdapterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/management/AppAdapterTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt index f1782e8b0569..f1782e8b0569 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt index c49867a30dc9..c49867a30dc9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/FavoritesModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/management/FavoritesModelTest.kt index 281addc053f9..281addc053f9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/FavoritesModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/management/FavoritesModelTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt index d8aac101e84f..d8aac101e84f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/management/PanelConfirmationDialogFactoryTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/StartActivityData.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/management/StartActivityData.kt index 977e3ba899f6..977e3ba899f6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/StartActivityData.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/management/StartActivityData.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/TestControlsRequestDialog.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/management/TestControlsRequestDialog.kt index ec239f64e254..ec239f64e254 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/TestControlsRequestDialog.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/management/TestControlsRequestDialog.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt index fd4c6810a7fc..fd4c6810a7fc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt index 86e3481ff263..86e3481ff263 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImplTest.kt index 3bdd5cf8cfe7..3bdd5cf8cfe7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImplTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/CanUseIconPredicateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/CanUseIconPredicateTest.kt index 193ce21dcfa0..193ce21dcfa0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/CanUseIconPredicateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/CanUseIconPredicateTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt index ca33f16b10ac..ca33f16b10ac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/ControlsDialogsFactoryTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/OverflowMenuAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/OverflowMenuAdapterTest.kt index 6092b8c5eb65..6092b8c5eb65 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/OverflowMenuAdapterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/OverflowMenuAdapterTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt index de2d8529adff..de2d8529adff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/TemperatureControlBehaviorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/TemperatureControlBehaviorTest.kt index b3f458821cba..b3f458821cba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/TemperatureControlBehaviorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/TemperatureControlBehaviorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/TestableControlsActivity.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/TestableControlsActivity.kt index d2fe68ad8e1a..d2fe68ad8e1a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/TestableControlsActivity.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/TestableControlsActivity.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ToggleRangeTemplateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/ToggleRangeTemplateTest.kt index 9f4836ad1d9b..9f4836ad1d9b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ToggleRangeTemplateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/ToggleRangeTemplateTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/coroutines/FlowTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/coroutines/FlowTest.kt index 23da3f1d3ac0..23da3f1d3ac0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/coroutines/FlowTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/coroutines/FlowTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt index 91259a65eff9..3e75cebb1c7d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt @@ -34,6 +34,7 @@ import android.hardware.face.FaceSensorPropertiesInternal import android.os.CancellationSignal import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState import com.android.internal.logging.InstanceId.fakeInstanceId import com.android.internal.logging.UiEventLogger import com.android.systemui.Flags as AConfigFlags @@ -55,6 +56,7 @@ import com.android.systemui.deviceentry.shared.model.FaceDetectionStatus import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus import com.android.systemui.display.data.repository.displayRepository import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.keyguard.data.repository.BiometricType import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository @@ -77,6 +79,9 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.testKosmos import com.android.systemui.user.data.model.SelectionStatus @@ -90,6 +95,7 @@ import com.google.common.truth.Truth.assertThat import java.io.PrintWriter import java.io.StringWriter import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent @@ -136,12 +142,12 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { @Captor private lateinit var faceLockoutResetCallback: ArgumentCaptor<FaceManager.LockoutResetCallback> - private val testDispatcher = kosmos.testDispatcher + private val testDispatcher by lazy { kosmos.testDispatcher } - private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository - private val testScope = kosmos.testScope - private val fakeUserRepository = kosmos.fakeUserRepository - private val fakeExecutor = kosmos.fakeExecutor + private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } + private val testScope by lazy { kosmos.testScope } + private val fakeUserRepository by lazy { kosmos.fakeUserRepository } + private val fakeExecutor by lazy { kosmos.fakeExecutor } private lateinit var authStatus: FlowValue<FaceAuthenticationStatus?> private lateinit var detectStatus: FlowValue<FaceDetectionStatus?> private lateinit var authRunning: FlowValue<Boolean?> @@ -149,18 +155,19 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { private lateinit var lockedOut: FlowValue<Boolean?> private lateinit var canFaceAuthRun: FlowValue<Boolean?> private lateinit var authenticated: FlowValue<Boolean?> - private val biometricSettingsRepository = kosmos.fakeBiometricSettingsRepository - private val deviceEntryFingerprintAuthRepository = + private val biometricSettingsRepository by lazy { kosmos.fakeBiometricSettingsRepository } + private val deviceEntryFingerprintAuthRepository by lazy { kosmos.fakeDeviceEntryFingerprintAuthRepository - private val trustRepository = kosmos.fakeTrustRepository - private val keyguardRepository = kosmos.fakeKeyguardRepository - private val powerInteractor = kosmos.powerInteractor - private val keyguardInteractor = kosmos.keyguardInteractor - private val alternateBouncerInteractor = kosmos.alternateBouncerInteractor - private val displayStateInteractor = kosmos.displayStateInteractor - private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository - private val displayRepository = kosmos.displayRepository - private val keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor + } + private val trustRepository by lazy { kosmos.fakeTrustRepository } + private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } + private val powerInteractor by lazy { kosmos.powerInteractor } + private val keyguardInteractor by lazy { kosmos.keyguardInteractor } + private val alternateBouncerInteractor by lazy { kosmos.alternateBouncerInteractor } + private val displayStateInteractor by lazy { kosmos.displayStateInteractor } + private val bouncerRepository by lazy { kosmos.fakeKeyguardBouncerRepository } + private val displayRepository by lazy { kosmos.displayRepository } + private val keyguardTransitionInteractor by lazy { kosmos.keyguardTransitionInteractor } private lateinit var featureFlags: FakeFeatureFlags private var wasAuthCancelled = false @@ -180,9 +187,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { whenever(bypassController.bypassEnabled).thenReturn(true) underTest = createDeviceEntryFaceAuthRepositoryImpl(faceManager, bypassController) - mSetFlagsRule.disableFlags( - AConfigFlags.FLAG_KEYGUARD_WM_STATE_REFACTOR, - ) + if (!SceneContainerFlag.isEnabled) { + mSetFlagsRule.disableFlags( + AConfigFlags.FLAG_KEYGUARD_WM_STATE_REFACTOR, + ) + } } private fun createDeviceEntryFaceAuthRepositoryImpl( @@ -227,6 +236,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { powerInteractor, keyguardInteractor, alternateBouncerInteractor, + { kosmos.sceneInteractor }, faceDetectBuffer, faceAuthBuffer, keyguardTransitionInteractor, @@ -547,6 +557,24 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } @Test + @EnableSceneContainer + fun withSceneContainerEnabled_authenticateDoesNotRunWhenKeyguardIsGoingAway() = + testScope.runTest { + testGatingCheckForFaceAuth(sceneContainerEnabled = true) { + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + KeyguardState.LOCKSCREEN, + KeyguardState.UNDEFINED, + value = 0.5f, + transitionState = TransitionState.RUNNING + ), + validateStep = false + ) + runCurrent() + } + } + + @Test fun authenticateDoesNotRunWhenDeviceIsGoingToSleep() = testScope.runTest { testGatingCheckForFaceAuth { @@ -595,6 +623,31 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } @Test + @EnableSceneContainer + fun withSceneContainer_authenticateRunsWhenSecureCameraIsActiveIfBouncerIsShowing() = + testScope.runTest { + initCollectors() + allPreconditionsToRunFaceAuthAreTrue(sceneContainerEnabled = true) + bouncerRepository.setAlternateVisible(false) + + // launch secure camera + keyguardInteractor.onCameraLaunchDetected(CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP) + keyguardRepository.setKeyguardOccluded(true) + kosmos.sceneInteractor.snapToScene(Scenes.Lockscreen, "for-test") + runCurrent() + assertThat(canFaceAuthRun()).isFalse() + + // but bouncer is shown after that. + kosmos.sceneInteractor.changeScene(Scenes.Bouncer, "for-test") + kosmos.sceneInteractor.setTransitionState( + MutableStateFlow(ObservableTransitionState.Idle(Scenes.Bouncer)) + ) + runCurrent() + + assertThat(canFaceAuthRun()).isTrue() + } + + @Test fun authenticateDoesNotRunOnUnsupportedPosture() = testScope.runTest { testGatingCheckForFaceAuth { @@ -841,6 +894,24 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } @Test + @EnableSceneContainer + fun withSceneContainer_faceDetectDoesNotRunWhenKeyguardGoingAway() = + testScope.runTest { + testGatingCheckForDetect(sceneContainerEnabled = true) { + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + KeyguardState.LOCKSCREEN, + KeyguardState.UNDEFINED, + value = 0.5f, + transitionState = TransitionState.RUNNING + ), + validateStep = false + ) + runCurrent() + } + } + + @Test fun detectDoesNotRunWhenDeviceSleepingStartingToSleep() = testScope.runTest { testGatingCheckForDetect { @@ -1052,10 +1123,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } private suspend fun TestScope.testGatingCheckForFaceAuth( + sceneContainerEnabled: Boolean = false, gatingCheckModifier: suspend () -> Unit ) { initCollectors() - allPreconditionsToRunFaceAuthAreTrue() + allPreconditionsToRunFaceAuthAreTrue(sceneContainerEnabled) gatingCheckModifier() runCurrent() @@ -1069,7 +1141,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { faceAuthenticateIsNotCalled() // flip the gating check back on. - allPreconditionsToRunFaceAuthAreTrue() + allPreconditionsToRunFaceAuthAreTrue(sceneContainerEnabled) assertThat(underTest.canRunFaceAuth.value).isTrue() faceAuthenticateIsCalled() @@ -1094,10 +1166,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } private suspend fun TestScope.testGatingCheckForDetect( + sceneContainerEnabled: Boolean = false, gatingCheckModifier: suspend () -> Unit ) { initCollectors() - allPreconditionsToRunFaceAuthAreTrue() + allPreconditionsToRunFaceAuthAreTrue(sceneContainerEnabled) // This will stop face auth from running but is required to be false for detect. biometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(false) @@ -1145,12 +1218,22 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { cancellationSignal.value.setOnCancelListener { wasAuthCancelled = true } } - private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue() { + private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue( + sceneContainerEnabled: Boolean = false + ) { fakeExecutor.runAllReady() verify(faceManager, atLeastOnce()) .addLockoutResetCallback(faceLockoutResetCallback.capture()) trustRepository.setCurrentUserTrusted(false) - keyguardRepository.setKeyguardGoingAway(false) + if (sceneContainerEnabled) { + // Keyguard is not going away + kosmos.fakeKeyguardTransitionRepository.sendTransitionStep( + TransitionStep(KeyguardState.OFF, KeyguardState.LOCKSCREEN, value = 1.0f), + validateStep = false + ) + } else { + keyguardRepository.setKeyguardGoingAway(false) + } powerInteractor.setAwakeForTest() biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) biometricSettingsRepository.setIsFaceAuthSupportedInCurrentPosture(true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt index 88ba0411b414..296a0fc2eb40 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt @@ -88,21 +88,14 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() { } @Test - fun testWakeUpSetsExitAnimationsRunning() { - controller.wakeUp() - - verify(stateController).setExitAnimationsRunning(true) - } - - @Test - fun testWakeUpAfterStartWillCancel() { + fun testOnWakeUpAfterStartWillCancel() { val mockStartAnimator: AnimatorSet = mock() controller.startEntryAnimations(false, animatorBuilder = { mockStartAnimator }) verify(mockStartAnimator, never()).cancel() - controller.wakeUp() + controller.onWakeUp() // Verify that we cancelled the start animator in favor of the exit // animator. diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java index 6412276ba34b..3895595aaea6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java @@ -62,6 +62,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -324,4 +325,13 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { // enabled. mController.onViewAttached(); } + + @Test + public void destroy_cleansUpState() { + mController.destroy(); + verify(mStateController).removeCallback(any()); + verify(mAmbientStatusBarViewController).destroy(); + verify(mComplicationHostViewController).destroy(); + verify(mLowLightTransitionCoordinator).setLowLightEnterListener(ArgumentMatchers.isNull()); + } } 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 89ec3cfe41c5..eda9039c748e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt @@ -68,7 +68,6 @@ import com.android.systemui.navigationbar.gestural.domain.GestureInteractor import com.android.systemui.testKosmos import com.android.systemui.touch.TouchInsetManager import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -89,7 +88,9 @@ import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq +import org.mockito.kotlin.mock import org.mockito.kotlin.spy +import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -115,8 +116,6 @@ class DreamOverlayServiceTest : SysuiTestCase() { @Mock lateinit var mComplicationComponentFactory: ComplicationComponent.Factory - @Mock lateinit var mComplicationComponent: ComplicationComponent - @Mock lateinit var mComplicationHostViewController: ComplicationHostViewController @Mock lateinit var mComplicationVisibilityController: ComplicationLayoutEngine @@ -125,20 +124,12 @@ class DreamOverlayServiceTest : SysuiTestCase() { lateinit var mDreamComplicationComponentFactory: com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory - @Mock - lateinit var mDreamComplicationComponent: - com.android.systemui.dreams.complication.dagger.ComplicationComponent - @Mock lateinit var mHideComplicationTouchHandler: HideComplicationTouchHandler @Mock lateinit var mDreamOverlayComponentFactory: DreamOverlayComponent.Factory - @Mock lateinit var mDreamOverlayComponent: DreamOverlayComponent - @Mock lateinit var mAmbientTouchComponentFactory: AmbientTouchComponent.Factory - @Mock lateinit var mAmbientTouchComponent: AmbientTouchComponent - @Mock lateinit var mDreamOverlayContainerView: DreamOverlayContainerView @Mock lateinit var mDreamOverlayContainerViewController: DreamOverlayContainerViewController @@ -170,40 +161,100 @@ class DreamOverlayServiceTest : SysuiTestCase() { private lateinit var communalRepository: FakeCommunalSceneRepository private var viewCaptureSpy = spy(ViewCaptureFactory.getInstance(context)) private lateinit var gestureInteractor: GestureInteractor + private lateinit var environmentComponents: EnvironmentComponents @Captor var mViewCaptor: ArgumentCaptor<View>? = null private lateinit var mService: DreamOverlayService - @Before - fun setup() { - MockitoAnnotations.initMocks(this) + private class EnvironmentComponents( + val dreamsComplicationComponent: + com.android.systemui.dreams.complication.dagger.ComplicationComponent, + val dreamOverlayComponent: DreamOverlayComponent, + val complicationComponent: ComplicationComponent, + val ambientTouchComponent: AmbientTouchComponent, + ) { + fun clearInvocations() { + clearInvocations( + dreamsComplicationComponent, + dreamOverlayComponent, + complicationComponent, + ambientTouchComponent + ) + } - lifecycleRegistry = FakeLifecycleRegistry(mLifecycleOwner) - bouncerRepository = kosmos.fakeKeyguardBouncerRepository - communalRepository = kosmos.fakeCommunalSceneRepository - gestureInteractor = spy(kosmos.gestureInteractor) + fun verifyNoMoreInteractions() { + Mockito.verifyNoMoreInteractions( + dreamsComplicationComponent, + dreamOverlayComponent, + complicationComponent, + ambientTouchComponent + ) + } + } - whenever(mDreamOverlayComponent.getDreamOverlayContainerViewController()) + private fun setupComponentFactories( + dreamComplicationComponentFactory: + com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory, + dreamOverlayComponentFactory: DreamOverlayComponent.Factory, + complicationComponentFactory: ComplicationComponent.Factory, + ambientTouchComponentFactory: AmbientTouchComponent.Factory + ): EnvironmentComponents { + val dreamOverlayComponent = mock<DreamOverlayComponent>() + whenever(dreamOverlayComponent.getDreamOverlayContainerViewController()) .thenReturn(mDreamOverlayContainerViewController) - whenever(mComplicationComponent.getComplicationHostViewController()) + + val complicationComponent = mock<ComplicationComponent>() + whenever(complicationComponent.getComplicationHostViewController()) .thenReturn(mComplicationHostViewController) whenever(mLifecycleOwner.registry).thenReturn(lifecycleRegistry) mCommunalInteractor = Mockito.spy(kosmos.communalInteractor) - whenever(mComplicationComponentFactory.create(any(), any(), any(), any())) - .thenReturn(mComplicationComponent) - whenever(mComplicationComponent.getVisibilityController()) + whenever(complicationComponentFactory.create(any(), any(), any(), any())) + .thenReturn(complicationComponent) + whenever(complicationComponent.getVisibilityController()) .thenReturn(mComplicationVisibilityController) - whenever(mDreamComplicationComponent.getHideComplicationTouchHandler()) + + val dreamComplicationComponent = + mock<com.android.systemui.dreams.complication.dagger.ComplicationComponent>() + whenever(dreamComplicationComponent.getHideComplicationTouchHandler()) .thenReturn(mHideComplicationTouchHandler) - whenever(mDreamComplicationComponentFactory.create(any(), any())) - .thenReturn(mDreamComplicationComponent) - whenever(mDreamOverlayComponentFactory.create(any(), any(), any())) - .thenReturn(mDreamOverlayComponent) - whenever(mAmbientTouchComponentFactory.create(any(), any())) - .thenReturn(mAmbientTouchComponent) - whenever(mAmbientTouchComponent.getTouchMonitor()).thenReturn(mTouchMonitor) + whenever(dreamComplicationComponentFactory.create(any(), any())) + .thenReturn(dreamComplicationComponent) + + whenever(dreamOverlayComponentFactory.create(any(), any(), any())) + .thenReturn(dreamOverlayComponent) + + val ambientTouchComponent = mock<AmbientTouchComponent>() + whenever(ambientTouchComponentFactory.create(any(), any())) + .thenReturn(ambientTouchComponent) + whenever(ambientTouchComponent.getTouchMonitor()).thenReturn(mTouchMonitor) + + return EnvironmentComponents( + dreamComplicationComponent, + dreamOverlayComponent, + complicationComponent, + ambientTouchComponent + ) + } + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + lifecycleRegistry = FakeLifecycleRegistry(mLifecycleOwner) + bouncerRepository = kosmos.fakeKeyguardBouncerRepository + communalRepository = kosmos.fakeCommunalSceneRepository + gestureInteractor = spy(kosmos.gestureInteractor) + + environmentComponents = + setupComponentFactories( + mDreamComplicationComponentFactory, + mDreamOverlayComponentFactory, + mComplicationComponentFactory, + mAmbientTouchComponentFactory + ) + whenever(mDreamOverlayContainerViewController.containerView) .thenReturn(mDreamOverlayContainerView) whenever(mScrimManager.getCurrentController()).thenReturn(mScrimController) @@ -570,9 +621,8 @@ class DreamOverlayServiceTest : SysuiTestCase() { // Assert that the overlay is not showing complications. assertThat(mService.shouldShowComplications()).isFalse() - Mockito.clearInvocations(mDreamOverlayComponent) - Mockito.clearInvocations(mAmbientTouchComponent) - Mockito.clearInvocations(mWindowManager) + environmentComponents.clearInvocations() + clearInvocations(mWindowManager) // New dream starting with dream complications showing. Note that when a new dream is // binding to the dream overlay service, it receives the same instance of IBinder as the @@ -594,8 +644,11 @@ class DreamOverlayServiceTest : SysuiTestCase() { // Verify that new instances of overlay container view controller and overlay touch monitor // are created. - verify(mDreamOverlayComponent).getDreamOverlayContainerViewController() - verify(mAmbientTouchComponent).getTouchMonitor() + verify(environmentComponents.dreamOverlayComponent).getDreamOverlayContainerViewController() + verify(environmentComponents.ambientTouchComponent).getTouchMonitor() + + // Verify DreamOverlayContainerViewController is destroyed. + verify(mDreamOverlayContainerViewController).destroy() } @Test @@ -611,14 +664,14 @@ class DreamOverlayServiceTest : SysuiTestCase() { ) mMainExecutor.runAllReady() mService.onWakeUp() - verify(mDreamOverlayContainerViewController).wakeUp() + verify(mDreamOverlayContainerViewController).onWakeUp() verify(mDreamOverlayCallbackController).onWakeUp() } @Test fun testWakeUpBeforeStartDoesNothing() { mService.onWakeUp() - verify(mDreamOverlayContainerViewController, Mockito.never()).wakeUp() + verify(mDreamOverlayContainerViewController, Mockito.never()).onWakeUp() } @Test @@ -1002,6 +1055,34 @@ class DreamOverlayServiceTest : SysuiTestCase() { .isEqualTo(ComponentName.unflattenFromString(DREAM_COMPONENT)?.packageName) } + @Test + fun testComponentsRecreatedBetweenDreams() { + clearInvocations( + mDreamComplicationComponentFactory, + mDreamOverlayComponentFactory, + mComplicationComponentFactory, + mAmbientTouchComponentFactory + ) + + mService.onEndDream() + + setupComponentFactories( + mDreamComplicationComponentFactory, + mDreamOverlayComponentFactory, + mComplicationComponentFactory, + mAmbientTouchComponentFactory + ) + + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + environmentComponents.verifyNoMoreInteractions() + } + internal class FakeLifecycleRegistry(provider: LifecycleOwner) : LifecycleRegistry(provider) { val mLifecycles: MutableList<State> = ArrayList() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt index f82beff11d8f..50b727c3fed9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt @@ -23,6 +23,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestableContext import com.android.systemui.contextualeducation.GestureType.BACK import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.education.data.model.EduDeviceConnectionTime import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher @@ -105,6 +106,19 @@ class ContextualEducationRepositoryTest : SysuiTestCase() { assertThat(model).isEqualTo(newModel) } + @Test + fun eduDeviceConnectionTimeDataChangedOnUpdate() = + testScope.runTest { + val newModel = + EduDeviceConnectionTime( + keyboardFirstConnectionTime = kosmos.fakeEduClock.instant(), + touchpadFirstConnectionTime = kosmos.fakeEduClock.instant(), + ) + underTest.updateEduDeviceConnectionTime { newModel } + val model by collectLastValue(underTest.readEduDeviceConnectionTime()) + assertThat(model).isEqualTo(newModel) + } + /** Test context which allows overriding getFilesDir path */ private class TestContext(context: Context, private val folder: File) : SysuiTestableContext(context) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt index 23f923a6fb09..3aed79f5915f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.education.domain.interactor +import android.content.pm.UserInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -26,10 +27,15 @@ import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.education.data.repository.contextualEducationRepository import com.android.systemui.education.data.repository.fakeEduClock import com.android.systemui.education.shared.model.EducationUiType +import com.android.systemui.keyboard.data.repository.keyboardRepository import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos +import com.android.systemui.touchpad.data.repository.touchpadRepository +import com.android.systemui.user.data.repository.fakeUserRepository import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -38,16 +44,23 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@kotlinx.coroutines.ExperimentalCoroutinesApi class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val contextualEduInteractor = kosmos.contextualEducationInteractor + private val touchpadRepository = kosmos.touchpadRepository + private val keyboardRepository = kosmos.keyboardRepository + private val userRepository = kosmos.fakeUserRepository + private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor private val eduClock = kosmos.fakeEduClock @Before fun setup() { underTest.start() + contextualEduInteractor.start() + userRepository.setUserInfos(USER_INFOS) } @Test @@ -67,7 +80,6 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { } @Test - @kotlinx.coroutines.ExperimentalCoroutinesApi fun newEducationNotificationOn2ndEducation() = testScope.runTest { val model by collectLastValue(underTest.educationTriggered) @@ -115,10 +127,103 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { ) } + @Test + fun newTouchpadConnectionTimeOnFirstTouchpadConnected() = + testScope.runTest { + setIsAnyTouchpadConnected(true) + val model = contextualEduInteractor.getEduDeviceConnectionTime() + assertThat(model.touchpadFirstConnectionTime).isEqualTo(eduClock.instant()) + } + + @Test + fun unchangedTouchpadConnectionTimeOnSecondConnection() = + testScope.runTest { + val firstConnectionTime = eduClock.instant() + setIsAnyTouchpadConnected(true) + setIsAnyTouchpadConnected(false) + + eduClock.offset(1.hours) + setIsAnyTouchpadConnected(true) + + val model = contextualEduInteractor.getEduDeviceConnectionTime() + assertThat(model.touchpadFirstConnectionTime).isEqualTo(firstConnectionTime) + } + + @Test + fun newTouchpadConnectionTimeOnUserChanged() = + testScope.runTest { + // Touchpad connected for user 0 + setIsAnyTouchpadConnected(true) + + // Change user + eduClock.offset(1.hours) + val newUserFirstConnectionTime = eduClock.instant() + userRepository.setSelectedUserInfo(USER_INFOS[0]) + runCurrent() + + val model = contextualEduInteractor.getEduDeviceConnectionTime() + assertThat(model.touchpadFirstConnectionTime).isEqualTo(newUserFirstConnectionTime) + } + + @Test + fun newKeyboardConnectionTimeOnKeyboardConnected() = + testScope.runTest { + setIsAnyKeyboardConnected(true) + val model = contextualEduInteractor.getEduDeviceConnectionTime() + assertThat(model.keyboardFirstConnectionTime).isEqualTo(eduClock.instant()) + } + + @Test + fun unchangedKeyboardConnectionTimeOnSecondConnection() = + testScope.runTest { + val firstConnectionTime = eduClock.instant() + setIsAnyKeyboardConnected(true) + setIsAnyKeyboardConnected(false) + + eduClock.offset(1.hours) + setIsAnyKeyboardConnected(true) + + val model = contextualEduInteractor.getEduDeviceConnectionTime() + assertThat(model.keyboardFirstConnectionTime).isEqualTo(firstConnectionTime) + } + + @Test + fun newKeyboardConnectionTimeOnUserChanged() = + testScope.runTest { + // Keyboard connected for user 0 + setIsAnyKeyboardConnected(true) + + // Change user + eduClock.offset(1.hours) + val newUserFirstConnectionTime = eduClock.instant() + userRepository.setSelectedUserInfo(USER_INFOS[0]) + runCurrent() + + val model = contextualEduInteractor.getEduDeviceConnectionTime() + assertThat(model.keyboardFirstConnectionTime).isEqualTo(newUserFirstConnectionTime) + } + private suspend fun triggerMaxEducationSignals(gestureType: GestureType) { // Increment max number of signal to try triggering education for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) { contextualEduInteractor.incrementSignalCount(gestureType) } } + + private fun TestScope.setIsAnyTouchpadConnected(isConnected: Boolean) { + touchpadRepository.setIsAnyTouchpadConnected(isConnected) + runCurrent() + } + + private fun TestScope.setIsAnyKeyboardConnected(isConnected: Boolean) { + keyboardRepository.setIsAnyKeyboardConnected(isConnected) + runCurrent() + } + + companion object { + private val USER_INFOS = + listOf( + UserInfo(101, "Second User", 0), + ) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt index 3a28471b5f9a..9bcc19defbd5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt @@ -369,4 +369,18 @@ class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() { invokeOnCallback { it.onStrongAuthStateChanged(0) } assertThat(shouldUpdateIndicatorVisibility).isTrue() } + + @Test + fun isLockedOut_initialStateFalse() = + testScope.runTest { + whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(false) + assertThat(underTest.isLockedOut.value).isEqualTo(false) + } + + @Test + fun isLockedOut_initialStateTrue() = + testScope.runTest { + whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(true) + assertThat(underTest.isLockedOut.value).isEqualTo(true) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt index 783e3b5ba810..ee4a0d2d4e75 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt @@ -150,13 +150,13 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT @Test @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) - fun testTransitionToLockscreen_onPowerButtonPress_canDream_glanceableHubAvailable() = + fun testTransitionToLockscreen_onWake_canDream_glanceableHubAvailable() = testScope.runTest { whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) kosmos.setCommunalAvailable(true) runCurrent() - powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_POWER_BUTTON) + powerInteractor.setAwakeForTest() runCurrent() // If dreaming is possible and communal is available, then we should transition to @@ -170,14 +170,14 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT @Test @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR, FLAG_COMMUNAL_SCENE_KTF_REFACTOR) - fun testTransitionToLockscreen_onPowerButtonPress_canDream_ktfRefactor() = + fun testTransitionToLockscreen_onWake_canDream_ktfRefactor() = testScope.runTest { whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) kosmos.setCommunalAvailable(true) runCurrent() clearInvocations(kosmos.fakeCommunalSceneRepository) - powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_POWER_BUTTON) + powerInteractor.setAwakeForTest() runCurrent() // If dreaming is possible and communal is available, then we should transition to @@ -188,13 +188,13 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT @Test @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) - fun testTransitionToLockscreen_onPowerButtonPress_canNotDream_glanceableHubAvailable() = + fun testTransitionToLockscreen_onWake_canNotDream_glanceableHubAvailable() = testScope.runTest { whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(false) kosmos.setCommunalAvailable(true) runCurrent() - powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_POWER_BUTTON) + powerInteractor.setAwakeForTest() runCurrent() // If dreaming is NOT possible but communal is available, then we should transition to @@ -208,13 +208,13 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT @Test @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) - fun testTransitionToLockscreen_onPowerButtonPress_canNDream_glanceableHubNotAvailable() = + fun testTransitionToLockscreen_onWake_canNDream_glanceableHubNotAvailable() = testScope.runTest { whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true) kosmos.setCommunalAvailable(false) runCurrent() - powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_POWER_BUTTON) + powerInteractor.setAwakeForTest() runCurrent() // If dreaming is possible but communal is NOT available, then we should transition to diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt index 2b2c121fb79a..aee72de22a24 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt @@ -25,8 +25,11 @@ import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.logging.uiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.shared.FaceAuthUiEvent import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState @@ -260,6 +263,23 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { } @Test + fun triggersFaceAuthWhenLockscreenIsClicked() = + testScope.runTest { + collectLastValue(underTest.isMenuVisible) + runCurrent() + kosmos.fakeDeviceEntryFaceAuthRepository.canRunFaceAuth.value = true + + underTest.onClick(100.0f, 100.0f) + runCurrent() + + val runningAuthRequest = + kosmos.fakeDeviceEntryFaceAuthRepository.runningAuthRequest.value + assertThat(runningAuthRequest?.first) + .isEqualTo(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED) + assertThat(runningAuthRequest?.second).isEqualTo(true) + } + + @Test fun showMenu_leaveLockscreen_returnToLockscreen_menuNotVisible() = testScope.runTest { val isMenuVisible by collectLastValue(underTest.isMenuVisible) @@ -302,6 +322,7 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { broadcastDispatcher = fakeBroadcastDispatcher, accessibilityManager = kosmos.accessibilityManagerWrapper, pulsingGestureListener = kosmos.pulsingGestureListener, + faceAuthInteractor = kosmos.deviceEntryFaceAuthInteractor, ) setUpState() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt index a40746859a9a..3e1f4f6da5e4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -504,6 +504,45 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() } @Test + @DisableSceneContainer + fun alphaFromShadeExpansion_doesNotEmitWhenLockscreenToDreamTransitionRunning() = + testScope.runTest { + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + testScope, + ) + + val alpha by collectLastValue(underTest.alpha(viewState)) + shadeTestUtil.setQsExpansion(0f) + + assertThat(alpha).isEqualTo(1f) + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DREAMING, + transitionState = TransitionState.STARTED, + value = 0f, + ), + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DREAMING, + transitionState = TransitionState.RUNNING, + value = 0.1f, + ), + ), + testScope, + ) + + val alphaBeforeExpansion = alpha + shadeTestUtil.setQsExpansion(0.5f) + // Alpha should remain unchanged instead of being affected by expansion. + assertThat(alpha).isEqualTo(alphaBeforeExpansion) + } + + @Test fun alpha_shadeClosedOverLockscreen_isOne() = testScope.runTest { val alpha by collectLastValue(underTest.alpha(viewState)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/BaseActivatableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/BaseActivatableTest.kt deleted file mode 100644 index f6f58c9bdcf2..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/BaseActivatableTest.kt +++ /dev/null @@ -1,328 +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. - */ - -@file:OptIn(ExperimentalCoroutinesApi::class) - -package com.android.systemui.lifecycle - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.kosmos.testScope -import com.android.systemui.testKosmos -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith - -@SmallTest -@RunWith(AndroidJUnit4::class) -class BaseActivatableTest : SysuiTestCase() { - - private val kosmos = testKosmos() - private val testScope = kosmos.testScope - - private val underTest = FakeActivatable() - - @Test - fun activate() = - testScope.runTest { - assertThat(underTest.isActive).isFalse() - assertThat(underTest.activationCount).isEqualTo(0) - assertThat(underTest.cancellationCount).isEqualTo(0) - - underTest.activateIn(testScope) - runCurrent() - assertThat(underTest.isActive).isTrue() - assertThat(underTest.activationCount).isEqualTo(1) - assertThat(underTest.cancellationCount).isEqualTo(0) - } - - @Test - fun activate_andCancel() = - testScope.runTest { - assertThat(underTest.isActive).isFalse() - assertThat(underTest.activationCount).isEqualTo(0) - assertThat(underTest.cancellationCount).isEqualTo(0) - - val job = Job() - underTest.activateIn(testScope, context = job) - runCurrent() - assertThat(underTest.isActive).isTrue() - assertThat(underTest.activationCount).isEqualTo(1) - assertThat(underTest.cancellationCount).isEqualTo(0) - - job.cancel() - runCurrent() - assertThat(underTest.isActive).isFalse() - assertThat(underTest.activationCount).isEqualTo(1) - assertThat(underTest.cancellationCount).isEqualTo(1) - } - - @Test - fun activate_afterCancellation() = - testScope.runTest { - assertThat(underTest.isActive).isFalse() - assertThat(underTest.activationCount).isEqualTo(0) - assertThat(underTest.cancellationCount).isEqualTo(0) - - val job = Job() - underTest.activateIn(testScope, context = job) - runCurrent() - assertThat(underTest.isActive).isTrue() - assertThat(underTest.activationCount).isEqualTo(1) - assertThat(underTest.cancellationCount).isEqualTo(0) - - job.cancel() - runCurrent() - assertThat(underTest.isActive).isFalse() - assertThat(underTest.activationCount).isEqualTo(1) - assertThat(underTest.cancellationCount).isEqualTo(1) - - underTest.activateIn(testScope) - runCurrent() - assertThat(underTest.isActive).isTrue() - assertThat(underTest.activationCount).isEqualTo(2) - assertThat(underTest.cancellationCount).isEqualTo(1) - } - - @Test(expected = IllegalStateException::class) - fun activate_whileActive_throws() = - testScope.runTest { - assertThat(underTest.isActive).isFalse() - assertThat(underTest.activationCount).isEqualTo(0) - assertThat(underTest.cancellationCount).isEqualTo(0) - - underTest.activateIn(testScope) - runCurrent() - assertThat(underTest.isActive).isTrue() - assertThat(underTest.activationCount).isEqualTo(1) - assertThat(underTest.cancellationCount).isEqualTo(0) - - underTest.activateIn(testScope) - runCurrent() - } - - @Test - fun addChild_beforeActive_activatesChildrenOnceActivated() = - testScope.runTest { - val child1 = FakeActivatable() - val child2 = FakeActivatable() - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isFalse() - - assertThat(underTest.isActive).isFalse() - underTest.addChild(child1) - underTest.addChild(child2) - assertThat(underTest.isActive).isFalse() - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isFalse() - - underTest.activateIn(this) - runCurrent() - - assertThat(underTest.isActive).isTrue() - assertThat(child1.isActive).isTrue() - assertThat(child2.isActive).isTrue() - } - - @Test - fun addChild_whileActive_activatesChildrenImmediately() = - testScope.runTest { - underTest.activateIn(this) - runCurrent() - assertThat(underTest.isActive).isTrue() - - val child1 = FakeActivatable() - val child2 = FakeActivatable() - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isFalse() - - underTest.addChild(child1) - underTest.addChild(child2) - runCurrent() - - assertThat(child1.isActive).isTrue() - assertThat(child2.isActive).isTrue() - } - - @Test - fun addChild_afterCancellation_doesNotActivateChildren() = - testScope.runTest { - val job = Job() - underTest.activateIn(this, context = job) - runCurrent() - assertThat(underTest.isActive).isTrue() - job.cancel() - runCurrent() - assertThat(underTest.isActive).isFalse() - - val child1 = FakeActivatable() - val child2 = FakeActivatable() - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isFalse() - - underTest.addChild(child1) - underTest.addChild(child2) - runCurrent() - - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isFalse() - } - - @Test - fun activate_cancellation_cancelsCurrentChildren() = - testScope.runTest { - val job = Job() - underTest.activateIn(this, context = job) - runCurrent() - assertThat(underTest.isActive).isTrue() - - val child1 = FakeActivatable() - val child2 = FakeActivatable() - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isFalse() - - underTest.addChild(child1) - underTest.addChild(child2) - runCurrent() - - assertThat(child1.isActive).isTrue() - assertThat(child2.isActive).isTrue() - - job.cancel() - runCurrent() - assertThat(underTest.isActive).isFalse() - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isFalse() - } - - @Test - fun activate_afterCancellation_reactivatesCurrentChildren() = - testScope.runTest { - val job = Job() - underTest.activateIn(this, context = job) - runCurrent() - assertThat(underTest.isActive).isTrue() - - val child1 = FakeActivatable() - val child2 = FakeActivatable() - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isFalse() - - underTest.addChild(child1) - underTest.addChild(child2) - runCurrent() - - assertThat(child1.isActive).isTrue() - assertThat(child2.isActive).isTrue() - - job.cancel() - runCurrent() - assertThat(underTest.isActive).isFalse() - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isFalse() - - underTest.activateIn(this) - runCurrent() - assertThat(underTest.isActive).isTrue() - assertThat(child1.isActive).isTrue() - assertThat(child2.isActive).isTrue() - } - - @Test - fun removeChild_beforeActive_neverActivatesChild() = - testScope.runTest { - val child1 = FakeActivatable() - val child2 = FakeActivatable() - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isFalse() - - assertThat(underTest.isActive).isFalse() - underTest.addChild(child1) - underTest.addChild(child2) - assertThat(underTest.isActive).isFalse() - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isFalse() - } - - @Test - fun removeChild_whileActive_cancelsChild() = - testScope.runTest { - val child1 = FakeActivatable() - val child2 = FakeActivatable() - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isFalse() - - assertThat(underTest.isActive).isFalse() - underTest.addChild(child1) - underTest.addChild(child2) - assertThat(underTest.isActive).isFalse() - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isFalse() - - underTest.activateIn(this) - runCurrent() - assertThat(underTest.isActive).isTrue() - assertThat(child1.isActive).isTrue() - assertThat(child2.isActive).isTrue() - - underTest.removeChild(child1) - runCurrent() - assertThat(underTest.isActive).isTrue() - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isTrue() - } - - @Test - fun removeChild_afterCancellation_doesNotReactivateChildren() = - testScope.runTest { - val child1 = FakeActivatable() - val child2 = FakeActivatable() - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isFalse() - - assertThat(underTest.isActive).isFalse() - underTest.addChild(child1) - underTest.addChild(child2) - assertThat(underTest.isActive).isFalse() - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isFalse() - - val job = Job() - underTest.activateIn(this, context = job) - runCurrent() - assertThat(underTest.isActive).isTrue() - assertThat(child1.isActive).isTrue() - assertThat(child2.isActive).isTrue() - - job.cancel() - runCurrent() - assertThat(underTest.isActive).isFalse() - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isFalse() - - underTest.removeChild(child1) - underTest.activateIn(this) - runCurrent() - assertThat(underTest.isActive).isTrue() - assertThat(child1.isActive).isFalse() - assertThat(child2.isActive).isTrue() - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ExclusiveActivatableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ExclusiveActivatableTest.kt new file mode 100644 index 000000000000..81b91802ec28 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ExclusiveActivatableTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.lifecycle + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ExclusiveActivatableTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val underTest = FakeActivatable() + + @Test + fun activate() = + testScope.runTest { + assertThat(underTest.activationCount).isEqualTo(0) + assertThat(underTest.cancellationCount).isEqualTo(0) + + underTest.activateIn(testScope) + runCurrent() + assertThat(underTest.activationCount).isEqualTo(1) + assertThat(underTest.cancellationCount).isEqualTo(0) + } + + @Test + fun activate_andCancel() = + testScope.runTest { + assertThat(underTest.activationCount).isEqualTo(0) + assertThat(underTest.cancellationCount).isEqualTo(0) + + val job = Job() + underTest.activateIn(testScope, context = job) + runCurrent() + assertThat(underTest.activationCount).isEqualTo(1) + assertThat(underTest.cancellationCount).isEqualTo(0) + + job.cancel() + runCurrent() + assertThat(underTest.activationCount).isEqualTo(1) + assertThat(underTest.cancellationCount).isEqualTo(1) + } + + @Test + fun activate_afterCancellation() = + testScope.runTest { + assertThat(underTest.activationCount).isEqualTo(0) + assertThat(underTest.cancellationCount).isEqualTo(0) + + val job = Job() + underTest.activateIn(testScope, context = job) + runCurrent() + assertThat(underTest.activationCount).isEqualTo(1) + assertThat(underTest.cancellationCount).isEqualTo(0) + + job.cancel() + runCurrent() + assertThat(underTest.activationCount).isEqualTo(1) + assertThat(underTest.cancellationCount).isEqualTo(1) + + underTest.activateIn(testScope) + runCurrent() + assertThat(underTest.activationCount).isEqualTo(2) + assertThat(underTest.cancellationCount).isEqualTo(1) + } + + @Test(expected = IllegalStateException::class) + fun activate_whileActive_throws() = + testScope.runTest { + assertThat(underTest.activationCount).isEqualTo(0) + assertThat(underTest.cancellationCount).isEqualTo(0) + + underTest.activateIn(testScope) + runCurrent() + assertThat(underTest.activationCount).isEqualTo(1) + assertThat(underTest.cancellationCount).isEqualTo(0) + + underTest.activateIn(testScope) + runCurrent() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt new file mode 100644 index 000000000000..8c9c527bd6fd --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt @@ -0,0 +1,84 @@ +/* + * 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.lifecycle + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class HydratorTest : SysuiTestCase() { + + @get:Rule val composeRule = createComposeRule() + + @Test + fun hydratedStateOf() { + val keepAliveMutable = mutableStateOf(true) + val upstreamStateFlow = MutableStateFlow(true) + val upstreamFlow = upstreamStateFlow.map { !it } + composeRule.setContent { + val keepAlive by keepAliveMutable + if (keepAlive) { + val viewModel = rememberViewModel { + FakeSysUiViewModel( + upstreamFlow = upstreamFlow, + upstreamStateFlow = upstreamStateFlow, + ) + } + + Column { + Text( + "upstreamStateFlow=${viewModel.stateBackedByStateFlow}", + Modifier.testTag("upstreamStateFlow") + ) + Text( + "upstreamFlow=${viewModel.stateBackedByFlow}", + Modifier.testTag("upstreamFlow") + ) + } + } + } + + composeRule.waitForIdle() + composeRule + .onNode(hasTestTag("upstreamStateFlow")) + .assertTextEquals("upstreamStateFlow=true") + composeRule.onNode(hasTestTag("upstreamFlow")).assertTextEquals("upstreamFlow=false") + + composeRule.runOnUiThread { upstreamStateFlow.value = false } + composeRule.waitForIdle() + composeRule + .onNode(hasTestTag("upstreamStateFlow")) + .assertTextEquals("upstreamStateFlow=false") + composeRule.onNode(hasTestTag("upstreamFlow")).assertTextEquals("upstreamFlow=true") + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt new file mode 100644 index 000000000000..22e589637432 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt @@ -0,0 +1,399 @@ +/** + * 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.domain.pipeline + +import android.app.Notification +import android.app.Notification.MediaStyle +import android.app.PendingIntent +import android.app.statusBarManager +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.media.AudioAttributes +import android.media.MediaDescription +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSession +import android.media.session.PlaybackState +import android.os.Bundle +import android.service.notification.StatusBarNotification +import androidx.media.utils.MediaConstants +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.Flags.MEDIA_RESUME_PROGRESS +import com.android.systemui.flags.Flags.MEDIA_SESSION_ACTIONS +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.graphics.imageLoader +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.util.fakeMediaControllerFactory +import com.android.systemui.media.controls.util.mediaFlags +import com.android.systemui.plugins.activityStarter +import com.android.systemui.res.R +import com.android.systemui.statusbar.SbnBuilder +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +private const val KEY = "KEY" +private const val PACKAGE_NAME = "com.example.app" +private const val SYSTEM_PACKAGE_NAME = "com.android.systemui" +private const val APP_NAME = "SystemUI" +private const val SESSION_ARTIST = "artist" +private const val SESSION_TITLE = "title" +private const val SESSION_EMPTY_TITLE = "" + +@SmallTest +@RunWith(AndroidJUnit4::class) +class MediaDataLoaderTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val testDispatcher = kosmos.testDispatcher + private val statusBarManager = kosmos.statusBarManager + private val mediaController = mock<MediaController>() + private val fakeFeatureFlags = kosmos.fakeFeatureFlagsClassic + private val mediaFlags = kosmos.mediaFlags + private val mediaControllerFactory = kosmos.fakeMediaControllerFactory + private val session = MediaSession(context, "MediaDataLoaderTestSession") + private val metadataBuilder = + MediaMetadata.Builder().apply { + putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) + putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) + } + + private val underTest: MediaDataLoader = + MediaDataLoader( + context, + testDispatcher, + testScope, + kosmos.activityStarter, + mediaControllerFactory, + mediaFlags, + kosmos.imageLoader, + statusBarManager + ) + + @Before + fun setUp() { + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) + mediaControllerFactory.setControllerForToken(session.sessionToken, mediaController) + } + + @Test + fun loadMediaData_returnsMediaData() = + testScope.runTest { + val song = "THIS_IS_A_SONG" + val artist = "THIS_IS_AN_ARTIST" + val albumArt = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) + + whenever(mediaController.playbackState) + .thenReturn( + PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 12, 1.0f).build() + ) + whenever(mediaController.playbackInfo) + .thenReturn( + MediaController.PlaybackInfo( + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL, + 0, + 0, + 0, + AudioAttributes.Builder().build(), + null + ) + ) + whenever(mediaController.metadata) + .thenReturn( + metadataBuilder + .putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, song) + .putString(MediaMetadata.METADATA_KEY_ARTIST, artist) + .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, albumArt) + .putLong( + MediaConstants.METADATA_KEY_IS_EXPLICIT, + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + ) + .build() + ) + + val result = underTest.loadMediaData(KEY, createMediaNotification()) + assertThat(result).isNotNull() + assertThat(result?.appIcon).isNotNull() + assertThat(result?.appIcon?.resId).isEqualTo(android.R.drawable.ic_media_pause) + assertThat(result?.artist).isEqualTo(artist) + assertThat(result?.song).isEqualTo(song) + assertThat(result?.artworkIcon).isNotNull() + assertThat(result?.artworkIcon?.bitmap?.width).isEqualTo(albumArt.width) + assertThat(result?.artworkIcon?.bitmap?.height).isEqualTo(albumArt.height) + assertThat(result?.token).isEqualTo(session.sessionToken) + assertThat(result?.device).isNull() + assertThat(result?.playbackLocation).isEqualTo(MediaData.PLAYBACK_LOCAL) + assertThat(result?.isPlaying).isTrue() + assertThat(result?.isExplicit).isTrue() + assertThat(result?.resumeAction).isNull() + assertThat(result?.resumeProgress).isNull() + } + + @Test + fun loadMediaDataForResumption_returnsMediaData() = + testScope.runTest { + fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) + + val song = "THIS_IS_A_SONG" + val artist = "THIS_IS_AN_ARTIST" + val albumArt = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) + + val extras = Bundle() + extras.putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED + ) + extras.putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.3) + extras.putLong( + MediaConstants.METADATA_KEY_IS_EXPLICIT, + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + ) + + val description = + MediaDescription.Builder() + .setTitle(song) + .setSubtitle(artist) + .setIconBitmap(albumArt) + .setExtras(extras) + .build() + + val intent = + PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE) + + val result = + underTest.loadMediaDataForResumption( + 0, + description, + Runnable {}, + null, + session.sessionToken, + APP_NAME, + intent, + PACKAGE_NAME + ) + assertThat(result).isNotNull() + assertThat(result?.appName).isEqualTo(APP_NAME) + assertThat(result?.song).isEqualTo(song) + assertThat(result?.artist).isEqualTo(artist) + assertThat(result?.artworkIcon).isNotNull() + assertThat(result?.artworkIcon?.bitmap?.width).isEqualTo(100) + assertThat(result?.artworkIcon?.bitmap?.height).isEqualTo(100) + assertThat(result?.token).isEqualTo(session.sessionToken) + assertThat(result?.clickIntent).isEqualTo(intent) + assertThat(result?.isExplicit).isTrue() + assertThat(result?.resumeProgress).isEqualTo(0.3) + } + + @Test + fun loadMediaData_songNameFallbacks() = + testScope.runTest { + // Check ordering of Song resolution: + // DISPLAY_TITLE > TITLE > notification TITLE > notification TITLE_BIG + + // DISPLAY_TITLE + whenever(mediaController.metadata) + .thenReturn( + MediaMetadata.Builder() + .putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, "title1") + .putString(MediaMetadata.METADATA_KEY_TITLE, "title2") + .build() + ) + val result1 = underTest.loadMediaData(KEY, createMediaNotification()) + assertThat(result1?.song).isEqualTo("title1") + + // TITLE + whenever(mediaController.metadata) + .thenReturn( + MediaMetadata.Builder() + .putString(MediaMetadata.METADATA_KEY_TITLE, "title2") + .build() + ) + val result2 = underTest.loadMediaData(KEY, createMediaNotification()) + assertThat(result2?.song).isEqualTo("title2") + + // notification TITLE + val notif = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.setContentTitle("notiftitle") + } + build() + } + whenever(mediaController.metadata).thenReturn(MediaMetadata.Builder().build()) + val result3 = underTest.loadMediaData(KEY, notif) + assertThat(result3?.song).isEqualTo("notiftitle") + + // Final fallback + whenever(mediaController.metadata).thenReturn(MediaMetadata.Builder().build()) + val result4 = underTest.loadMediaData(KEY, createMediaNotification()) + assertThat(result4?.song) + .isEqualTo(context.getString(R.string.controls_media_empty_title, result4?.appName)) + } + + @Test + fun loadMediaData_emptyTitle_hasPlaceholder() = + testScope.runTest { + val packageManager = mock<PackageManager>() + context.setMockPackageManager(packageManager) + whenever(packageManager.getApplicationLabel(any())).thenReturn(APP_NAME) + whenever(mediaController.metadata) + .thenReturn( + metadataBuilder + .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE) + .build() + ) + + val result = underTest.loadMediaData(KEY, createMediaNotification()) + + val placeholderTitle = context.getString(R.string.controls_media_empty_title, APP_NAME) + assertThat(result).isNotNull() + assertThat(result?.song).isEqualTo(placeholderTitle) + } + + @Test + fun loadMediaData_emptyMetadata_usesNotificationTitle() = + testScope.runTest { + val packageManager = mock<PackageManager>() + context.setMockPackageManager(packageManager) + whenever(packageManager.getApplicationLabel(any())).thenReturn(APP_NAME) + whenever(mediaController.metadata) + .thenReturn( + metadataBuilder + .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE) + .putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, SESSION_EMPTY_TITLE) + .build() + ) + val mediaNotification = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setContentTitle(SESSION_TITLE) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + } + build() + } + + val result = underTest.loadMediaData(KEY, mediaNotification) + + assertThat(result).isNotNull() + assertThat(result?.song).isEqualTo(SESSION_TITLE) + } + + @Test + fun loadMediaData_badArtwork_isNotUsed() = + testScope.runTest { + val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + val mediaNotification = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.setLargeIcon(artwork) + } + build() + } + + val result = underTest.loadMediaData(KEY, mediaNotification) + + assertThat(result).isNotNull() + } + + @Test + fun loadMediaData_invalidTokenNoCrash() = + testScope.runTest { + val bundle = Bundle() + // wrong data type + bundle.putParcelable(Notification.EXTRA_MEDIA_SESSION, Bundle()) + val rcn = + SbnBuilder().run { + setPkg(SYSTEM_PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.addExtras(bundle) + it.setStyle( + MediaStyle().apply { setRemotePlaybackInfo("Remote device", 0, null) } + ) + } + build() + } + + val result = underTest.loadMediaData(KEY, rcn) + assertThat(result).isNull() + } + + @Test + fun testLoadMediaDataInBg_invalidMediaRemoteIntentNoCrash() = + testScope.runTest { + val bundle = Bundle() + // wrong data type + bundle.putParcelable(Notification.EXTRA_MEDIA_REMOTE_INTENT, Bundle()) + val rcn = + SbnBuilder().run { + setPkg(SYSTEM_PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.addExtras(bundle) + it.setStyle( + MediaStyle().apply { + setMediaSession(session.sessionToken) + setRemotePlaybackInfo("Remote device", 0, null) + } + ) + } + build() + } + + val result = underTest.loadMediaData(KEY, rcn) + assertThat(result).isNotNull() + } + + private fun createMediaNotification( + mediaSession: MediaSession? = session, + applicationInfo: ApplicationInfo? = null + ): StatusBarNotification = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(mediaSession?.sessionToken) }) + if (applicationInfo != null) { + val bundle = Bundle() + bundle.putParcelable( + Notification.EXTRA_BUILDER_APPLICATION_INFO, + applicationInfo + ) + it.addExtras(bundle) + } + } + build() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt index 59992650cfc7..768fbca90b56 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt @@ -31,6 +31,8 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.qs.fgsManagerController import com.android.systemui.res.R import com.android.systemui.shade.largeScreenHeaderHelper +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.sysuiStatusBarStateController import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers @@ -140,6 +142,42 @@ class QSFragmentComposeViewModelTest : SysuiTestCase() { } } + @Test + fun statusBarState_followsController() = + with(kosmos) { + testScope.testWithinLifecycle { + val statusBarState by collectLastValue(underTest.statusBarState) + runCurrent() + + sysuiStatusBarStateController.setState(StatusBarState.SHADE) + assertThat(statusBarState).isEqualTo(StatusBarState.SHADE) + + sysuiStatusBarStateController.setState(StatusBarState.KEYGUARD) + assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD) + + sysuiStatusBarStateController.setState(StatusBarState.SHADE_LOCKED) + assertThat(statusBarState).isEqualTo(StatusBarState.SHADE_LOCKED) + } + } + + @Test + fun statusBarState_changesEarlyIfUpcomingStateIsKeyguard() = + with(kosmos) { + testScope.testWithinLifecycle { + val statusBarState by collectLastValue(underTest.statusBarState) + + sysuiStatusBarStateController.setState(StatusBarState.SHADE) + sysuiStatusBarStateController.setUpcomingState(StatusBarState.SHADE_LOCKED) + assertThat(statusBarState).isEqualTo(StatusBarState.SHADE) + + sysuiStatusBarStateController.setUpcomingState(StatusBarState.KEYGUARD) + assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD) + + sysuiStatusBarStateController.setUpcomingState(StatusBarState.SHADE) + assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD) + } + } + private inline fun TestScope.testWithinLifecycle( crossinline block: suspend TestScope.() -> TestResult ): TestResult { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt index 1118a6150fcc..e2149d907688 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt @@ -26,7 +26,6 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.kosmos.testScope -import com.android.systemui.lifecycle.activateIn import com.android.systemui.media.controls.data.repository.mediaFilterRepository import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor import com.android.systemui.media.controls.shared.model.MediaData @@ -82,7 +81,6 @@ class QuickSettingsSceneContentViewModelTest : SysuiTestCase() { footerActionsController = footerActionsController, mediaCarouselInteractor = kosmos.mediaCarouselInteractor, ) - underTest.activateIn(testScope) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 163b9b0959a8..c63381687f18 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -495,7 +495,9 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private fun getCurrentSceneInUi(): SceneKey { return when (val state = transitionState.value) { is ObservableTransitionState.Idle -> state.currentScene - is ObservableTransitionState.Transition -> state.fromScene + is ObservableTransitionState.Transition.ChangeCurrentScene -> state.fromScene + is ObservableTransitionState.Transition.ShowOrHideOverlay -> state.currentScene + is ObservableTransitionState.Transition.ReplaceOverlay -> state.currentScene } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModelTest.kt index 206d3ac67778..dd4432dd9797 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModelTest.kt @@ -55,7 +55,6 @@ class SceneActionsViewModelTest : SysuiTestCase() { testScope.runTest { val actions by collectLastValue(underTest.actions) - assertThat(underTest.isActive).isFalse() assertThat(actions).isEmpty() } @@ -66,7 +65,6 @@ class SceneActionsViewModelTest : SysuiTestCase() { underTest.activateIn(testScope) runCurrent() - assertThat(underTest.isActive).isTrue() assertThat(actions).isEmpty() } @@ -76,7 +74,6 @@ class SceneActionsViewModelTest : SysuiTestCase() { val actions by collectLastValue(underTest.actions) underTest.activateIn(testScope) runCurrent() - assertThat(underTest.isActive).isTrue() val expected1 = mapOf( @@ -116,7 +113,6 @@ class SceneActionsViewModelTest : SysuiTestCase() { job.cancel() runCurrent() - assertThat(underTest.isActive).isFalse() assertThat(actions).isEmpty() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelTest.kt new file mode 100644 index 000000000000..a310ef44cf35 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.statusbar.notification.row.ui.viewmodel + +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.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.notification.row.data.repository.fakeNotificationRowRepository +import com.android.systemui.statusbar.notification.row.shared.EnRouteContentModel +import com.android.systemui.statusbar.notification.row.shared.IconModel +import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@RunWith(AndroidJUnit4::class) +@SmallTest +@EnableFlags(RichOngoingNotificationFlag.FLAG_NAME) +class EnRouteViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val repository = kosmos.fakeNotificationRowRepository + + private var contentModel: EnRouteContentModel? + get() = repository.richOngoingContentModel.value as? EnRouteContentModel + set(value) { + repository.richOngoingContentModel.value = value + } + + private lateinit var underTest: EnRouteViewModel + + @Before + fun setup() { + underTest = kosmos.getEnRouteViewModel(repository) + } + + @Test + fun viewModelShowsContent() = + testScope.runTest { + val title by collectLastValue(underTest.title) + val text by collectLastValue(underTest.text) + contentModel = + exampleEnRouteContent( + title = "Example EnRoute Title", + text = "Example EnRoute Text", + ) + assertThat(title).isEqualTo("Example EnRoute Title") + assertThat(text).isEqualTo("Example EnRoute Text") + } + + private fun exampleEnRouteContent( + icon: IconModel = mock(), + title: CharSequence = "example text", + text: CharSequence = "example title", + ) = + EnRouteContentModel( + smallIcon = icon, + title = title, + text = text, + ) +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt index 789a47304ecf..f920b187e7e5 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.util.Log import android.view.View import androidx.annotation.VisibleForTesting +import androidx.core.text.util.LocalePreferences typealias WeatherTouchAction = (View) -> Unit @@ -54,12 +55,35 @@ data class WeatherData( } } - private fun readIntFromBundle(extras: Bundle, key: String): Int? = + private fun readIntFromBundle(extras: Bundle, key: String): Int? { try { - extras.getString(key)?.toInt() + return extras.getString(key)?.toInt() } catch (e: Exception) { - null + return null } + } + + fun getPlaceholderWeatherData(): WeatherData { + return getPlaceholderWeatherData( + LocalePreferences.getTemperatureUnit() == LocalePreferences.TemperatureUnit.CELSIUS + ) + } + + private const val DESCRIPTION_PLACEHODLER = "" + private const val TEMPERATURE_FAHRENHEIT_PLACEHOLDER = 58 + private const val TEMPERATURE_CELSIUS_PLACEHOLDER = 21 + private val WEATHERICON_PLACEHOLDER = WeatherData.WeatherStateIcon.MOSTLY_SUNNY + + fun getPlaceholderWeatherData(useCelsius: Boolean): WeatherData { + return WeatherData( + description = DESCRIPTION_PLACEHODLER, + state = WEATHERICON_PLACEHOLDER, + temperature = + if (useCelsius) TEMPERATURE_CELSIUS_PLACEHOLDER + else TEMPERATURE_FAHRENHEIT_PLACEHOLDER, + useCelsius = useCelsius, + ) + } } // Values for WeatherStateIcon must stay in sync with go/g3-WeatherStateIcon diff --git a/packages/SystemUI/res/layout/notification_template_en_route_contracted.xml b/packages/SystemUI/res/layout/notification_template_en_route_contracted.xml new file mode 100644 index 000000000000..59cfeccbeb36 --- /dev/null +++ b/packages/SystemUI/res/layout/notification_template_en_route_contracted.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<com.android.systemui.statusbar.notification.row.ui.view.EnRouteView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/status_bar_latest_event_content" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:minHeight="@*android:dimen/notification_headerless_min_height" + android:tag="enroute" + > + + <include layout="@*android:layout/notification_template_material_base" /> + +</com.android.systemui.statusbar.notification.row.ui.view.EnRouteView>
\ No newline at end of file diff --git a/packages/SystemUI/res/values-sw600dp-land/config.xml b/packages/SystemUI/res/values-sw600dp-land/config.xml index fc6d20e11d3b..0c11d2fa1d11 100644 --- a/packages/SystemUI/res/values-sw600dp-land/config.xml +++ b/packages/SystemUI/res/values-sw600dp-land/config.xml @@ -27,6 +27,8 @@ <!-- Whether to use the split 2-column notification shade --> <bool name="config_use_split_notification_shade">true</bool> + <bool name="config_use_large_screen_shade_header">true</bool> + <!-- The number of columns in the QuickSettings --> <integer name="quick_settings_num_columns">2</integer> diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml index b4383156dc71..c594f1cd9313 100644 --- a/packages/SystemUI/res/values-sw600dp/config.xml +++ b/packages/SystemUI/res/values-sw600dp/config.xml @@ -35,8 +35,6 @@ <!-- How many lines to show in the security footer --> <integer name="qs_security_footer_maxLines">1</integer> - <bool name="config_use_large_screen_shade_header">true</bool> - <!-- Whether to show bottom sheets edge to edge --> <bool name="config_edgeToEdgeBottomSheetDialog">false</bool> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 18b70731700a..fd943d0a5414 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1869,6 +1869,8 @@ <!-- Text displayed indicating that the user is connected to a satellite signal. --> <string name="satellite_connected_carrier_text">Satellite SOS</string> + <!-- Text displayed indicating that the user might be able to use satellite SOS. --> + <string name="satellite_emergency_only_carrier_text">Emergency calls or SOS</string> <!-- Accessibility label for managed profile icon (not shown on screen) [CHAR LIMIT=NONE] --> <string name="accessibility_managed_profile">Work profile</string> diff --git a/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/3.json b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/3.json new file mode 100644 index 000000000000..fe996b7d4b64 --- /dev/null +++ b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/3.json @@ -0,0 +1,81 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "02e2da2d36e6955200edd5fb49e63c72", + "entities": [ + { + "tableName": "communal_widget_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `widget_id` INTEGER NOT NULL, `component_name` TEXT NOT NULL, `item_id` INTEGER NOT NULL, `user_serial_number` INTEGER NOT NULL DEFAULT -1)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "widgetId", + "columnName": "widget_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "componentName", + "columnName": "component_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itemId", + "columnName": "item_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userSerialNumber", + "columnName": "user_serial_number", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + } + }, + { + "tableName": "communal_item_rank_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `rank` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rank", + "columnName": "rank", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '02e2da2d36e6955200edd5fb49e63c72')" + ] + } +}
\ No newline at end of file diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp index fbe139930e11..e68da09b26d1 100644 --- a/packages/SystemUI/shared/Android.bp +++ b/packages/SystemUI/shared/Android.bp @@ -48,6 +48,7 @@ android_library { "src/**/*.kt", "src/**/*.aidl", ":wm_shell-aidls", + ":wm_shell-shared-aidls", ":wm_shell_util-sources", ], static_libs: [ @@ -69,6 +70,7 @@ android_library { "dagger2", "jsr330", "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", + "//frameworks/libs/systemui:msdl", ], resource_dirs: [ "res", diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java index 4ef1f93481f7..121577e438b0 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java @@ -342,8 +342,7 @@ public class QuickStepContract { // the keyguard) if ((sysuiStateFlags & SYSUI_STATE_BOUNCER_SHOWING) != 0 || (sysuiStateFlags & SYSUI_STATE_DIALOG_SHOWING) != 0 - || (sysuiStateFlags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0 - || (sysuiStateFlags & SYSUI_STATE_COMMUNAL_HUB_SHOWING) != 0) { + || (sysuiStateFlags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0) { return false; } if ((sysuiStateFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0) { diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 5dcf1618ed6b..c1eae2e53a44 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -477,6 +477,12 @@ constructor( smallClockFrame?.viewTreeObserver?.removeOnGlobalLayoutListener(onGlobalLayoutListener) } + fun setFallbackWeatherData(data: WeatherData) { + if (weatherData != null) return + weatherData = data + clock?.run { events.onWeatherDataChanged(data) } + } + /** * Sets this clock as showing in a secondary display. * diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index bf905dbe5543..7efe2dde1320 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -344,7 +344,7 @@ public class KeyguardSecurityContainer extends ConstraintLayout { R.dimen.keyguard_security_container_padding_top), getPaddingRight(), getPaddingBottom()); setBackgroundColor(Utils.getColorAttrDefaultColor(getContext(), - com.android.internal.R.attr.materialColorSurface)); + com.android.internal.R.attr.materialColorSurfaceDim)); } void onResume(SecurityMode securityMode, boolean faceAuthEnabled) { @@ -808,7 +808,7 @@ public class KeyguardSecurityContainer extends ConstraintLayout { void reloadColors() { mViewMode.reloadColors(); setBackgroundColor(Utils.getColorAttrDefaultColor(getContext(), - com.android.internal.R.attr.materialColorSurface)); + com.android.internal.R.attr.materialColorSurfaceDim)); } /** Handles density or font scale changes. */ diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserver.java new file mode 100644 index 000000000000..c94487848b81 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserver.java @@ -0,0 +1,67 @@ +/* + * 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.accessibility; + +import android.content.Context; +import android.provider.Settings; + +import androidx.annotation.MainThread; +import androidx.annotation.Nullable; + +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.settings.UserTracker; + +import javax.inject.Inject; + +/** + * Controller for tracking the current accessibility gesture list. + * + * @see Settings.Secure#ACCESSIBILITY_GESTURE_TARGETS + */ +@MainThread +@SysUISingleton +public class AccessibilityGestureTargetsObserver extends + SecureSettingsContentObserver<AccessibilityGestureTargetsObserver.TargetsChangedListener> { + + /** Listener for accessibility gesture targets changes. */ + public interface TargetsChangedListener { + + /** + * Called when accessibility gesture targets changes. + * + * @param targets Current content of {@link Settings.Secure#ACCESSIBILITY_GESTURE_TARGETS} + */ + void onAccessibilityGestureTargetsChanged(String targets); + } + + @Inject + public AccessibilityGestureTargetsObserver(Context context, UserTracker userTracker) { + super(context, userTracker, Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS); + } + + @Override + void onValueChanged(TargetsChangedListener listener, String value) { + listener.onAccessibilityGestureTargetsChanged(value); + } + + /** Returns the current string from settings key + * {@link Settings.Secure#ACCESSIBILITY_GESTURE_TARGETS}. */ + @Nullable + public String getCurrentAccessibilityGestureTargets() { + return getSettingsValue(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/ambient/statusbar/ui/AmbientStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/ambient/statusbar/ui/AmbientStatusBarViewController.java index abdc3338b7f2..04595a2a698e 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/statusbar/ui/AmbientStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/statusbar/ui/AmbientStatusBarViewController.java @@ -48,6 +48,7 @@ import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController; import com.android.systemui.statusbar.policy.NextAlarmController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.window.StatusBarWindowStateController; +import com.android.systemui.statusbar.window.StatusBarWindowStateListener; import com.android.systemui.util.ViewController; import com.android.systemui.util.time.DateFormatUtil; @@ -127,6 +128,9 @@ public class AmbientStatusBarViewController extends ViewController<AmbientStatus private final DreamOverlayStatusBarItemsProvider.Callback mStatusBarItemsProviderCallback = this::onStatusBarItemsChanged; + private final StatusBarWindowStateListener mStatusBarWindowStateListener = + this::onSystemStatusBarStateChanged; + @Inject public AmbientStatusBarViewController( AmbientStatusBarView view, @@ -161,10 +165,22 @@ public class AmbientStatusBarViewController extends ViewController<AmbientStatus mWifiInteractor = wifiInteractor; mCommunalSceneInteractor = communalSceneInteractor; mLogger = new DreamLogger(logBuffer, TAG); + } + + @Override + protected void onInit() { + super.onInit(); // Register to receive show/hide updates for the system status bar. Our custom status bar // needs to hide when the system status bar is showing to ovoid overlapping status bars. - statusBarWindowStateController.addListener(this::onSystemStatusBarStateChanged); + mStatusBarWindowStateController.addListener(mStatusBarWindowStateListener); + } + + @Override + public void destroy() { + mStatusBarWindowStateController.removeListener(mStatusBarWindowStateListener); + + super.destroy(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java index 190bc1587525..d27e72a9c185 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java @@ -122,4 +122,9 @@ public interface TouchHandler { * @param session */ void onSessionStart(TouchSession session); + + /** + * Called when the handler is being torn down. + */ + default void onDestroy() {} } diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java index efa55e90081e..1be6f9e7ca4f 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java @@ -581,6 +581,10 @@ public class TouchMonitor { mBoundsFlow.cancel(new CancellationException()); } + for (TouchHandler handler : mHandlers) { + handler.onDestroy(); + } + mInitialized = false; } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt index 8b2449aa1ffd..a8f7fc345001 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt @@ -29,6 +29,7 @@ import androidx.annotation.VisibleForTesting import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.UiEventLogger import com.android.settingslib.bluetooth.BluetoothUtils +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast import com.android.systemui.Prefs import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator @@ -271,7 +272,7 @@ constructor( val intent = Intent(ACTION_BLUETOOTH_DEVICE_DETAILS).apply { putExtra( - ":settings:show_fragment_args", + EXTRA_SHOW_FRAGMENT_ARGUMENTS, Bundle().apply { putString("device_address", deviceItem.cachedBluetoothDevice.address) } @@ -292,7 +293,16 @@ constructor( override fun onAudioSharingButtonClicked(view: View) { uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_AUDIO_SHARING_BUTTON_CLICKED) - startSettingsActivity(Intent(ACTION_AUDIO_SHARING), view) + val intent = + Intent(ACTION_AUDIO_SHARING).apply { + putExtra( + EXTRA_SHOW_FRAGMENT_ARGUMENTS, + Bundle().apply { + putBoolean(LocalBluetoothLeBroadcast.EXTRA_START_LE_AUDIO_SHARING, true) + } + ) + } + startSettingsActivity(intent, view) } private fun cancelJob() { @@ -320,6 +330,7 @@ constructor( companion object { private const val INTERACTION_JANK_TAG = "bluetooth_tile_dialog" private const val CONTENT_HEIGHT_PREF_KEY = Prefs.Key.BLUETOOTH_TILE_DIALOG_CONTENT_HEIGHT + private const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args" private fun getSubtitleResId(isBluetoothEnabled: Boolean) = if (isBluetoothEnabled) R.string.quick_settings_bluetooth_tile_subtitle diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt index 85fffbf610fc..d125c36cd8db 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt @@ -33,9 +33,7 @@ import com.android.systemui.bouncer.shared.model.BouncerMessageStrings import com.android.systemui.bouncer.shared.model.Message import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository import com.android.systemui.deviceentry.domain.interactor.DeviceEntryBiometricsAllowedInteractor -import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor import com.android.systemui.flags.SystemPropertiesHelper import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository import com.android.systemui.keyguard.data.repository.TrustRepository @@ -75,8 +73,6 @@ constructor( primaryBouncerInteractor: PrimaryBouncerInteractor, @Application private val applicationScope: CoroutineScope, private val facePropertyRepository: FacePropertyRepository, - private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, - faceAuthRepository: DeviceEntryFaceAuthRepository, private val securityModel: KeyguardSecurityModel, deviceEntryBiometricsAllowedInteractor: DeviceEntryBiometricsAllowedInteractor, ) { @@ -97,6 +93,17 @@ constructor( private val kumCallback = object : KeyguardUpdateMonitorCallback() { override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType?) { + // Only show the biometric failure messages if the biometric is NOT locked out. + // If the biometric is locked out, rely on the lock out message to show + // the lockout message & don't override it with the failure message. + if ( + (biometricSourceType == BiometricSourceType.FACE && + deviceEntryBiometricsAllowedInteractor.isFaceLockedOut.value) || + (biometricSourceType == BiometricSourceType.FINGERPRINT && + deviceEntryBiometricsAllowedInteractor.isFingerprintLockedOut.value) + ) { + return + } repository.setMessage( when (biometricSourceType) { BiometricSourceType.FINGERPRINT -> @@ -159,8 +166,8 @@ constructor( biometricSettingsRepository.authenticationFlags, trustRepository.isCurrentUserTrustManaged, isAnyBiometricsEnabledAndEnrolled, - deviceEntryFingerprintAuthInteractor.isLockedOut, - faceAuthRepository.isLockedOut, + deviceEntryBiometricsAllowedInteractor.isFingerprintLockedOut, + deviceEntryBiometricsAllowedInteractor.isFaceLockedOut, isFingerprintAuthCurrentlyAllowedOnBouncer, ::Septuple ) diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt index df50e8fdb90b..abca518745d1 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt @@ -21,6 +21,7 @@ import com.android.app.tracing.coroutines.flow.collectLatest import com.android.systemui.authentication.domain.interactor.AuthenticationResult import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.SysUiViewModel import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.channels.Channel @@ -39,7 +40,7 @@ sealed class AuthMethodBouncerViewModel( * being able to attempt to unlock the device. */ val isInputEnabled: StateFlow<Boolean>, -) : SysUiViewModel() { +) : SysUiViewModel, ExclusiveActivatable() { private val _animateFailure = MutableStateFlow(false) /** diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt index cfd4f506f169..d21eccdfb047 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt @@ -38,6 +38,7 @@ import com.android.systemui.deviceentry.shared.model.FaceLockoutMessage import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage import com.android.systemui.deviceentry.shared.model.FingerprintFailureMessage import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel @@ -79,7 +80,7 @@ constructor( private val deviceUnlockedInteractor: DeviceUnlockedInteractor, private val deviceEntryBiometricsAllowedInteractor: DeviceEntryBiometricsAllowedInteractor, private val flags: ComposeBouncerFlags, -) : SysUiViewModel() { +) : SysUiViewModel, ExclusiveActivatable() { /** * A message shown when the user has attempted the wrong credential too many times and now must * wait a while before attempting to authenticate again. diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt index 63b6f0193502..79e5f8d4a683 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt @@ -33,6 +33,7 @@ import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import dagger.assisted.AssistedFactory @@ -62,7 +63,7 @@ constructor( private val pinViewModelFactory: PinBouncerViewModel.Factory, private val patternViewModelFactory: PatternBouncerViewModel.Factory, private val passwordViewModelFactory: PasswordBouncerViewModel.Factory, -) : SysUiViewModel() { +) : SysUiViewModel, ExclusiveActivatable() { private val _selectedUserImage = MutableStateFlow<Bitmap?>(null) val selectedUserImage: StateFlow<Bitmap?> = _selectedUserImage.asStateFlow() diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/EditTextActivity.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/EditTextActivity.java index a43447f7fcf4..aae21b97b163 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/EditTextActivity.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/EditTextActivity.java @@ -66,7 +66,8 @@ public class EditTextActivity extends Activity @Override public WindowInsets onApplyWindowInsets(@NonNull View view, @NonNull WindowInsets windowInsets) { - Insets insets = windowInsets.getInsets(WindowInsets.Type.systemBars()); + Insets insets = windowInsets.getInsets( + WindowInsets.Type.systemBars() | WindowInsets.Type.ime()); ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); layoutParams.leftMargin = insets.left; diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt index 46db34618c70..208adc22a3e0 100644 --- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt +++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt @@ -18,6 +18,7 @@ package com.android.systemui.common.data.repository import android.content.pm.PackageInstaller import android.os.Handler +import android.text.TextUtils import com.android.internal.annotations.GuardedBy import com.android.systemui.common.shared.model.PackageInstallSession import com.android.systemui.dagger.SysUISingleton @@ -63,6 +64,7 @@ constructor( synchronized(sessions) { sessions.putAll( packageInstaller.allSessions + .filter { !TextUtils.isEmpty(it.appPackageName) } .map { session -> session.toModel() } .associateBy { it.sessionId } ) diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt index dff63527ba05..8f1854f93fe4 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt @@ -26,7 +26,7 @@ import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import com.android.systemui.res.R -@Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 2) +@Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 3) abstract class CommunalDatabase : RoomDatabase() { abstract fun communalWidgetDao(): CommunalWidgetDao @@ -55,7 +55,7 @@ abstract class CommunalDatabase : RoomDatabase() { context.resources.getString(R.string.config_communalDatabase) ) .also { builder -> - builder.addMigrations(MIGRATION_1_2) + builder.addMigrations(MIGRATION_1_2, MIGRATION_2_3) builder.fallbackToDestructiveMigration(dropAllTables = true) callback?.let { callback -> builder.addCallback(callback) } } @@ -87,5 +87,21 @@ abstract class CommunalDatabase : RoomDatabase() { ) } } + + /** + * This migration reverses the ranks. For example, if the ranks are 2, 1, 0, then after the + * migration they will be 0, 1, 2. + */ + @VisibleForTesting + val MIGRATION_2_3 = + object : Migration(2, 3) { + override fun migrate(db: SupportSQLiteDatabase) { + Log.i(TAG, "Migrating from version 2 to 3") + db.execSQL( + "UPDATE communal_item_rank_table " + + "SET rank = (SELECT MAX(rank) FROM communal_item_rank_table) - rank" + ) + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt index 933a25a21578..93b86bd10133 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt @@ -97,7 +97,7 @@ constructor( .addWidget( widgetId = id, componentName = name, - priority = defaultWidgets.size - index, + rank = index, userSerialNumber = userSerialNumber, ) } @@ -132,10 +132,17 @@ interface CommunalWidgetDao { @Query( "SELECT * FROM communal_widget_table JOIN communal_item_rank_table " + "ON communal_item_rank_table.uid = communal_widget_table.item_id " + - "ORDER BY communal_item_rank_table.rank DESC" + "ORDER BY communal_item_rank_table.rank ASC" ) fun getWidgets(): Flow<Map<CommunalItemRank, CommunalWidgetItem>> + @Query( + "SELECT * FROM communal_widget_table JOIN communal_item_rank_table " + + "ON communal_item_rank_table.uid = communal_widget_table.item_id " + + "ORDER BY communal_item_rank_table.rank ASC" + ) + fun getWidgetsNow(): Map<CommunalItemRank, CommunalWidgetItem> + @Query("SELECT * FROM communal_widget_table WHERE widget_id = :id") fun getWidgetByIdNow(id: Int): CommunalWidgetItem? @@ -167,11 +174,11 @@ interface CommunalWidgetDao { @Query("DELETE FROM communal_item_rank_table") fun clearCommunalItemRankTable() @Transaction - fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) { - widgetIdToPriorityMap.forEach { (id, priority) -> + fun updateWidgetOrder(widgetIdToRankMap: Map<Int, Int>) { + widgetIdToRankMap.forEach { (id, rank) -> val widget = getWidgetByIdNow(id) if (widget != null) { - updateItemRank(widget.itemId, priority) + updateItemRank(widget.itemId, rank) } } } @@ -180,13 +187,13 @@ interface CommunalWidgetDao { fun addWidget( widgetId: Int, provider: ComponentName, - priority: Int, + rank: Int? = null, userSerialNumber: Int, ): Long { return addWidget( widgetId = widgetId, componentName = provider.flattenToString(), - priority = priority, + rank = rank, userSerialNumber = userSerialNumber, ) } @@ -195,13 +202,27 @@ interface CommunalWidgetDao { fun addWidget( widgetId: Int, componentName: String, - priority: Int, + rank: Int? = null, userSerialNumber: Int, ): Long { + val widgets = getWidgetsNow() + + // If rank is not specified, rank it last by finding the current maximum rank and increment + // by 1. If the new widget is the first widget, set the rank to 0. + val newRank = rank ?: widgets.keys.maxOfOrNull { it.rank + 1 } ?: 0 + + // Shift widgets after [rank], unless widget is added at the end. + if (rank != null) { + widgets.forEach { (rankEntry, widgetEntry) -> + if (rankEntry.rank < newRank) return@forEach + updateItemRank(widgetEntry.itemId, rankEntry.rank + 1) + } + } + return insertWidget( widgetId = widgetId, componentName = componentName, - itemId = insertItemRank(priority), + itemId = insertItemRank(newRank), userSerialNumber = userSerialNumber, ) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt index ad0bfc76ebbf..6cdd9fffe077 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt @@ -57,12 +57,17 @@ interface CommunalWidgetRepository { /** A flow of information about active communal widgets stored in database. */ val communalWidgets: Flow<List<CommunalWidgetContentModel>> - /** Add a widget at the specified position in the app widget service and the database. */ + /** + * Add a widget in the app widget service and the database. + * + * @param rank The rank of the widget determines its position in the grid. 0 is first place, 1 + * is second, etc. If rank is not specified, widget is added at the end. + */ fun addWidget( provider: ComponentName, user: UserHandle, - priority: Int, - configurator: WidgetConfigurator? = null + rank: Int?, + configurator: WidgetConfigurator? = null, ) {} /** @@ -75,9 +80,9 @@ interface CommunalWidgetRepository { /** * Update the order of widgets in the database. * - * @param widgetIdToPriorityMap mapping of the widget ids to the priority of the widget. + * @param widgetIdToRankMap mapping of the widget ids to the rank of the widget. */ - fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) {} + fun updateWidgetOrder(widgetIdToRankMap: Map<Int, Int>) {} /** * Restores the database by reading a state file from disk and updating the widget ids according @@ -121,7 +126,7 @@ constructor( CommunalWidgetEntry( appWidgetId = widget.widgetId, componentName = widget.componentName, - priority = rank.rank, + rank = rank.rank, providerInfo = providers[widget.widgetId] ) } @@ -151,8 +156,8 @@ constructor( override fun addWidget( provider: ComponentName, user: UserHandle, - priority: Int, - configurator: WidgetConfigurator? + rank: Int?, + configurator: WidgetConfigurator?, ) { bgScope.launch { val id = communalWidgetHost.allocateIdAndBindWidget(provider, user) @@ -190,14 +195,14 @@ constructor( communalWidgetDao.addWidget( widgetId = id, provider = provider, - priority = priority, + rank = rank, userSerialNumber = userManager.getUserSerialNumber(user.identifier), ) backupManager.dataChanged() } else { appWidgetHost.deleteAppWidgetId(id) } - logger.i("Added widget ${provider.flattenToString()} at position $priority.") + logger.i("Added widget ${provider.flattenToString()} at position $rank.") } } @@ -211,11 +216,11 @@ constructor( } } - override fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) { + override fun updateWidgetOrder(widgetIdToRankMap: Map<Int, Int>) { bgScope.launch { - communalWidgetDao.updateWidgetOrder(widgetIdToPriorityMap) + communalWidgetDao.updateWidgetOrder(widgetIdToRankMap) logger.i({ "Updated the order of widget list with ids: $str1." }) { - str1 = widgetIdToPriorityMap.toString() + str1 = widgetIdToRankMap.toString() } backupManager.dataChanged() } @@ -342,7 +347,7 @@ constructor( addWidget( provider = ComponentName.unflattenFromString(widget.componentName)!!, user = newUser, - priority = widget.rank, + rank = widget.rank, ) } @@ -377,7 +382,7 @@ constructor( return CommunalWidgetContentModel.Available( appWidgetId = entry.appWidgetId, providerInfo = entry.providerInfo!!, - priority = entry.priority, + rank = entry.rank, ) } @@ -394,7 +399,7 @@ constructor( return CommunalWidgetContentModel.Available( appWidgetId = entry.appWidgetId, providerInfo = entry.providerInfo!!, - priority = entry.priority, + rank = entry.rank, ) } @@ -403,7 +408,7 @@ constructor( return if (componentName != null && session != null) { CommunalWidgetContentModel.Pending( appWidgetId = entry.appWidgetId, - priority = entry.priority, + rank = entry.rank, componentName = componentName, icon = session.icon, user = session.user, @@ -416,7 +421,7 @@ constructor( private data class CommunalWidgetEntry( val appWidgetId: Int, val componentName: String, - val priority: Int, + val rank: Int, var providerInfo: AppWidgetProviderInfo? = null, ) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 7181b15138b9..98abbebd1951 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -140,6 +140,10 @@ constructor( */ val editActivityShowing: StateFlow<Boolean> = _editActivityShowing.asStateFlow() + private val _selectedKey: MutableStateFlow<String?> = MutableStateFlow(null) + + val selectedKey: StateFlow<String?> = _selectedKey.asStateFlow() + /** Whether communal features are enabled. */ val isCommunalEnabled: StateFlow<Boolean> = communalSettingsInteractor.isCommunalEnabled @@ -179,6 +183,10 @@ constructor( } } + fun setSelectedKey(key: String?) { + _selectedKey.value = key + } + /** Whether to show communal when exiting the occluded state. */ val showCommunalFromOccluded: Flow<Boolean> = keyguardTransitionInteractor.startedKeyguardTransitionStep @@ -345,11 +353,10 @@ constructor( /** Show the widget editor Activity. */ fun showWidgetEditor( - preselectedKey: String? = null, shouldOpenWidgetPickerOnStart: Boolean = false, ) { communalSceneInteractor.setEditModeState(EditModeState.STARTING) - editWidgetsActivityStarter.startActivity(preselectedKey, shouldOpenWidgetPickerOnStart) + editWidgetsActivityStarter.startActivity(shouldOpenWidgetPickerOnStart) } /** @@ -367,13 +374,16 @@ constructor( /** Dismiss the CTA tile from the hub in view mode. */ suspend fun dismissCtaTile() = communalPrefsInteractor.setCtaDismissed() - /** Add a widget at the specified position. */ + /** + * Add a widget at the specified rank. If rank is not provided, the widget will be added at the + * end. + */ fun addWidget( componentName: ComponentName, user: UserHandle, - priority: Int, + rank: Int? = null, configurator: WidgetConfigurator?, - ) = widgetRepository.addWidget(componentName, user, priority, configurator) + ) = widgetRepository.addWidget(componentName, user, rank, configurator) /** * Delete a widget by id. Called when user deletes a widget from the hub or a widget is @@ -384,10 +394,10 @@ constructor( /** * Reorder the widgets. * - * @param widgetIdToPriorityMap mapping of the widget ids to their new priorities. + * @param widgetIdToRankMap mapping of the widget ids to their new priorities. */ - fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) = - widgetRepository.updateWidgetOrder(widgetIdToPriorityMap) + fun updateWidgetOrder(widgetIdToRankMap: Map<Int, Int>) = + widgetRepository.updateWidgetOrder(widgetIdToRankMap) /** Request to unpause work profile that is currently in quiet mode. */ fun unpauseWorkProfile() { @@ -440,7 +450,7 @@ constructor( is CommunalWidgetContentModel.Available -> { WidgetContent.Widget( appWidgetId = widget.appWidgetId, - priority = widget.priority, + rank = widget.rank, providerInfo = widget.providerInfo, appWidgetHost = appWidgetHost, inQuietMode = isQuietModeEnabled(widget.providerInfo.profile) @@ -449,7 +459,7 @@ constructor( is CommunalWidgetContentModel.Pending -> { WidgetContent.PendingWidget( appWidgetId = widget.appWidgetId, - priority = widget.priority, + rank = widget.rank, componentName = widget.componentName, icon = widget.icon, ) @@ -604,11 +614,6 @@ constructor( _firstVisibleItemOffset = firstVisibleItemOffset } - fun resetScrollPosition() { - _firstVisibleItemIndex = 0 - _firstVisibleItemOffset = 0 - } - val firstVisibleItemIndex: Int get() = _firstVisibleItemIndex diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt index a0b996675331..8f756a23a9da 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt @@ -88,6 +88,7 @@ constructor( keyguardState: KeyguardState? = null, ) { applicationScope.launch("$TAG#changeScene") { + if (currentScene.value == newScene) return@launch logger.logSceneChangeRequested( from = currentScene.value, to = newScene, @@ -108,6 +109,7 @@ constructor( ) { applicationScope.launch("$TAG#snapToScene") { delay(delayMillis) + if (currentScene.value == newScene) return@launch logger.logSceneChangeRequested( from = currentScene.value, to = newScene, diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt index 73c6ce30b202..4c821d482eef 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt @@ -47,12 +47,12 @@ sealed interface CommunalContentModel { sealed interface WidgetContent : CommunalContentModel { val appWidgetId: Int - val priority: Int + val rank: Int val componentName: ComponentName data class Widget( override val appWidgetId: Int, - override val priority: Int, + override val rank: Int, val providerInfo: AppWidgetProviderInfo, val appWidgetHost: CommunalAppWidgetHost, val inQuietMode: Boolean, @@ -71,7 +71,7 @@ sealed interface CommunalContentModel { data class DisabledWidget( override val appWidgetId: Int, - override val priority: Int, + override val rank: Int, val providerInfo: AppWidgetProviderInfo ) : WidgetContent { override val key = KEY.disabledWidget(appWidgetId) @@ -85,7 +85,7 @@ sealed interface CommunalContentModel { data class PendingWidget( override val appWidgetId: Int, - override val priority: Int, + override val rank: Int, override val componentName: ComponentName, val icon: Bitmap? = null, ) : WidgetContent { diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalMetricsLogger.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalMetricsLogger.kt index 9ce8cf72983a..7cfad60b84cd 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalMetricsLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalMetricsLogger.kt @@ -31,7 +31,7 @@ constructor( private val statsLogProxy: StatsLogProxy, ) { /** Logs an add widget event for metrics. No-op if widget is not loggable. */ - fun logAddWidget(componentName: String, rank: Int) { + fun logAddWidget(componentName: String, rank: Int?) { if (!componentName.isLoggable()) { return } @@ -39,7 +39,7 @@ constructor( statsLogProxy.writeCommunalHubWidgetEventReported( SysUiStatsLog.COMMUNAL_HUB_WIDGET_EVENT_REPORTED__ACTION__ADD, componentName, - rank, + rank ?: -1, ) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt index 7cddb7226601..63b1a14b3135 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt @@ -24,19 +24,19 @@ import android.os.UserHandle /** Encapsulates data for a communal widget. */ sealed interface CommunalWidgetContentModel { val appWidgetId: Int - val priority: Int + val rank: Int /** Widget is ready to display */ data class Available( override val appWidgetId: Int, val providerInfo: AppWidgetProviderInfo, - override val priority: Int, + override val rank: Int, ) : CommunalWidgetContentModel /** Widget is pending installation */ data class Pending( override val appWidgetId: Int, - override val priority: Int, + override val rank: Int, val componentName: ComponentName, val icon: Bitmap?, val user: UserHandle, diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index b8221332eadf..0929d3e1bda9 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -55,11 +55,8 @@ abstract class BaseCommunalViewModel( /** Whether widgets are currently being re-ordered. */ open val reorderingWidgets: StateFlow<Boolean> = MutableStateFlow(false) - private val _selectedKey: MutableStateFlow<String?> = MutableStateFlow(null) - /** The key of the currently selected item, or null if no item selected. */ - val selectedKey: StateFlow<String?> - get() = _selectedKey + val selectedKey: StateFlow<String?> = communalInteractor.selectedKey private val _isTouchConsumed: MutableStateFlow<Boolean> = MutableStateFlow(false) @@ -153,7 +150,7 @@ abstract class BaseCommunalViewModel( open fun onAddWidget( componentName: ComponentName, user: UserHandle, - priority: Int, + rank: Int? = null, configurator: WidgetConfigurator? = null, ) {} @@ -161,23 +158,23 @@ abstract class BaseCommunalViewModel( open fun onDeleteWidget( id: Int, componentName: ComponentName, - priority: Int, + rank: Int, ) {} /** Called as the UI detects a tap event on the widget. */ open fun onTapWidget( componentName: ComponentName, - priority: Int, + rank: Int, ) {} /** * Called as the UI requests reordering widgets. * - * @param widgetIdToPriorityMap mapping of the widget ids to its priority. When re-ordering to - * add a new item in the middle, provide the priorities of existing widgets as if the new item - * existed, and then, call [onAddWidget] to add the new item at intended order. + * @param widgetIdToRankMap mapping of the widget ids to its rank. When re-ordering to add a new + * item in the middle, provide the priorities of existing widgets as if the new item existed, + * and then, call [onAddWidget] to add the new item at intended order. */ - open fun onReorderWidgets(widgetIdToPriorityMap: Map<Int, Int>) {} + open fun onReorderWidgets(widgetIdToRankMap: Map<Int, Int>) {} /** Called as the UI requests opening the widget editor with an optional preselected widget. */ open fun onOpenWidgetEditor( @@ -226,7 +223,7 @@ abstract class BaseCommunalViewModel( /** Set the key of the currently selected item */ fun setSelectedKey(key: String?) { - _selectedKey.value = key + communalInteractor.setSelectedKey(key) } /** Invoked once touches inside the lazy grid are consumed */ diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index 1a86c717b962..16788d15b269 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -125,24 +125,24 @@ constructor( override fun onAddWidget( componentName: ComponentName, user: UserHandle, - priority: Int, + rank: Int?, configurator: WidgetConfigurator? ) { - communalInteractor.addWidget(componentName, user, priority, configurator) - metricsLogger.logAddWidget(componentName.flattenToString(), priority) + communalInteractor.addWidget(componentName, user, rank, configurator) + metricsLogger.logAddWidget(componentName.flattenToString(), rank) } override fun onDeleteWidget( id: Int, componentName: ComponentName, - priority: Int, + rank: Int, ) { communalInteractor.deleteWidget(id) - metricsLogger.logRemoveWidget(componentName.flattenToString(), priority) + metricsLogger.logRemoveWidget(componentName.flattenToString(), rank) } - override fun onReorderWidgets(widgetIdToPriorityMap: Map<Int, Int>) = - communalInteractor.updateWidgetOrder(widgetIdToPriorityMap) + override fun onReorderWidgets(widgetIdToRankMap: Map<Int, Int>) = + communalInteractor.updateWidgetOrder(widgetIdToRankMap) override fun onReorderWidgetStart() { // Clear selection status diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index c0a18f29149d..5a39a6272c94 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -262,7 +262,7 @@ constructor( shouldOpenWidgetPickerOnStart: Boolean, ) { persistScrollPosition() - communalInteractor.showWidgetEditor(selectedKey.value, shouldOpenWidgetPickerOnStart) + communalInteractor.showWidgetEditor(shouldOpenWidgetPickerOnStart) } override fun onDismissCtaTile() { @@ -272,8 +272,8 @@ constructor( } } - override fun onTapWidget(componentName: ComponentName, priority: Int) { - metricsLogger.logTapWidget(componentName.flattenToString(), priority) + override fun onTapWidget(componentName: ComponentName, rank: Int) { + metricsLogger.logTapWidget(componentName.flattenToString(), rank) } fun onClick() { diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index b421e5932352..55a24d0f595a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -16,7 +16,10 @@ package com.android.systemui.communal.widgets +import android.app.Activity +import android.app.Application.ActivityLifecycleCallbacks import android.content.Intent +import android.content.IntentSender import android.os.Bundle import android.os.RemoteException import android.util.Log @@ -34,6 +37,7 @@ import androidx.lifecycle.lifecycleScope import com.android.compose.theme.LocalAndroidColorScheme import com.android.compose.theme.PlatformTheme import com.android.internal.logging.UiEventLogger +import com.android.systemui.Flags.communalEditWidgetsActivityFinishFix import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.CommunalTransitionKeys @@ -64,16 +68,109 @@ constructor( companion object { private const val TAG = "EditWidgetsActivity" private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag" - const val EXTRA_PRESELECTED_KEY = "preselected_key" const val EXTRA_OPEN_WIDGET_PICKER_ON_START = "open_widget_picker_on_start" } + /** + * [ActivityController] handles closing the activity in the case it is backgrounded without + * waiting for an activity result + */ + interface ActivityController { + /** + * Invoked when waiting for an activity result changes, either initiating such wait or + * finishing due to the return of a result. + */ + fun onWaitingForResult(waitingForResult: Boolean) {} + + /** Set the visibility of the activity under control. */ + fun setActivityFullyVisible(fullyVisible: Boolean) {} + } + + /** + * A nop ActivityController to be use when the communalEditWidgetsActivityFinishFix flag is + * false. + */ + class NopActivityController : ActivityController + + /** + * A functional ActivityController to be used when the communalEditWidgetsActivityFinishFix flag + * is true. + */ + class ActivityControllerImpl(activity: Activity) : ActivityController { + companion object { + private const val STATE_EXTRA_IS_WAITING_FOR_RESULT = "extra_is_waiting_for_result" + } + + private var waitingForResult = false + private var activityFullyVisible = false + + init { + activity.registerActivityLifecycleCallbacks( + object : ActivityLifecycleCallbacks { + override fun onActivityCreated( + activity: Activity, + savedInstanceState: Bundle? + ) { + waitingForResult = + savedInstanceState?.getBoolean(STATE_EXTRA_IS_WAITING_FOR_RESULT) + ?: false + } + + override fun onActivityStarted(activity: Activity) { + // Nothing to implement. + } + + override fun onActivityResumed(activity: Activity) { + // Nothing to implement. + } + + override fun onActivityPaused(activity: Activity) { + // Nothing to implement. + } + + override fun onActivityStopped(activity: Activity) { + // If we're not backgrounded due to waiting for a result (either widget + // selection or configuration), and we are fully visible, then finish the + // activity. + if ( + !waitingForResult && + activityFullyVisible && + !activity.isChangingConfigurations + ) { + activity.finish() + } + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + outState.putBoolean(STATE_EXTRA_IS_WAITING_FOR_RESULT, waitingForResult) + } + + override fun onActivityDestroyed(activity: Activity) { + // Nothing to implement. + } + } + ) + } + + override fun onWaitingForResult(waitingForResult: Boolean) { + this.waitingForResult = waitingForResult + } + + override fun setActivityFullyVisible(fullyVisible: Boolean) { + activityFullyVisible = fullyVisible + } + } + private val logger = Logger(logBuffer, "EditWidgetsActivity") private val widgetConfigurator by lazy { widgetConfiguratorFactory.create(this) } private var shouldOpenWidgetPickerOnStart = false + private val activityController: ActivityController = + if (communalEditWidgetsActivityFinishFix()) ActivityControllerImpl(this) + else NopActivityController() + private val addWidgetActivityLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(StartActivityForResult()) { result -> when (result.resultCode) { @@ -89,11 +186,11 @@ constructor( if (!isPendingWidgetDrag) { val (componentName, user) = getWidgetExtraFromIntent(intent) if (componentName != null && user != null) { + // Add widget at the end. communalViewModel.onAddWidget( componentName, user, - 0, - widgetConfigurator + configurator = widgetConfigurator, ) } else { run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") } @@ -111,20 +208,19 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + listenForTransitionAndChangeScene() + activityController.setActivityFullyVisible(false) communalViewModel.setEditModeOpen(true) val windowInsetsController = window.decorView.windowInsetsController windowInsetsController?.hide(WindowInsets.Type.systemBars()) window.setDecorFitsSystemWindows(false) - val preselectedKey = intent.getStringExtra(EXTRA_PRESELECTED_KEY) shouldOpenWidgetPickerOnStart = intent.getBooleanExtra(EXTRA_OPEN_WIDGET_PICKER_ON_START, false) - communalViewModel.setSelectedKey(preselectedKey) - setContent { PlatformTheme { Box( @@ -159,6 +255,9 @@ constructor( communalViewModel.currentScene.first { it == CommunalScenes.Blank } communalViewModel.setEditModeState(EditModeState.SHOWING) + // Inform the ActivityController that we are now fully visible. + activityController.setActivityFullyVisible(true) + // Show the widget picker, if necessary, after the edit activity has animated in. // Waiting until after the activity has appeared avoids transitions issues. if (shouldOpenWidgetPickerOnStart) { @@ -198,7 +297,34 @@ constructor( } } + override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) { + activityController.onWaitingForResult(true) + super.startActivityForResult(intent, requestCode, options) + } + + override fun startIntentSenderForResult( + intent: IntentSender, + requestCode: Int, + fillInIntent: Intent?, + flagsMask: Int, + flagsValues: Int, + extraFlags: Int, + options: Bundle? + ) { + activityController.onWaitingForResult(true) + super.startIntentSenderForResult( + intent, + requestCode, + fillInIntent, + flagsMask, + flagsValues, + extraFlags, + options + ) + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + activityController.onWaitingForResult(false) super.onActivityResult(requestCode, resultCode, data) if (requestCode == WidgetConfigurationController.REQUEST_CODE) { widgetConfigurator.setConfigurationResult(resultCode) diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt index af87f09d3c89..63121a83c522 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt @@ -19,7 +19,6 @@ package com.android.systemui.communal.widgets import android.content.Context import android.content.Intent import com.android.systemui.communal.widgets.EditWidgetsActivity.Companion.EXTRA_OPEN_WIDGET_PICKER_ON_START -import com.android.systemui.communal.widgets.EditWidgetsActivity.Companion.EXTRA_PRESELECTED_KEY import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R @@ -27,7 +26,6 @@ import javax.inject.Inject interface EditWidgetsActivityStarter { fun startActivity( - preselectedKey: String? = null, shouldOpenWidgetPickerOnStart: Boolean = false, ) } @@ -39,12 +37,11 @@ constructor( private val activityStarter: ActivityStarter, ) : EditWidgetsActivityStarter { - override fun startActivity(preselectedKey: String?, shouldOpenWidgetPickerOnStart: Boolean) { + override fun startActivity(shouldOpenWidgetPickerOnStart: Boolean) { activityStarter.startActivityDismissingKeyguard( Intent(applicationContext, EditWidgetsActivity::class.java) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) .apply { - preselectedKey?.let { putExtra(EXTRA_PRESELECTED_KEY, preselectedKey) } putExtra(EXTRA_OPEN_WIDGET_PICKER_ON_START, shouldOpenWidgetPickerOnStart) }, /* onlyProvisioned = */ true, diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 0363a684ec67..411cbd511a22 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -66,6 +66,7 @@ import com.android.systemui.education.dagger.ContextualEducationModule; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.FlagDependenciesModule; import com.android.systemui.flags.FlagsModule; +import com.android.systemui.haptics.msdl.dagger.MSDLModule; import com.android.systemui.inputmethod.InputMethodModule; import com.android.systemui.keyboard.KeyboardModule; import com.android.systemui.keyevent.data.repository.KeyEventRepositoryModule; @@ -231,6 +232,7 @@ import javax.inject.Named; MediaProjectionTaskSwitcherModule.class, MediaRouterModule.class, MotionToolModule.class, + MSDLModule.class, PeopleHubModule.class, PeopleModule.class, PluginModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt index 9460eaf8abca..d288ccee2ae8 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt @@ -57,7 +57,10 @@ import com.android.systemui.log.FaceAuthenticationLogger import com.android.systemui.log.SessionTracker import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.shared.model.Scenes.Bouncer import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.repository.UserRepository @@ -159,6 +162,7 @@ constructor( private val powerInteractor: PowerInteractor, private val keyguardInteractor: KeyguardInteractor, private val alternateBouncerInteractor: AlternateBouncerInteractor, + private val sceneInteractor: dagger.Lazy<SceneInteractor>, @FaceDetectTableLog private val faceDetectLog: TableLogBuffer, @FaceAuthTableLog private val faceAuthLog: TableLogBuffer, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, @@ -385,7 +389,16 @@ constructor( biometricSettingsRepository.isFaceAuthEnrolledAndEnabled, "isFaceAuthEnrolledAndEnabled" ), - Pair(keyguardRepository.isKeyguardGoingAway.isFalse(), "keyguardNotGoingAway"), + Pair( + if (SceneContainerFlag.isEnabled) { + keyguardTransitionInteractor + .isInTransitionWhere(toStatePredicate = { it == KeyguardState.UNDEFINED }) + .isFalse() + } else { + keyguardRepository.isKeyguardGoingAway.isFalse() + }, + "keyguardNotGoingAway" + ), Pair( keyguardTransitionInteractor .isInTransitionWhere(toStatePredicate = KeyguardState::deviceIsAsleepInState) @@ -397,7 +410,11 @@ constructor( .isFalse() .or( alternateBouncerInteractor.isVisible.or( - keyguardInteractor.primaryBouncerShowing + if (SceneContainerFlag.isEnabled) { + sceneInteractor.get().transitionState.map { it.isIdle(Bouncer) } + } else { + keyguardInteractor.primaryBouncerShowing + } ) ), "secureCameraNotActiveOrAnyBouncerIsShowing" diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricsAllowedInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricsAllowedInteractor.kt index 79b176c8af01..7aee12f70f41 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricsAllowedInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryBiometricsAllowedInteractor.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -44,18 +45,30 @@ constructor( biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor, facePropertyRepository: FacePropertyRepository, ) { + /** + * Whether face is locked out due to too many failed face attempts. This currently includes + * whether face is not allowed based on other biometric lockouts; however does not include if + * face isn't allowed due to other strong or primary authentication requirements. + */ + val isFaceLockedOut: StateFlow<Boolean> = deviceEntryFaceAuthInteractor.isLockedOut private val isStrongFaceAuth: Flow<Boolean> = facePropertyRepository.sensorInfo.map { it?.strength == SensorStrength.STRONG } private val isStrongFaceAuthLockedOut: Flow<Boolean> = - combine(isStrongFaceAuth, deviceEntryFaceAuthInteractor.isLockedOut) { - isStrongFaceAuth, - isFaceAuthLockedOut -> + combine(isStrongFaceAuth, isFaceLockedOut) { isStrongFaceAuth, isFaceAuthLockedOut -> isStrongFaceAuth && isFaceAuthLockedOut } /** + * Whether fingerprint is locked out due to too many failed fingerprint attempts. This does NOT + * include whether fingerprint is not allowed based on other biometric lockouts nor if + * fingerprint isn't allowed due to other strong or primary authentication requirements. + */ + val isFingerprintLockedOut: StateFlow<Boolean> = + deviceEntryFingerprintAuthInteractor.isLockedOut + + /** * Whether fingerprint authentication is currently allowed for the user. This is true if the * user has fingerprint auth enabled, enrolled, it is not disabled by any security timeouts by * [com.android.systemui.keyguard.shared.model.AuthenticationFlags], not locked out due to too @@ -64,7 +77,7 @@ constructor( */ val isFingerprintAuthCurrentlyAllowed: Flow<Boolean> = combine( - deviceEntryFingerprintAuthInteractor.isLockedOut, + isFingerprintLockedOut, biometricSettingsInteractor.fingerprintAuthCurrentlyAllowed, isStrongFaceAuthLockedOut, ) { fpLockedOut, fpAllowedBySettings, strongAuthFaceAuthLockedOut -> diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt index 969f53f23f5d..5c058fe92ccb 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt @@ -54,7 +54,7 @@ constructor( val authenticationStatus: Flow<FingerprintAuthenticationStatus> = repository.authenticationStatus - val isLockedOut: Flow<Boolean> = repository.isLockedOut + val isLockedOut: StateFlow<Boolean> = repository.isLockedOut val fingerprintFailure: Flow<FailFingerprintAuthenticationStatus> = repository.authenticationStatus.filterIsInstance<FailFingerprintAuthenticationStatus>() diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt index c536d6b4f6f8..183e0e96e765 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt @@ -46,6 +46,9 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.log.FaceAuthenticationLogger import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.res.R +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.kotlin.pairwise @@ -90,6 +93,7 @@ constructor( private val powerInteractor: PowerInteractor, private val biometricSettingsRepository: BiometricSettingsRepository, private val trustManager: TrustManager, + private val sceneInteractor: Lazy<SceneInteractor>, deviceEntryFaceAuthStatusInteractor: DeviceEntryFaceAuthStatusInteractor, ) : DeviceEntryFaceAuthInteractor { @@ -103,9 +107,7 @@ constructor( keyguardUpdateMonitor.setFaceAuthInteractor(this) observeFaceAuthStateUpdates() faceAuthenticationLogger.interactorStarted() - primaryBouncerInteractor - .get() - .isShowing + isBouncerVisible .whenItFlipsToTrue() .onEach { faceAuthenticationLogger.bouncerVisibilityChanged() @@ -181,19 +183,23 @@ constructor( // auth so that the switched user can unlock the device with face auth. userRepository.selectedUser .pairwise() - .onEach { (previous, curr) -> + .filter { (previous, curr) -> val wasSwitching = previous.selectionStatus == SelectionStatus.SELECTION_IN_PROGRESS val isSwitching = curr.selectionStatus == SelectionStatus.SELECTION_IN_PROGRESS - if (wasSwitching && !isSwitching) { - resetLockedOutState(curr.userInfo.id) - yield() - runFaceAuth( - FaceAuthUiEvent.FACE_AUTH_UPDATED_USER_SWITCHING, - // Fallback to detection if bouncer is not showing so that we can detect a - // face and then show the bouncer to the user if face auth can't run - fallbackToDetect = !primaryBouncerInteractor.get().isBouncerShowing() - ) - } + // User switching was in progress and is complete now. + wasSwitching && !isSwitching + } + .map { (_, curr) -> curr.userInfo.id } + .sample(isBouncerVisible, ::Pair) + .onEach { (userId, isBouncerCurrentlyVisible) -> + resetLockedOutState(userId) + yield() + runFaceAuth( + FaceAuthUiEvent.FACE_AUTH_UPDATED_USER_SWITCHING, + // Fallback to detection if bouncer is not showing so that we can detect a + // face and then show the bouncer to the user if face auth can't run + fallbackToDetect = !isBouncerCurrentlyVisible + ) } .launchIn(applicationScope) @@ -210,6 +216,14 @@ constructor( .launchIn(applicationScope) } + private val isBouncerVisible: Flow<Boolean> by lazy { + if (SceneContainerFlag.isEnabled) { + sceneInteractor.get().transitionState.map { it.isIdle(Scenes.Bouncer) } + } else { + primaryBouncerInteractor.get().isShowing + } + } + private suspend fun resetLockedOutState(currentUserId: Int) { val lockoutMode = facePropertyRepository.getLockoutMode(currentUserId) repository.setLockedOut( diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt index b45ebd865c55..905174519245 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt @@ -44,6 +44,7 @@ import com.android.systemui.statusbar.BlurUtils import com.android.systemui.statusbar.CrossFadeHelper import javax.inject.Inject import javax.inject.Named +import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.launch /** Controller for dream overlay animations. */ @@ -84,51 +85,62 @@ constructor( private var mCurrentBlurRadius: Float = 0f + private var mLifecycleFlowHandle: DisposableHandle? = null + fun init(view: View) { this.view = view - view.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - dreamViewModel.dreamOverlayTranslationY.collect { px -> - ComplicationLayoutParams.iteratePositions( - { position: Int -> setElementsTranslationYAtPosition(px, position) }, - POSITION_TOP or POSITION_BOTTOM - ) + mLifecycleFlowHandle = + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.CREATED) { + launch { + dreamViewModel.dreamOverlayTranslationY.collect { px -> + ComplicationLayoutParams.iteratePositions( + { position: Int -> + setElementsTranslationYAtPosition(px, position) + }, + POSITION_TOP or POSITION_BOTTOM + ) + } } - } - launch { - dreamViewModel.dreamOverlayTranslationX.collect { px -> - ComplicationLayoutParams.iteratePositions( - { position: Int -> setElementsTranslationXAtPosition(px, position) }, - POSITION_TOP or POSITION_BOTTOM - ) + launch { + dreamViewModel.dreamOverlayTranslationX.collect { px -> + ComplicationLayoutParams.iteratePositions( + { position: Int -> + setElementsTranslationXAtPosition(px, position) + }, + POSITION_TOP or POSITION_BOTTOM + ) + } } - } - launch { - dreamViewModel.dreamOverlayAlpha.collect { alpha -> - ComplicationLayoutParams.iteratePositions( - { position: Int -> - setElementsAlphaAtPosition( - alpha = alpha, - position = position, - fadingOut = true, - ) - }, - POSITION_TOP or POSITION_BOTTOM - ) + launch { + dreamViewModel.dreamOverlayAlpha.collect { alpha -> + ComplicationLayoutParams.iteratePositions( + { position: Int -> + setElementsAlphaAtPosition( + alpha = alpha, + position = position, + fadingOut = true, + ) + }, + POSITION_TOP or POSITION_BOTTOM + ) + } } - } - launch { - dreamViewModel.transitionEnded.collect { _ -> - mOverlayStateController.setExitAnimationsRunning(false) + launch { + dreamViewModel.transitionEnded.collect { _ -> + mOverlayStateController.setExitAnimationsRunning(false) + } } } } - } + } + + fun destroy() { + mLifecycleFlowHandle?.dispose() } /** @@ -243,10 +255,8 @@ constructor( return mAnimator as AnimatorSet } - /** Starts the dream content and dream overlay exit animations. */ - fun wakeUp() { + fun onWakeUp() { cancelAnimations() - mOverlayStateController.setExitAnimationsRunning(true) } /** Cancels the dream content and dream overlay animations, if they're currently running. */ diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java index 76c7d2383751..3dd2561614b7 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java @@ -59,6 +59,7 @@ import com.android.systemui.touch.TouchInsetManager; import com.android.systemui.util.ViewController; import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.DisposableHandle; import kotlinx.coroutines.flow.FlowKt; import java.util.Arrays; @@ -185,6 +186,8 @@ public class DreamOverlayContainerViewController extends } }; + private DisposableHandle mFlowHandle; + @Inject public DreamOverlayContainerViewController( DreamOverlayContainerView containerView, @@ -252,6 +255,17 @@ public class DreamOverlayContainerViewController extends } @Override + public void destroy() { + mStateController.removeCallback(mDreamOverlayStateCallback); + mStatusBarViewController.destroy(); + mComplicationHostViewController.destroy(); + mDreamOverlayAnimationsController.destroy(); + mLowLightTransitionCoordinator.setLowLightEnterListener(null); + + super.destroy(); + } + + @Override protected void onViewAttached() { mWakingUpFromSwipe = false; mJitterStartTimeMillis = System.currentTimeMillis(); @@ -263,7 +277,7 @@ public class DreamOverlayContainerViewController extends emptyRegion.recycle(); if (dreamHandlesBeingObscured()) { - collectFlow( + mFlowHandle = collectFlow( mView, FlowKt.distinctUntilChanged(combineFlows( mKeyguardTransitionInteractor.isFinishedIn( @@ -295,6 +309,10 @@ public class DreamOverlayContainerViewController extends @Override protected void onViewDetached() { + if (mFlowHandle != null) { + mFlowHandle.dispose(); + mFlowHandle = null; + } mHandler.removeCallbacksAndMessages(null); mPrimaryBouncerCallbackInteractor.removeBouncerExpansionCallback(mBouncerExpansionCallback); mBouncerlessScrimController.removeCallback(mBouncerlessExpansionCallback); @@ -363,16 +381,17 @@ public class DreamOverlayContainerViewController extends } /** - * Handle the dream waking up and run any necessary animations. + * Handle the dream waking up. */ - public void wakeUp() { + public void onWakeUp() { + // TODO(b/361872929): clean up this bool as it doesn't do anything anymore // When swiping causes wakeup, do not run any animations as the dream should exit as soon // as possible. if (mWakingUpFromSwipe) { return; } - mDreamOverlayAnimationsController.wakeUp(); + mDreamOverlayAnimationsController.onWakeUp(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index 7a9537ba4227..caf5b01db846 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -60,7 +60,6 @@ import com.android.systemui.ambient.touch.scrim.ScrimManager; import com.android.systemui.communal.domain.interactor.CommunalInteractor; import com.android.systemui.communal.shared.log.CommunalUiEvent; import com.android.systemui.communal.shared.model.CommunalScenes; -import com.android.systemui.complication.Complication; import com.android.systemui.complication.dagger.ComplicationComponent; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.dagger.DreamOverlayComponent; @@ -70,8 +69,12 @@ import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.touch.TouchInsetManager; import com.android.systemui.util.concurrency.DelayableExecutor; +import kotlinx.coroutines.Job; + +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.concurrent.CancellationException; import java.util.function.Consumer; import javax.inject.Inject; @@ -129,17 +132,21 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ */ private boolean mBouncerShowing = false; - private final ComplicationComponent mComplicationComponent; + private final com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory + mDreamComplicationComponentFactory; + private final ComplicationComponent.Factory mComplicationComponentFactory; + private final DreamOverlayComponent.Factory mDreamOverlayComponentFactory; + private final AmbientTouchComponent.Factory mAmbientTouchComponentFactory; - private final AmbientTouchComponent mAmbientTouchComponent; + private final TouchInsetManager mTouchInsetManager; + private final LifecycleOwner mLifecycleOwner; - private final com.android.systemui.dreams.complication.dagger.ComplicationComponent - mDreamComplicationComponent; - private final DreamOverlayComponent mDreamOverlayComponent; private ComponentName mCurrentBlockedGestureDreamActivityComponent; + private final ArrayList<Job> mFlows = new ArrayList<>(); + /** * This {@link LifecycleRegistry} controls when dream overlay functionality, like touch * handling, should be active. It will automatically be paused when the dream overlay is hidden @@ -285,36 +292,27 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback); mStateController = stateController; mUiEventLogger = uiEventLogger; + mComplicationComponentFactory = complicationComponentFactory; + mDreamComplicationComponentFactory = dreamComplicationComponentFactory; mDreamOverlayCallbackController = dreamOverlayCallbackController; mWindowTitle = windowTitle; mCommunalInteractor = communalInteractor; mSystemDialogsCloser = systemDialogsCloser; mGestureInteractor = gestureInteractor; - - final ViewModelStore viewModelStore = new ViewModelStore(); - final Complication.Host host = - () -> mExecutor.execute(DreamOverlayService.this::requestExit); - - mComplicationComponent = complicationComponentFactory.create(lifecycleOwner, host, - viewModelStore, touchInsetManager); - mDreamComplicationComponent = dreamComplicationComponentFactory.create( - mComplicationComponent.getVisibilityController(), touchInsetManager); - mDreamOverlayComponent = dreamOverlayComponentFactory.create(lifecycleOwner, - mComplicationComponent.getComplicationHostViewController(), touchInsetManager); - mAmbientTouchComponent = ambientTouchComponentFactory.create(lifecycleOwner, - new HashSet<>(Arrays.asList( - mDreamComplicationComponent.getHideComplicationTouchHandler(), - mDreamOverlayComponent.getCommunalTouchHandler()))); + mDreamOverlayComponentFactory = dreamOverlayComponentFactory; + mAmbientTouchComponentFactory = ambientTouchComponentFactory; + mTouchInsetManager = touchInsetManager; + mLifecycleOwner = lifecycleOwner; mLifecycleRegistry = lifecycleOwner.getRegistry(); mExecutor.execute(() -> setLifecycleStateLocked(Lifecycle.State.CREATED)); - collectFlow(getLifecycle(), mCommunalInteractor.isCommunalAvailable(), - mIsCommunalAvailableCallback); - collectFlow(getLifecycle(), communalInteractor.isCommunalVisible(), - mCommunalVisibleConsumer); - collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing, - mBouncerShowingConsumer); + mFlows.add(collectFlow(getLifecycle(), mCommunalInteractor.isCommunalAvailable(), + mIsCommunalAvailableCallback)); + mFlows.add(collectFlow(getLifecycle(), communalInteractor.isCommunalVisible(), + mCommunalVisibleConsumer)); + mFlows.add(collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing, + mBouncerShowingConsumer)); } @NonNull @@ -339,6 +337,11 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ public void onDestroy() { mKeyguardUpdateMonitor.removeCallback(mKeyguardCallback); + for (Job job : mFlows) { + job.cancel(new CancellationException()); + } + mFlows.clear(); + mExecutor.execute(() -> { setLifecycleStateLocked(Lifecycle.State.DESTROYED); @@ -353,6 +356,23 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ @Override public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) { + final ComplicationComponent complicationComponent = mComplicationComponentFactory.create( + mLifecycleOwner, + () -> mExecutor.execute(DreamOverlayService.this::requestExit), + new ViewModelStore(), mTouchInsetManager); + final com.android.systemui.dreams.complication.dagger.ComplicationComponent + dreamComplicationComponent = mDreamComplicationComponentFactory.create( + complicationComponent.getVisibilityController(), mTouchInsetManager); + + final DreamOverlayComponent dreamOverlayComponent = mDreamOverlayComponentFactory.create( + mLifecycleOwner, complicationComponent.getComplicationHostViewController(), + mTouchInsetManager); + final AmbientTouchComponent ambientTouchComponent = mAmbientTouchComponentFactory.create( + mLifecycleOwner, + new HashSet<>(Arrays.asList( + dreamComplicationComponent.getHideComplicationTouchHandler(), + dreamOverlayComponent.getCommunalTouchHandler()))); + setLifecycleStateLocked(Lifecycle.State.STARTED); mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START); @@ -371,8 +391,8 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ } mDreamOverlayContainerViewController = - mDreamOverlayComponent.getDreamOverlayContainerViewController(); - mTouchMonitor = mAmbientTouchComponent.getTouchMonitor(); + dreamOverlayComponent.getDreamOverlayContainerViewController(); + mTouchMonitor = ambientTouchComponent.getTouchMonitor(); mTouchMonitor.init(); mStateController.setShouldShowComplications(shouldShowComplications()); @@ -461,7 +481,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ public void onWakeUp() { if (mDreamOverlayContainerViewController != null) { mDreamOverlayCallbackController.onWakeUp(); - mDreamOverlayContainerViewController.wakeUp(); + mDreamOverlayContainerViewController.onWakeUp(); } } @@ -541,6 +561,10 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ } private void removeContainerViewFromParentLocked() { + if (mDreamOverlayContainerViewController == null) { + return; + } + View containerView = mDreamOverlayContainerViewController.getContainerView(); if (containerView == null) { return; @@ -559,8 +583,13 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ return; } + // This ensures the container view of the current dream is removed before + // the controller is potentially reset. + removeContainerViewFromParentLocked(); + if (mStarted && mWindow != null) { try { + mWindow.clearContentView(); mWindowManager.removeView(mWindow.getDecorView()); } catch (IllegalArgumentException e) { Log.e(TAG, "Error removing decor view when resetting overlay", e); @@ -571,7 +600,10 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mStateController.setLowLightActive(false); mStateController.setEntryAnimationsFinished(false); - mDreamOverlayContainerViewController = null; + if (mDreamOverlayContainerViewController != null) { + mDreamOverlayContainerViewController.destroy(); + mDreamOverlayContainerViewController = null; + } if (mTouchMonitor != null) { mTouchMonitor.destroy(); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt index befd822e14cd..d547de24beb5 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt @@ -43,6 +43,8 @@ import com.android.systemui.util.concurrency.DelayableExecutor import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import java.lang.ref.WeakReference +import java.util.concurrent.Executor typealias FragmentInfoCallback = (TaskFragmentInfo) -> Unit @@ -68,14 +70,18 @@ constructor( } private val fragmentToken = Binder() - private val organizer: TaskFragmentOrganizer = - object : TaskFragmentOrganizer(executor) { - override fun onTransactionReady(transaction: TaskFragmentTransaction) { - handleTransactionReady(transaction) - } - } - .apply { registerOrganizer(true /* isSystemOrganizer */) } + class Organizer(val component: WeakReference<TaskFragmentComponent>, executor: Executor) : + TaskFragmentOrganizer(executor) { + override fun onTransactionReady(transaction: TaskFragmentTransaction) { + component.get()?.handleTransactionReady(transaction) + } + } + + private val organizer: TaskFragmentOrganizer = + Organizer(WeakReference(this), executor).apply { + registerOrganizer(true /* isSystemOrganizer */) + } private fun handleTransactionReady(transaction: TaskFragmentTransaction) { val resultT = WindowContainerTransaction() diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java index ee7b6f52ac55..5ba780f9c99d 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java @@ -33,7 +33,11 @@ import com.android.systemui.communal.domain.interactor.CommunalInteractor; import com.android.systemui.dreams.touch.dagger.CommunalTouchModule; import com.android.systemui.statusbar.phone.CentralSurfaces; +import kotlinx.coroutines.Job; + +import java.util.ArrayList; import java.util.Optional; +import java.util.concurrent.CancellationException; import java.util.function.Consumer; import javax.inject.Inject; @@ -49,6 +53,8 @@ public class CommunalTouchHandler implements TouchHandler { private final ConfigurationInteractor mConfigurationInteractor; private Boolean mIsEnabled = false; + private ArrayList<Job> mFlows = new ArrayList<>(); + private int mLayoutDirection = LayoutDirection.LTR; @VisibleForTesting @@ -70,17 +76,17 @@ public class CommunalTouchHandler implements TouchHandler { mCommunalInteractor = communalInteractor; mConfigurationInteractor = configurationInteractor; - collectFlow( + mFlows.add(collectFlow( mLifecycle, mCommunalInteractor.isCommunalAvailable(), mIsCommunalAvailableCallback - ); + )); - collectFlow( + mFlows.add(collectFlow( mLifecycle, mConfigurationInteractor.getLayoutDirection(), mLayoutDirectionCallback - ); + )); } @Override @@ -140,4 +146,13 @@ public class CommunalTouchHandler implements TouchHandler { } }); } + + @Override + public void onDestroy() { + for (Job job : mFlows) { + job.cancel(new CancellationException()); + } + mFlows.clear(); + TouchHandler.super.onDestroy(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/education/data/model/EduDeviceConnectionTime.kt b/packages/SystemUI/src/com/android/systemui/education/data/model/EduDeviceConnectionTime.kt new file mode 100644 index 000000000000..8682848eeea1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/education/data/model/EduDeviceConnectionTime.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.education.data.model + +import java.time.Instant + +data class EduDeviceConnectionTime( + val keyboardFirstConnectionTime: Instant? = null, + val touchpadFirstConnectionTime: Instant? = null +) diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt index 4fd79d764cdd..01f838ff1ea7 100644 --- a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt @@ -29,6 +29,7 @@ import com.android.systemui.contextualeducation.GestureType import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.education.dagger.ContextualEducationModule.EduDataStoreScope +import com.android.systemui.education.data.model.EduDeviceConnectionTime import com.android.systemui.education.data.model.GestureEduModel import java.time.Instant import javax.inject.Inject @@ -53,10 +54,16 @@ interface ContextualEducationRepository { fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel> + fun readEduDeviceConnectionTime(): Flow<EduDeviceConnectionTime> + suspend fun updateGestureEduModel( gestureType: GestureType, transform: (GestureEduModel) -> GestureEduModel ) + + suspend fun updateEduDeviceConnectionTime( + transform: (EduDeviceConnectionTime) -> EduDeviceConnectionTime + ) } /** @@ -76,6 +83,8 @@ constructor( const val LAST_SHORTCUT_TRIGGERED_TIME_SUFFIX = "_LAST_SHORTCUT_TRIGGERED_TIME" const val USAGE_SESSION_START_TIME_SUFFIX = "_USAGE_SESSION_START_TIME" const val LAST_EDUCATION_TIME_SUFFIX = "_LAST_EDUCATION_TIME" + const val KEYBOARD_FIRST_CONNECTION_TIME = "KEYBOARD_FIRST_CONNECTION_TIME" + const val TOUCHPAD_FIRST_CONNECTION_TIME = "TOUCHPAD_FIRST_CONNECTION_TIME" const val DATASTORE_DIR = "education/USER%s_ContextualEducation" } @@ -158,6 +167,37 @@ constructor( } } + override fun readEduDeviceConnectionTime(): Flow<EduDeviceConnectionTime> = + prefData.map { preferences -> getEduDeviceConnectionTime(preferences) } + + override suspend fun updateEduDeviceConnectionTime( + transform: (EduDeviceConnectionTime) -> EduDeviceConnectionTime + ) { + datastore.filterNotNull().first().edit { preferences -> + val currentModel = getEduDeviceConnectionTime(preferences) + val updatedModel = transform(currentModel) + setInstant( + preferences, + updatedModel.keyboardFirstConnectionTime, + getKeyboardFirstConnectionTimeKey() + ) + setInstant( + preferences, + updatedModel.touchpadFirstConnectionTime, + getTouchpadFirstConnectionTimeKey() + ) + } + } + + private fun getEduDeviceConnectionTime(preferences: Preferences): EduDeviceConnectionTime { + return EduDeviceConnectionTime( + keyboardFirstConnectionTime = + preferences[getKeyboardFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) }, + touchpadFirstConnectionTime = + preferences[getTouchpadFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) } + ) + } + private fun getSignalCountKey(gestureType: GestureType): Preferences.Key<Int> = intPreferencesKey(gestureType.name + SIGNAL_COUNT_SUFFIX) @@ -173,6 +213,12 @@ constructor( private fun getLastEducationTimeKey(gestureType: GestureType): Preferences.Key<Long> = longPreferencesKey(gestureType.name + LAST_EDUCATION_TIME_SUFFIX) + private fun getKeyboardFirstConnectionTimeKey(): Preferences.Key<Long> = + longPreferencesKey(KEYBOARD_FIRST_CONNECTION_TIME) + + private fun getTouchpadFirstConnectionTimeKey(): Preferences.Key<Long> = + longPreferencesKey(TOUCHPAD_FIRST_CONNECTION_TIME) + private fun setInstant( preferences: MutablePreferences, instant: Instant?, diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt index db5c386a6c65..10be26e1ce0f 100644 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt @@ -22,6 +22,7 @@ import com.android.systemui.contextualeducation.GestureType.BACK import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.education.dagger.ContextualEducationModule.EduClock +import com.android.systemui.education.data.model.EduDeviceConnectionTime import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.education.data.repository.ContextualEducationRepository import com.android.systemui.user.domain.interactor.SelectedUserInteractor @@ -32,6 +33,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch @@ -67,6 +69,10 @@ constructor( .flowOn(backgroundDispatcher) } + suspend fun getEduDeviceConnectionTime(): EduDeviceConnectionTime { + return repository.readEduDeviceConnectionTime().first() + } + suspend fun incrementSignalCount(gestureType: GestureType) { repository.updateGestureEduModel(gestureType) { it.copy( @@ -100,4 +106,16 @@ constructor( it.copy(usageSessionStartTime = clock.instant(), signalCount = 1) } } + + suspend fun updateKeyboardFirstConnectionTime() { + repository.updateEduDeviceConnectionTime { + it.copy(keyboardFirstConnectionTime = clock.instant()) + } + } + + suspend fun updateTouchpadFirstConnectionTime() { + repository.updateEduDeviceConnectionTime { + it.copy(touchpadFirstConnectionTime = clock.instant()) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt index ad3335b7cdeb..e88349b2b664 100644 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt @@ -24,6 +24,7 @@ import com.android.systemui.education.dagger.ContextualEducationModule.EduClock import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.education.shared.model.EducationInfo import com.android.systemui.education.shared.model.EducationUiType +import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository import java.time.Clock import javax.inject.Inject import kotlin.time.Duration.Companion.hours @@ -39,6 +40,7 @@ class KeyboardTouchpadEduInteractor constructor( @Background private val backgroundScope: CoroutineScope, private val contextualEducationInteractor: ContextualEducationInteractor, + private val userInputDeviceRepository: UserInputDeviceRepository, @EduClock private val clock: Clock, ) : CoreStartable { @@ -61,6 +63,32 @@ constructor( } } } + + backgroundScope.launch { + userInputDeviceRepository.isAnyTouchpadConnectedForUser.collect { + if ( + it.isConnected && + contextualEducationInteractor + .getEduDeviceConnectionTime() + .touchpadFirstConnectionTime == null + ) { + contextualEducationInteractor.updateTouchpadFirstConnectionTime() + } + } + } + + backgroundScope.launch { + userInputDeviceRepository.isAnyKeyboardConnectedForUser.collect { + if ( + it.isConnected && + contextualEducationInteractor + .getEduDeviceConnectionTime() + .keyboardFirstConnectionTime == null + ) { + contextualEducationInteractor.updateKeyboardFirstConnectionTime() + } + } + } } private fun isEducationNeeded(model: GestureEduModel): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/graphics/ImageLoader.kt b/packages/SystemUI/src/com/android/systemui/graphics/ImageLoader.kt index 567bf70be3e1..ca43871415e6 100644 --- a/packages/SystemUI/src/com/android/systemui/graphics/ImageLoader.kt +++ b/packages/SystemUI/src/com/android/systemui/graphics/ImageLoader.kt @@ -35,6 +35,7 @@ import android.graphics.drawable.Icon import android.util.Log import android.util.Size import androidx.core.content.res.ResourcesCompat +import com.android.app.tracing.traceSection import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background @@ -162,20 +163,21 @@ constructor( @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT - ): Bitmap? { - return try { - ImageDecoder.decodeBitmap(source) { decoder, info, _ -> - configureDecoderForMaximumSize(decoder, info.size, maxWidth, maxHeight) - decoder.allocator = allocator + ): Bitmap? = + traceSection("ImageLoader#loadBitmap") { + return try { + ImageDecoder.decodeBitmap(source) { decoder, info, _ -> + configureDecoderForMaximumSize(decoder, info.size, maxWidth, maxHeight) + decoder.allocator = allocator + } + } catch (e: IOException) { + Log.w(TAG, "Failed to load source $source", e) + return null + } catch (e: DecodeException) { + Log.w(TAG, "Failed to decode source $source", e) + return null } - } catch (e: IOException) { - Log.w(TAG, "Failed to load source $source", e) - return null - } catch (e: DecodeException) { - Log.w(TAG, "Failed to decode source $source", e) - return null } - } /** * Loads passed [Source] on a background thread and returns the [Drawable]. @@ -253,28 +255,31 @@ constructor( @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT - ): Drawable? { - return try { - loadDrawableSync( - toImageDecoderSource(source, defaultContext), - maxWidth, - maxHeight, - allocator - ) - ?: - // If we have a resource, retry fallback using the "normal" Resource loading system. - // This will come into effect in cases like trying to load AnimatedVectorDrawable. - if (source is Res) { - val context = source.context ?: defaultContext - ResourcesCompat.getDrawable(context.resources, source.resId, context.theme) - } else { - null - } - } catch (e: NotFoundException) { - Log.w(TAG, "Couldn't load resource $source", e) - null + ): Drawable? = + traceSection("ImageLoader#loadDrawable") { + return try { + loadDrawableSync( + toImageDecoderSource(source, defaultContext), + maxWidth, + maxHeight, + allocator + ) + ?: + // If we have a resource, retry fallback using the "normal" Resource loading + // system. + // This will come into effect in cases like trying to load + // AnimatedVectorDrawable. + if (source is Res) { + val context = source.context ?: defaultContext + ResourcesCompat.getDrawable(context.resources, source.resId, context.theme) + } else { + null + } + } catch (e: NotFoundException) { + Log.w(TAG, "Couldn't load resource $source", e) + null + } } - } /** * Loads passed [ImageDecoder.Source] synchronously and returns the drawable. @@ -297,20 +302,21 @@ constructor( @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT - ): Drawable? { - return try { - ImageDecoder.decodeDrawable(source) { decoder, info, _ -> - configureDecoderForMaximumSize(decoder, info.size, maxWidth, maxHeight) - decoder.allocator = allocator + ): Drawable? = + traceSection("ImageLoader#loadDrawable") { + return try { + ImageDecoder.decodeDrawable(source) { decoder, info, _ -> + configureDecoderForMaximumSize(decoder, info.size, maxWidth, maxHeight) + decoder.allocator = allocator + } + } catch (e: IOException) { + Log.w(TAG, "Failed to load source $source", e) + return null + } catch (e: DecodeException) { + Log.w(TAG, "Failed to decode source $source", e) + return null } - } catch (e: IOException) { - Log.w(TAG, "Failed to load source $source", e) - return null - } catch (e: DecodeException) { - Log.w(TAG, "Failed to decode source $source", e) - return null } - } /** Loads icon drawable while attempting to size restrict the drawable. */ @WorkerThread @@ -320,55 +326,59 @@ constructor( @Px maxWidth: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, @Px maxHeight: Int = DEFAULT_MAX_SAFE_BITMAP_SIZE_PX, allocator: Int = ImageDecoder.ALLOCATOR_DEFAULT - ): Drawable? { - return when (icon.type) { - Icon.TYPE_URI, - Icon.TYPE_URI_ADAPTIVE_BITMAP -> { - val source = ImageDecoder.createSource(context.contentResolver, icon.uri) - loadDrawableSync(source, maxWidth, maxHeight, allocator) - } - Icon.TYPE_RESOURCE -> { - val resources = resolveResourcesForIcon(context, icon) - resources?.let { + ): Drawable? = + traceSection("ImageLoader#loadDrawable") { + return when (icon.type) { + Icon.TYPE_URI, + Icon.TYPE_URI_ADAPTIVE_BITMAP -> { + val source = ImageDecoder.createSource(context.contentResolver, icon.uri) + loadDrawableSync(source, maxWidth, maxHeight, allocator) + } + Icon.TYPE_RESOURCE -> { + val resources = resolveResourcesForIcon(context, icon) + resources?.let { + loadDrawableSync( + ImageDecoder.createSource(it, icon.resId), + maxWidth, + maxHeight, + allocator + ) + } + // Fallback to non-ImageDecoder load if the attempt failed (e.g. the + // resource + // is a Vector drawable which ImageDecoder doesn't support.) + ?: loadIconDrawable(icon, context) + } + Icon.TYPE_BITMAP -> { + BitmapDrawable(context.resources, icon.bitmap) + } + Icon.TYPE_ADAPTIVE_BITMAP -> { + AdaptiveIconDrawable(null, BitmapDrawable(context.resources, icon.bitmap)) + } + Icon.TYPE_DATA -> { loadDrawableSync( - ImageDecoder.createSource(it, icon.resId), + ImageDecoder.createSource(icon.dataBytes, icon.dataOffset, icon.dataLength), maxWidth, maxHeight, allocator ) } - // Fallback to non-ImageDecoder load if the attempt failed (e.g. the resource - // is a Vector drawable which ImageDecoder doesn't support.) - ?: loadIconDrawable(icon, context) - } - Icon.TYPE_BITMAP -> { - BitmapDrawable(context.resources, icon.bitmap) - } - Icon.TYPE_ADAPTIVE_BITMAP -> { - AdaptiveIconDrawable(null, BitmapDrawable(context.resources, icon.bitmap)) - } - Icon.TYPE_DATA -> { - loadDrawableSync( - ImageDecoder.createSource(icon.dataBytes, icon.dataOffset, icon.dataLength), - maxWidth, - maxHeight, - allocator - ) - } - else -> { - // We don't recognize this icon, just fallback. - loadIconDrawable(icon, context) + else -> { + // We don't recognize this icon, just fallback. + loadIconDrawable(icon, context) + } + }?.let { drawable -> + // Icons carry tint which we need to propagate down to a Drawable. + tintDrawable(icon, drawable) + drawable } - }?.let { drawable -> - // Icons carry tint which we need to propagate down to a Drawable. - tintDrawable(icon, drawable) - drawable } - } @WorkerThread fun loadIconDrawable(icon: Icon, context: Context): Drawable? { - icon.loadDrawable(context)?.let { return it } + icon.loadDrawable(context)?.let { + return it + } Log.w(TAG, "Failed to load drawable for $icon") return null diff --git a/packages/SystemUI/src/com/android/systemui/haptics/msdl/dagger/MSDLModule.kt b/packages/SystemUI/src/com/android/systemui/haptics/msdl/dagger/MSDLModule.kt new file mode 100644 index 000000000000..5ea96b8388bb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/haptics/msdl/dagger/MSDLModule.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.haptics.msdl.dagger + +import android.content.Context +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.google.android.msdl.domain.MSDLPlayer +import dagger.Module +import dagger.Provides + +@Module +object MSDLModule { + @Provides + @SysUISingleton + fun provideMSDLPlayer(@Application context: Context): MSDLPlayer = + MSDLPlayer.createPlayer(context) +} diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt index 6bc640d4d1f4..1aa5ee01f22d 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt @@ -20,7 +20,6 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -97,7 +96,6 @@ private fun rememberScreenColors(): TutorialScreenConfig.Colors { val secondaryFixedDim = LocalAndroidColorScheme.current.secondaryFixedDim val onSecondaryFixed = LocalAndroidColorScheme.current.onSecondaryFixed val onSecondaryFixedVariant = LocalAndroidColorScheme.current.onSecondaryFixedVariant - val surfaceContainer = MaterialTheme.colorScheme.surfaceContainer val dynamicProperties = rememberLottieDynamicProperties( rememberColorFilterProperty(".primaryFixedDim", primaryFixedDim), @@ -106,11 +104,10 @@ private fun rememberScreenColors(): TutorialScreenConfig.Colors { rememberColorFilterProperty(".onSecondaryFixedVariant", onSecondaryFixedVariant) ) val screenColors = - remember(surfaceContainer, dynamicProperties) { + remember(dynamicProperties) { TutorialScreenConfig.Colors( background = onSecondaryFixed, - successBackground = surfaceContainer, - title = primaryFixedDim, + title = secondaryFixedDim, animationColors = dynamicProperties, ) } diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt index c50b7dc06265..b27135674fb1 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt @@ -24,13 +24,13 @@ import androidx.annotation.StringRes import androidx.compose.animation.AnimatedContent import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition -import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -46,7 +46,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.stringResource @@ -60,6 +59,7 @@ import com.airbnb.lottie.compose.LottieDynamicProperty import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition import com.airbnb.lottie.compose.rememberLottieDynamicProperty +import com.android.compose.modifiers.background import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.FINISHED import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.IN_PROGRESS import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NOT_STARTED @@ -76,19 +76,11 @@ fun ActionTutorialContent( onDoneButtonClicked: () -> Unit, config: TutorialScreenConfig ) { - val animatedColor by - animateColorAsState( - targetValue = - if (actionState == FINISHED) config.colors.successBackground - else config.colors.background, - animationSpec = tween(durationMillis = 150, easing = LinearEasing), - label = "backgroundColor" - ) Column( verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxSize() - .drawBehind { drawRect(animatedColor) } + .background(config.colors.background) .padding(start = 48.dp, top = 124.dp, end = 48.dp, bottom = 48.dp) ) { Row(modifier = Modifier.fillMaxWidth().weight(1f)) { diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt index 0406bb9e6fef..55e5f2d79e60 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt @@ -29,7 +29,6 @@ data class TutorialScreenConfig( data class Colors( val background: Color, - val successBackground: Color, val title: Color, val animationColors: LottieDynamicProperties ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 871d04693452..fe3a1e447b07 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -79,6 +79,7 @@ import com.android.systemui.SystemUIApplication; import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyguard.domain.interactor.KeyguardDismissInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardWakeDirectlyToGoneInteractor; @@ -319,6 +320,7 @@ public class KeyguardService extends Service { private final WindowManagerOcclusionManager mWmOcclusionManager; private final KeyguardEnabledInteractor mKeyguardEnabledInteractor; private final KeyguardWakeDirectlyToGoneInteractor mKeyguardWakeDirectlyToGoneInteractor; + private final KeyguardDismissInteractor mKeyguardDismissInteractor; private final Lazy<FoldGracePeriodProvider> mFoldGracePeriodProvider = new Lazy<>() { @Override public FoldGracePeriodProvider get() { @@ -346,7 +348,8 @@ public class KeyguardService extends Service { KeyguardInteractor keyguardInteractor, KeyguardEnabledInteractor keyguardEnabledInteractor, Lazy<KeyguardStateCallbackStartable> keyguardStateCallbackStartableLazy, - KeyguardWakeDirectlyToGoneInteractor keyguardWakeDirectlyToGoneInteractor) { + KeyguardWakeDirectlyToGoneInteractor keyguardWakeDirectlyToGoneInteractor, + KeyguardDismissInteractor keyguardDismissInteractor) { super(); mKeyguardViewMediator = keyguardViewMediator; mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher; @@ -375,6 +378,7 @@ public class KeyguardService extends Service { mWmOcclusionManager = windowManagerOcclusionManager; mKeyguardEnabledInteractor = keyguardEnabledInteractor; mKeyguardWakeDirectlyToGoneInteractor = keyguardWakeDirectlyToGoneInteractor; + mKeyguardDismissInteractor = keyguardDismissInteractor; } @Override @@ -482,7 +486,11 @@ public class KeyguardService extends Service { public void dismiss(IKeyguardDismissCallback callback, CharSequence message) { trace("dismiss message=" + message); checkPermission(); - mKeyguardViewMediator.dismiss(callback, message); + if (KeyguardWmStateRefactor.isEnabled()) { + mKeyguardDismissInteractor.dismissKeyguardWithCallback(callback); + } else { + mKeyguardViewMediator.dismiss(callback, message); + } } @Override // Binder interface diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index ba533ce3b1bc..362e016cc97c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -72,6 +72,7 @@ import com.android.systemui.statusbar.KeyguardIndicationController import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator +import com.google.android.msdl.domain.MSDLPlayer import dagger.Lazy import java.util.Optional import javax.inject.Inject @@ -112,6 +113,7 @@ constructor( private val keyguardViewMediator: KeyguardViewMediator, private val deviceEntryUnlockTrackerViewBinder: Optional<DeviceEntryUnlockTrackerViewBinder>, @Main private val mainDispatcher: CoroutineDispatcher, + private val msdlPlayer: MSDLPlayer, ) : CoreStartable { private var rootViewHandle: DisposableHandle? = null @@ -219,6 +221,7 @@ constructor( falsingManager, keyguardViewMediator, mainDispatcher, + msdlPlayer, ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/OWNERS b/packages/SystemUI/src/com/android/systemui/keyguard/OWNERS index 443e98762c47..208a17c0a220 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/keyguard/OWNERS @@ -9,3 +9,4 @@ chandruis@google.com jglazier@google.com mpietal@google.com tsuji@google.com +yuandizhou@google.com diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt index b1589dadb300..e68d79937063 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt @@ -39,7 +39,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.SharingStarted.Companion.Eagerly import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.buffer @@ -53,7 +53,7 @@ import kotlinx.coroutines.flow.stateIn /** Encapsulates state about device entry fingerprint auth mechanism. */ interface DeviceEntryFingerprintAuthRepository { /** Whether the device entry fingerprint auth is locked out. */ - val isLockedOut: Flow<Boolean> + val isLockedOut: StateFlow<Boolean> /** * Whether the fingerprint sensor is currently listening, this doesn't mean that the user is @@ -127,7 +127,7 @@ constructor( else if (authController.isRearFpsSupported) BiometricType.REAR_FINGERPRINT else null } - override val isLockedOut: Flow<Boolean> = + override val isLockedOut: StateFlow<Boolean> by lazy { conflatedCallbackFlow { val sendLockoutUpdate = fun() { @@ -151,7 +151,12 @@ constructor( sendLockoutUpdate() awaitClose { keyguardUpdateMonitor.removeCallback(callback) } } - .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false) + .stateIn( + scope, + started = Eagerly, + initialValue = keyguardUpdateMonitor.isFingerprintLockedOut + ) + } override val isRunning: Flow<Boolean> get() = @@ -309,6 +314,7 @@ constructor( ) { sendShouldUpdateIndicatorVisibility(true) } + override fun onStrongAuthStateChanged(userId: Int) { sendShouldUpdateIndicatorVisibility(true) } @@ -318,7 +324,7 @@ constructor( awaitClose { keyguardUpdateMonitor.removeCallback(callback) } } .flowOn(mainDispatcher) - .shareIn(scope, started = SharingStarted.WhileSubscribed(), replay = 1) + .shareIn(scope, started = WhileSubscribed(), replay = 1) companion object { const val TAG = "DeviceEntryFingerprintAuthRepositoryImpl" diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index 49e4c707af18..80a0cee4f319 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -34,7 +34,6 @@ import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositor import com.android.systemui.keyguard.shared.model.BiometricUnlockMode.Companion.isWakeAndUnlock import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.util.kotlin.Utils.Companion.sample import com.android.systemui.util.kotlin.sample @@ -155,12 +154,7 @@ constructor( if (!SceneContainerFlag.isEnabled) { startTransitionTo(KeyguardState.GLANCEABLE_HUB) } - } else if ( - powerInteractor.detailedWakefulness.value.lastWakeReason == - WakeSleepReason.POWER_BUTTON && - isCommunalAvailable && - dreamManager.canStartDreaming(true) - ) { + } else if (isCommunalAvailable && dreamManager.canStartDreaming(true)) { // This case handles tapping the power button to transition through // dream -> off -> hub. if (!SceneContainerFlag.isEnabled) { @@ -226,14 +220,7 @@ constructor( ownerReason = "waking from dozing" ) } - } else if ( - powerInteractor.detailedWakefulness.value.lastWakeReason == - WakeSleepReason.POWER_BUTTON && - isCommunalAvailable && - dreamManager.canStartDreaming(true) - ) { - // This case handles tapping the power button to transition through - // dream -> off -> hub. + } else if (isCommunalAvailable && dreamManager.canStartDreaming(true)) { if (!SceneContainerFlag.isEnabled) { transitionToGlanceableHub() } 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 628e912253eb..d7e6bdb8f02c 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 @@ -16,9 +16,13 @@ package com.android.systemui.keyguard.domain.interactor +import com.android.internal.policy.IKeyguardDismissCallback import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.data.repository.TrustRepository import com.android.systemui.keyguard.shared.model.DismissAction @@ -28,23 +32,30 @@ import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.kotlin.Utils.Companion.toQuad import com.android.systemui.util.kotlin.sample import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** Encapsulates business logic for requesting the keyguard to dismiss/finish/done. */ @SysUISingleton class KeyguardDismissInteractor @Inject constructor( - trustRepository: TrustRepository, + @Main private val mainDispatcher: CoroutineDispatcher, + @Application private val scope: CoroutineScope, private val keyguardRepository: KeyguardRepository, - primaryBouncerInteractor: PrimaryBouncerInteractor, + private val primaryBouncerInteractor: PrimaryBouncerInteractor, + private val selectedUserInteractor: SelectedUserInteractor, + private val dismissCallbackRegistry: DismissCallbackRegistry, + trustRepository: TrustRepository, alternateBouncerInteractor: AlternateBouncerInteractor, powerInteractor: PowerInteractor, - private val selectedUserInteractor: SelectedUserInteractor, ) { /* * Updates when a biometric has authenticated the device and is requesting to dismiss @@ -127,4 +138,29 @@ constructor( suspend fun setKeyguardDone(keyguardDoneTiming: KeyguardDone) { keyguardRepository.setKeyguardDone(keyguardDoneTiming) } + + /** + * Dismiss the keyguard (or show the bouncer) and invoke the provided callback once dismissed. + * + * TODO(b/358412565): Support dismiss messages. + */ + fun dismissKeyguardWithCallback( + callback: IKeyguardDismissCallback?, + ) { + scope.launch { + withContext(mainDispatcher) { + if (callback != null) { + dismissCallbackRegistry.addCallback(callback) + } + + // This will either show the bouncer, or dismiss the keyguard if insecure. + // We currently need to request showing the primary bouncer in order to start a + // transition to PRIMARY_BOUNCER. Once we refactor that so that starting the + // transition is what causes the bouncer to show, we can remove this entire method, + // and simply ask KeyguardTransitionInteractor to transition to a bouncer state or + // dismiss keyguard. + primaryBouncerInteractor.show(true) + } + } + } } 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 4cab2bb5dba8..a96d7a8f0997 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,6 +19,7 @@ package com.android.systemui.keyguard.domain.interactor 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 @@ -38,6 +39,7 @@ import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.res.R @@ -87,6 +89,7 @@ constructor( sceneInteractorProvider: Provider<SceneInteractor>, private val fromGoneTransitionInteractor: Provider<FromGoneTransitionInteractor>, private val fromLockscreenTransitionInteractor: Provider<FromLockscreenTransitionInteractor>, + private val fromOccludedTransitionInteractor: Provider<FromOccludedTransitionInteractor>, sharedNotificationContainerInteractor: Provider<SharedNotificationContainerInteractor>, @Application applicationScope: CoroutineScope, ) { @@ -484,7 +487,11 @@ constructor( /** Temporary shim, until [KeyguardWmStateRefactor] is enabled */ fun dismissKeyguard() { - fromLockscreenTransitionInteractor.get().dismissKeyguard() + when (keyguardTransitionInteractor.transitionState.value.to) { + LOCKSCREEN -> fromLockscreenTransitionInteractor.get().dismissKeyguard() + OCCLUDED -> fromOccludedTransitionInteractor.get().dismissFromOccluded() + else -> Log.v(TAG, "Keyguard was dismissed, no direct transition call needed") + } } fun onCameraLaunchDetected(source: Int) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt index cd49c6a4d2e0..4a8ada7f1184 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt @@ -27,6 +27,7 @@ import com.android.internal.logging.UiEventLogger import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.KeyguardRepository @@ -67,6 +68,7 @@ constructor( broadcastDispatcher: BroadcastDispatcher, private val accessibilityManager: AccessibilityManagerWrapper, private val pulsingGestureListener: PulsingGestureListener, + private val faceAuthInteractor: DeviceEntryFaceAuthInteractor, ) { /** Whether the long-press handling feature should be enabled. */ val isLongPressHandlingEnabled: StateFlow<Boolean> = @@ -129,7 +131,8 @@ constructor( } } - /** Notifies that the user has long-pressed on the lock screen. + /** + * Notifies that the user has long-pressed on the lock screen. * * @param isA11yAction: Whether the action was performed as an a11y action */ @@ -174,6 +177,7 @@ constructor( /** Notifies that the lockscreen has been clicked at position [x], [y]. */ fun onClick(x: Float, y: Float) { pulsingGestureListener.onSingleTapUp(x, y) + faceAuthInteractor.onNotificationPanelClicked() } /** Notifies that the lockscreen has been double clicked. */ 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 89851dbec7bc..a7a832148130 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 @@ -22,6 +22,7 @@ import android.annotation.DrawableRes import android.annotation.SuppressLint import android.graphics.Point import android.graphics.Rect +import android.os.VibrationAttributes import android.util.Log import android.view.HapticFeedbackConstants import android.view.View @@ -40,6 +41,7 @@ import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launch import com.android.internal.jank.InteractionJankMonitor import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD +import com.android.systemui.Flags.msdlFeedback import com.android.systemui.Flags.newAodTransition import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text @@ -79,6 +81,9 @@ import com.android.systemui.util.ui.AnimatedValue import com.android.systemui.util.ui.isAnimating import com.android.systemui.util.ui.stopAnimating import com.android.systemui.util.ui.value +import com.google.android.msdl.data.model.MSDLToken +import com.google.android.msdl.domain.InteractionProperties +import com.google.android.msdl.domain.MSDLPlayer import kotlin.math.min import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle @@ -112,6 +117,7 @@ object KeyguardRootViewBinder { falsingManager: FalsingManager?, keyguardViewMediator: KeyguardViewMediator?, mainImmediateDispatcher: CoroutineDispatcher, + msdlPlayer: MSDLPlayer?, ): DisposableHandle { val disposables = DisposableHandles() val childViews = mutableMapOf<Int, View>() @@ -351,21 +357,43 @@ object KeyguardRootViewBinder { if (deviceEntryHapticsInteractor != null && vibratorHelper != null) { launch { deviceEntryHapticsInteractor.playSuccessHaptic.collect { - vibratorHelper.performHapticFeedback( - view, - HapticFeedbackConstants.CONFIRM, - HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, - ) + if (msdlFeedback()) { + val properties = + object : InteractionProperties { + override val vibrationAttributes: VibrationAttributes = + VibrationAttributes.createForUsage( + VibrationAttributes.USAGE_HARDWARE_FEEDBACK + ) + } + msdlPlayer?.playToken(MSDLToken.UNLOCK, properties) + } else { + vibratorHelper.performHapticFeedback( + view, + HapticFeedbackConstants.CONFIRM, + HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, + ) + } } } launch { deviceEntryHapticsInteractor.playErrorHaptic.collect { - vibratorHelper.performHapticFeedback( - view, - HapticFeedbackConstants.REJECT, - HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, - ) + if (msdlFeedback()) { + val properties = + object : InteractionProperties { + override val vibrationAttributes: VibrationAttributes = + VibrationAttributes.createForUsage( + VibrationAttributes.USAGE_HARDWARE_FEEDBACK + ) + } + msdlPlayer?.playToken(MSDLToken.FAILURE, properties) + } else { + vibratorHelper.performHapticFeedback( + view, + HapticFeedbackConstants.REJECT, + HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 51ce3556ffbd..f581a2e24546 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -79,6 +79,7 @@ import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessage import com.android.systemui.monet.ColorScheme import com.android.systemui.monet.Style import com.android.systemui.plugins.clocks.ClockController +import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shared.clocks.ClockRegistry @@ -188,6 +189,7 @@ constructor( init { coroutineScope = CoroutineScope(applicationScope.coroutineContext + Job()) disposables += DisposableHandle { coroutineScope.cancel() } + clockController.setFallbackWeatherData(WeatherData.getPlaceholderWeatherData()) if (KeyguardBottomAreaRefactor.isEnabled) { quickAffordancesCombinedViewModel.enablePreviewMode( @@ -416,6 +418,7 @@ constructor( null, // falsing manager not required for preview mode null, // keyguard view mediator is not required for preview mode mainDispatcher, + null, ) } rootView.addView( 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 3e3cbd0540a2..7b0b23ffb2ff 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 @@ -19,7 +19,6 @@ package com.android.systemui.keyguard.ui.viewmodel import android.graphics.Color import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor -import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER @@ -42,7 +41,6 @@ constructor( keyguardTransitionInteractor: KeyguardTransitionInteractor, private val dismissCallbackRegistry: DismissCallbackRegistry, alternateBouncerInteractor: Lazy<AlternateBouncerInteractor>, - private val primaryBouncerInteractor: PrimaryBouncerInteractor, ) { // When we're fully transitioned to the AlternateBouncer, the alpha of the scrim should be: private val alternateBouncerScrimAlpha = .66f @@ -70,13 +68,10 @@ constructor( fun onRemovedFromWindow() { statusBarKeyguardViewManager.hideAlternateBouncer(false) - primaryBouncerInteractor.setDismissAction(null, null) - dismissCallbackRegistry.notifyDismissCancelled() } fun onBackRequested() { statusBarKeyguardViewManager.hideAlternateBouncer(false) - primaryBouncerInteractor.setDismissAction(null, null) dismissCallbackRegistry.notifyDismissCancelled() } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 54964d6ec768..a96869df001a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -162,30 +162,26 @@ constructor( private val alphaOnShadeExpansion: Flow<Float> = combineTransform( - keyguardTransitionInteractor.isInTransition( - edge = Edge.create(from = LOCKSCREEN, to = Scenes.Gone), - edgeWithoutSceneContainer = Edge.create(from = LOCKSCREEN, to = GONE), - ), - keyguardTransitionInteractor.isInTransition( - edge = Edge.create(from = Scenes.Bouncer, to = LOCKSCREEN), - edgeWithoutSceneContainer = - Edge.create(from = PRIMARY_BOUNCER, to = LOCKSCREEN), + anyOf( + keyguardTransitionInteractor.isInTransition( + edge = Edge.create(from = LOCKSCREEN, to = Scenes.Gone), + edgeWithoutSceneContainer = Edge.create(from = LOCKSCREEN, to = GONE), + ), + keyguardTransitionInteractor.isInTransition( + edge = Edge.create(from = Scenes.Bouncer, to = LOCKSCREEN), + edgeWithoutSceneContainer = + Edge.create(from = PRIMARY_BOUNCER, to = LOCKSCREEN), + ), + keyguardTransitionInteractor.isInTransition( + Edge.create(from = LOCKSCREEN, to = DREAMING) + ), ), isOnLockscreen, shadeInteractor.qsExpansion, shadeInteractor.shadeExpansion, - ) { - lockscreenToGoneTransitionRunning, - primaryBouncerToLockscreenTransitionRunning, - isOnLockscreen, - qsExpansion, - shadeExpansion -> + ) { disabledTransitionRunning, isOnLockscreen, qsExpansion, shadeExpansion -> // Fade out quickly as the shade expands - if ( - isOnLockscreen && - !lockscreenToGoneTransitionRunning && - !primaryBouncerToLockscreenTransitionRunning - ) { + if (isOnLockscreen && !disabledTransitionRunning) { val alpha = 1f - MathUtils.constrainedMap( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt index 666c9f8e243b..2b6c3c080b78 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt @@ -20,9 +20,11 @@ import android.content.res.Resources import com.android.compose.animation.scene.ContentKey import com.android.internal.annotations.VisibleForTesting import com.android.systemui.biometrics.AuthController +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.ClockSize +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor @@ -57,7 +59,8 @@ constructor( private val shadeInteractor: ShadeInteractor, private val unfoldTransitionInteractor: UnfoldTransitionInteractor, private val occlusionInteractor: SceneContainerOcclusionInteractor, -) : SysUiViewModel() { + private val deviceEntryInteractor: DeviceEntryInteractor, +) : SysUiViewModel, ExclusiveActivatable() { @VisibleForTesting val clockSize = clockInteractor.clockSize val isUdfpsVisible: Boolean @@ -73,6 +76,10 @@ constructor( /** Whether the content of the scene UI should be shown. */ val isContentVisible: StateFlow<Boolean> = _isContentVisible.asStateFlow() + /** @see DeviceEntryInteractor.isBypassEnabled */ + val isBypassEnabled: StateFlow<Boolean> + get() = deviceEntryInteractor.isBypassEnabled + override suspend fun onActivated(): Nothing { coroutineScope { launch { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt index 1314e8863c71..6adf3e9894bd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt @@ -67,7 +67,7 @@ constructor( var leaveShadeOpen = false return transitionAnimation.sharedFlow( - duration = 200.milliseconds, + duration = 80.milliseconds, onStart = { leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide() startAlpha = viewState.alpha() diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/BaseActivatable.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/BaseActivatable.kt deleted file mode 100644 index 03476ec264c2..000000000000 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/BaseActivatable.kt +++ /dev/null @@ -1,115 +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.lifecycle - -import java.util.concurrent.atomic.AtomicBoolean -import kotlinx.coroutines.Job -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch - -/** - * A base [Activatable] with the following characteristics: - * 1. **Can be concurrently activated by no more than one owner.** A previous call to [activate] - * must be canceled before a new call to [activate] can be made. Trying to call [activate] while - * already active will fail with an error - * 2. **Can manage child [Activatable]s**. See [addChild] and [removeChild]. Added children - * automatically track the activation state of the parent such that when the parent is active, - * the children are active and vice-versa. Children are also retained such that deactivating the - * parent and reactivating it also cancels and reactivates the children. - */ -abstract class BaseActivatable : Activatable { - - private val _isActive = AtomicBoolean(false) - - var isActive: Boolean - get() = _isActive.get() - private set(value) { - _isActive.set(value) - } - - final override suspend fun activate(): Nothing { - val allowed = _isActive.compareAndSet(false, true) - check(allowed) { "Cannot activate an already active activatable!" } - - coroutineScope { - try { - launch { manageChildren() } - onActivated() - } finally { - isActive = false - } - } - } - - /** - * Notifies that the [Activatable] has been activated. - * - * Serves as an entrypoint to kick off coroutine work that the object requires in order to keep - * its state fresh and/or perform side-effects. - * - * The method suspends and doesn't return until all work required by the object is finished. In - * most cases, it's expected for the work to remain ongoing forever so this method will forever - * suspend its caller until the coroutine that called it is canceled. - * - * Implementations could follow this pattern: - * ```kotlin - * override suspend fun onActivated(): Nothing { - * coroutineScope { - * launch { ... } - * launch { ... } - * launch { ... } - * } - * } - * ``` - * - * @see activate - */ - protected abstract suspend fun onActivated(): Nothing - - private val newChildren = Channel<Activatable>(Channel.BUFFERED) - private val jobByChild: MutableMap<Activatable, Job> by lazy { mutableMapOf() } - - private suspend fun manageChildren(): Nothing { - coroutineScope { - // Reactivate children that were added during a previous activation: - jobByChild.keys.forEach { child -> jobByChild[child] = launch { child.activate() } } - - // Process requests to add more children: - newChildren.receiveAsFlow().collect { newChild -> - removeChildInternal(newChild) - jobByChild[newChild] = launch { newChild.activate() } - } - - awaitCancellation() - } - } - - fun addChild(child: Activatable) { - newChildren.trySend(child) - } - - fun removeChild(child: Activatable) { - removeChildInternal(child) - } - - private fun removeChildInternal(child: Activatable) { - jobByChild.remove(child)?.cancel() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/ExclusiveActivatable.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/ExclusiveActivatable.kt new file mode 100644 index 000000000000..08373986611f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/ExclusiveActivatable.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.lifecycle + +import java.util.concurrent.atomic.AtomicBoolean + +/** + * A base [Activatable] that can only be activated by a single owner (hence "exclusive"). A previous + * call to [activate] must be canceled before a new call to [activate] can be made. Trying to call + * [activate] while already active will result in a runtime error. + */ +abstract class ExclusiveActivatable : Activatable { + + private val _isActive = AtomicBoolean(false) + + protected var isActive: Boolean + get() = _isActive.get() + private set(value) { + _isActive.set(value) + } + + final override suspend fun activate(): Nothing { + val allowed = _isActive.compareAndSet(false, true) + check(allowed) { "Cannot activate an already active ExclusiveActivatable!" } + + try { + onActivated() + } finally { + isActive = false + } + } + + /** + * Notifies that the [Activatable] has been activated. + * + * Serves as an entrypoint to kick off coroutine work that the object requires in order to keep + * its state fresh and/or perform side-effects. + * + * The method suspends and doesn't return until all work required by the object is finished. In + * most cases, it's expected for the work to remain ongoing forever so this method will forever + * suspend its caller until the coroutine that called it is canceled. + * + * Implementations could follow this pattern: + * ```kotlin + * override suspend fun onActivated(): Nothing { + * coroutineScope { + * launch { ... } + * launch { ... } + * launch { ... } + * awaitCancellation() + * } + * } + * ``` + * + * @see activate + */ + protected abstract suspend fun onActivated(): Nothing +} diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/Hydrator.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/Hydrator.kt new file mode 100644 index 000000000000..59ec2af2d697 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/Hydrator.kt @@ -0,0 +1,90 @@ +/* + * 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.lifecycle + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.snapshots.StateFactoryMarker +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +/** + * Keeps snapshot/Compose [State]s up-to-date. + * + * ```kotlin + * val hydrator = Hydrator() + * val state: Int by hydrator.hydratedStateOf(upstreamFlow) + * + * override suspend fun activate(): Nothing { + * hydrator.activate() + * } + * ``` + */ +class Hydrator : ExclusiveActivatable() { + + private val children = mutableListOf<Activatable>() + + /** + * Returns a snapshot [State] that's kept up-to-date as long as the [SysUiViewModel] is active. + * + * @param source The upstream [StateFlow] to collect from; values emitted to it will be + * automatically set on the returned [State]. + */ + @StateFactoryMarker + fun <T> hydratedStateOf( + source: StateFlow<T>, + ): State<T> { + return hydratedStateOf( + initialValue = source.value, + source = source, + ) + } + + /** + * Returns a snapshot [State] that's kept up-to-date as long as the [SysUiViewModel] is active. + * + * @param initialValue The first value to place on the [State] + * @param source The upstream [Flow] to collect from; values emitted to it will be automatically + * set on the returned [State]. + */ + @StateFactoryMarker + fun <T> hydratedStateOf( + initialValue: T, + source: Flow<T>, + ): State<T> { + check(!isActive) { "Cannot call hydratedStateOf after Hydrator is already active." } + + val mutableState = mutableStateOf(initialValue) + children.add( + object : ExclusiveActivatable() { + override suspend fun onActivated(): Nothing { + source.collect { mutableState.value = it } + awaitCancellation() + } + } + ) + return mutableState + } + + override suspend fun onActivated() = coroutineScope { + children.forEach { child -> launch { child.activate() } } + awaitCancellation() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt index 979eaef30a23..29ffcbd15125 100644 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt @@ -18,75 +18,31 @@ package com.android.systemui.lifecycle import android.view.View import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.snapshots.StateFactoryMarker +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -/** Base class for all System UI view-models. */ -abstract class SysUiViewModel : BaseActivatable() { - - /** - * Returns a snapshot [State] that's kept up-to-date as long as the [SysUiViewModel] is active. - * - * @param source The upstream [StateFlow] to collect from; values emitted to it will be - * automatically set on the returned [State]. - */ - @StateFactoryMarker - fun <T> hydratedStateOf( - source: StateFlow<T>, - ): State<T> { - return hydratedStateOf( - initialValue = source.value, - source = source, - ) - } - - /** - * Returns a snapshot [State] that's kept up-to-date as long as the [SysUiViewModel] is active. - * - * @param initialValue The first value to place on the [State] - * @param source The upstream [Flow] to collect from; values emitted to it will be automatically - * set on the returned [State]. - */ - @StateFactoryMarker - fun <T> hydratedStateOf( - initialValue: T, - source: Flow<T>, - ): State<T> { - val mutableState = mutableStateOf(initialValue) - addChild( - object : BaseActivatable() { - override suspend fun onActivated(): Nothing { - source.collect { mutableState.value = it } - awaitCancellation() - } - } - ) - return mutableState - } - - override suspend fun onActivated(): Nothing { - awaitCancellation() - } -} +/** Defines interface for all System UI view-models. */ +interface SysUiViewModel /** - * Returns a remembered [SysUiViewModel] of the type [T] that's automatically kept active until this - * composable leaves the composition. - * - * If the [key] changes, the old [SysUiViewModel] is deactivated and a new one will be instantiated, + * Returns a remembered [SysUiViewModel] of the type [T]. If the returned instance is also an + * [Activatable], it's automatically kept active until this composable leaves the composition; if + * the [key] changes, the old [SysUiViewModel] is deactivated and a new one will be instantiated, * activated, and returned. */ @Composable fun <T : SysUiViewModel> rememberViewModel( key: Any = Unit, factory: () -> T, -): T = rememberActivated(key, factory) +): T { + val instance = remember(key) { factory() } + if (instance is Activatable) { + LaunchedEffect(instance) { instance.activate() } + } + return instance +} /** * Invokes [block] in a new coroutine with a new [SysUiViewModel] that is automatically activated @@ -100,6 +56,8 @@ suspend fun <T : SysUiViewModel> View.viewModel( ): Nothing = repeatOnWindowLifecycle(minWindowLifecycleState) { val instance = factory() - launch { instance.activate() } + if (instance is Activatable) { + launch { instance.activate() } + } block(instance) } diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/CommunalTouchLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/CommunalTouchLog.kt new file mode 100644 index 000000000000..b0abdb7c128d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/CommunalTouchLog.kt @@ -0,0 +1,25 @@ +/* + * 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.log.dagger + +import javax.inject.Qualifier + +/** A [com.android.systemui.log.LogBuffer] for communal touch-handling logging. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class CommunalTouchLog diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 5cae58a81cf9..ba3c1d216099 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -618,6 +618,16 @@ public class LogModule { } /** + * Provides a {@link LogBuffer} for communal touch-handling logs. + */ + @Provides + @SysUISingleton + @CommunalTouchLog + public static LogBuffer provideCommunalTouchLogBuffer(LogBufferFactory factory) { + return factory.create("CommunalTouchLog", 250); + } + + /** * Provides a {@link TableLogBuffer} for communal-related logs. */ @Provides diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt index 9c29bab80d14..ed5080d66c33 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt @@ -22,7 +22,7 @@ import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManage import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor -import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.scene.shared.flag.SceneContainerFlag import dagger.Binds import dagger.Module import dagger.Provides @@ -51,9 +51,8 @@ interface MediaDomainModule { fun providesMediaDataManager( legacyProvider: Provider<LegacyMediaDataManagerImpl>, newProvider: Provider<MediaCarouselInteractor>, - mediaFlags: MediaFlags, ): MediaDataManager { - return if (mediaFlags.isSceneContainerEnabled()) { + return if (SceneContainerFlag.isEnabled) { newProvider.get() } else { legacyProvider.get() diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt index 143d66b69f57..24c57bea8bec 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt @@ -16,9 +16,8 @@ package com.android.systemui.media.controls.domain.pipeline +import android.annotation.MainThread import android.annotation.SuppressLint -import android.app.ActivityOptions -import android.app.BroadcastOptions import android.app.Notification import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME import android.app.PendingIntent @@ -39,7 +38,6 @@ import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.ImageDecoder -import android.graphics.drawable.Animatable import android.graphics.drawable.Icon import android.media.MediaDescription import android.media.MediaMetadata @@ -62,8 +60,10 @@ import com.android.internal.annotations.Keep import com.android.internal.logging.InstanceId import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.Dumpable +import com.android.systemui.Flags import com.android.systemui.broadcast.BroadcastDispatcher 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.dump.DumpManager @@ -86,7 +86,6 @@ import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.BcSmartspaceDataPlugin import com.android.systemui.res.R -import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.statusbar.notification.row.HybridGroupManager import com.android.systemui.tuner.TunerService @@ -97,8 +96,13 @@ import com.android.systemui.util.concurrency.ThreadFactory import com.android.systemui.util.time.SystemClock import java.io.IOException import java.io.PrintWriter +import java.util.Collections import java.util.concurrent.Executor import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext // URI fields to try loading album art from private val ART_URIS = @@ -167,8 +171,11 @@ private fun allowMediaRecommendations(context: Context): Boolean { class LegacyMediaDataManagerImpl( private val context: Context, @Background private val backgroundExecutor: Executor, + @Background private val backgroundDispatcher: CoroutineDispatcher, @Main private val uiExecutor: Executor, @Main private val foregroundExecutor: DelayableExecutor, + @Main private val mainDispatcher: CoroutineDispatcher, + @Application private val applicationScope: CoroutineScope, private val mediaControllerFactory: MediaControllerFactory, private val broadcastDispatcher: BroadcastDispatcher, dumpManager: DumpManager, @@ -188,6 +195,7 @@ class LegacyMediaDataManagerImpl( private val logger: MediaUiEventLogger, private val smartspaceManager: SmartspaceManager?, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val mediaDataLoader: dagger.Lazy<MediaDataLoader>, ) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener, MediaDataManager { companion object { @@ -219,7 +227,12 @@ class LegacyMediaDataManagerImpl( // listeners are listeners that depend on MediaDataManager. // TODO(b/159539991#comment5): Move internal listeners to separate package. private val internalListeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() - private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() + private val mediaEntries: MutableMap<String, MediaData> = + if (Flags.mediaLoadMetadataViaMediaDataLoader()) { + Collections.synchronizedMap(LinkedHashMap()) + } else { + LinkedHashMap() + } // There should ONLY be at most one Smartspace media recommendation. var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA @Keep private var smartspaceSession: SmartspaceSession? = null @@ -245,8 +258,11 @@ class LegacyMediaDataManagerImpl( constructor( context: Context, threadFactory: ThreadFactory, + @Background backgroundDispatcher: CoroutineDispatcher, @Main uiExecutor: Executor, @Main foregroundExecutor: DelayableExecutor, + @Main mainDispatcher: CoroutineDispatcher, + @Application applicationScope: CoroutineScope, mediaControllerFactory: MediaControllerFactory, dumpManager: DumpManager, broadcastDispatcher: BroadcastDispatcher, @@ -264,13 +280,17 @@ class LegacyMediaDataManagerImpl( logger: MediaUiEventLogger, smartspaceManager: SmartspaceManager?, keyguardUpdateMonitor: KeyguardUpdateMonitor, + mediaDataLoader: dagger.Lazy<MediaDataLoader>, ) : this( context, // Loading bitmap for UMO background can take longer time, so it cannot run on the default // background thread. Use a custom thread for media. threadFactory.buildExecutorOnNewThread(TAG), + backgroundDispatcher, uiExecutor, foregroundExecutor, + mainDispatcher, + applicationScope, mediaControllerFactory, broadcastDispatcher, dumpManager, @@ -290,6 +310,7 @@ class LegacyMediaDataManagerImpl( logger, smartspaceManager, keyguardUpdateMonitor, + mediaDataLoader, ) private val appChangeReceiver = @@ -464,16 +485,31 @@ class LegacyMediaDataManagerImpl( logSingleVsMultipleMediaAdded(appUid, packageName, instanceId) logger.logResumeMediaAdded(appUid, packageName, instanceId) } - backgroundExecutor.execute { - loadMediaDataInBgForResumption( - userId, - desc, - action, - token, - appName, - appIntent, - packageName - ) + + if (Flags.mediaLoadMetadataViaMediaDataLoader()) { + applicationScope.launch { + loadMediaDataForResumption( + userId, + desc, + action, + token, + appName, + appIntent, + packageName + ) + } + } else { + backgroundExecutor.execute { + loadMediaDataInBgForResumption( + userId, + desc, + action, + token, + appName, + appIntent, + packageName + ) + } } } @@ -498,9 +534,90 @@ class LegacyMediaDataManagerImpl( oldKey: String?, isNewlyActiveEntry: Boolean = false, ) { - backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) } + if (Flags.mediaLoadMetadataViaMediaDataLoader()) { + applicationScope.launch { + loadMediaDataWithLoader(key, sbn, oldKey, isNewlyActiveEntry) + } + } else { + backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) } + } } + private suspend fun loadMediaDataWithLoader( + key: String, + sbn: StatusBarNotification, + oldKey: String?, + isNewlyActiveEntry: Boolean = false, + ) = + withContext(backgroundDispatcher) { + val lastActive = systemClock.elapsedRealtime() + val result = mediaDataLoader.get().loadMediaData(key, sbn) + if (result == null) { + Log.d(TAG, "No result from loadMediaData") + return@withContext + } + + val currentEntry = mediaEntries[key] + val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() + val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L + val resumeAction: Runnable? = currentEntry?.resumeAction + val hasCheckedForResume = currentEntry?.hasCheckedForResume == true + val active = currentEntry?.active ?: true + + // We need to log the correct media added. + if (isNewlyActiveEntry) { + logSingleVsMultipleMediaAdded(result.appUid, sbn.packageName, instanceId) + logger.logActiveMediaAdded( + result.appUid, + sbn.packageName, + instanceId, + result.playbackLocation + ) + } else if (result.playbackLocation != currentEntry?.playbackLocation) { + logger.logPlaybackLocationChange( + result.appUid, + sbn.packageName, + instanceId, + result.playbackLocation + ) + } + + withContext(mainDispatcher) { + onMediaDataLoaded( + key, + oldKey, + MediaData( + userId = sbn.normalizedUserId, + initialized = true, + app = result.appName, + appIcon = result.appIcon, + artist = result.artist, + song = result.song, + artwork = result.artworkIcon, + actions = result.actionIcons, + actionsToShowInCompact = result.actionsToShowInCompact, + semanticActions = result.semanticActions, + packageName = sbn.packageName, + token = result.token, + clickIntent = result.clickIntent, + device = result.device, + active = active, + resumeAction = resumeAction, + playbackLocation = result.playbackLocation, + notificationKey = key, + hasCheckedForResume = hasCheckedForResume, + isPlaying = result.isPlaying, + isClearable = !sbn.isOngoing, + lastActive = lastActive, + createdTimestampMillis = createdTimestampMillis, + instanceId = instanceId, + appUid = result.appUid, + isExplicit = result.isExplicit, + ) + ) + } + } + /** Add a listener for changes in this class */ override fun addListener(listener: MediaDataManager.Listener) { // mediaDataFilter is the current end of the internal pipeline. Register external @@ -697,6 +814,75 @@ class LegacyMediaDataManagerImpl( notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) } + private suspend fun loadMediaDataForResumption( + userId: Int, + desc: MediaDescription, + resumeAction: Runnable, + token: MediaSession.Token, + appName: String, + appIntent: PendingIntent, + packageName: String + ) = + withContext(backgroundDispatcher) { + val lastActive = systemClock.elapsedRealtime() + val currentEntry = mediaEntries[packageName] + val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L + val result = + mediaDataLoader + .get() + .loadMediaDataForResumption( + userId, + desc, + resumeAction, + currentEntry, + token, + appName, + appIntent, + packageName + ) + if (result == null || desc.title.isNullOrBlank()) { + Log.d(TAG, "No MediaData result for resumption") + mediaEntries.remove(packageName) + return@withContext + } + + val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() + withContext(mainDispatcher) { + onMediaDataLoaded( + packageName, + null, + MediaData( + userId = userId, + initialized = true, + app = result.appName, + appIcon = null, + artist = result.artist, + song = result.song, + artwork = result.artworkIcon, + actions = result.actionIcons, + actionsToShowInCompact = result.actionsToShowInCompact, + semanticActions = result.semanticActions, + packageName = packageName, + token = result.token, + clickIntent = result.clickIntent, + device = result.device, + active = false, + resumeAction = resumeAction, + resumption = true, + notificationKey = packageName, + hasCheckedForResume = true, + lastActive = lastActive, + createdTimestampMillis = createdTimestampMillis, + instanceId = instanceId, + appUid = result.appUid, + isExplicit = result.isExplicit, + resumeProgress = result.resumeProgress, + ) + ) + } + } + + @Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up") private fun loadMediaDataInBgForResumption( userId: Int, desc: MediaDescription, @@ -780,6 +966,7 @@ class LegacyMediaDataManagerImpl( } } + @Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up") fun loadMediaDataInBg( key: String, sbn: StatusBarNotification, @@ -802,8 +989,7 @@ class LegacyMediaDataManagerImpl( notif.extras.getParcelable( Notification.EXTRA_BUILDER_APPLICATION_INFO, ApplicationInfo::class.java - ) - ?: getAppInfoFromPackage(sbn.packageName) + ) ?: getAppInfoFromPackage(sbn.packageName) // App name val appName = getAppName(sbn, appInfo) @@ -894,7 +1080,7 @@ class LegacyMediaDataManagerImpl( var actionsToShowCollapsed: List<Int> = emptyList() val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user) if (semanticActions == null) { - val actions = createActionsFromNotification(sbn) + val actions = createActionsFromNotification(context, activityStarter, sbn) actionIcons = actions.first actionsToShowCollapsed = actions.second } @@ -975,6 +1161,7 @@ class LegacyMediaDataManagerImpl( } } + @Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up") private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? { try { return context.packageManager.getApplicationInfo(packageName, 0) @@ -984,6 +1171,7 @@ class LegacyMediaDataManagerImpl( return null } + @Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up") private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String { val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME) if (name != null) { @@ -997,264 +1185,19 @@ class LegacyMediaDataManagerImpl( } } - /** Generate action buttons based on notification actions */ - private fun createActionsFromNotification( - sbn: StatusBarNotification - ): Pair<List<MediaAction>, List<Int>> { - val notif = sbn.notification - val actionIcons: MutableList<MediaAction> = ArrayList() - val actions = notif.actions - var actionsToShowCollapsed = - notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() - ?: mutableListOf() - if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) { - Log.e( - TAG, - "Too many compact actions for ${sbn.key}," + - "limiting to first $MAX_COMPACT_ACTIONS" - ) - actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS) - } - - if (actions != null) { - for ((index, action) in actions.withIndex()) { - if (index == MAX_NOTIFICATION_ACTIONS) { - Log.w( - TAG, - "Too many notification actions for ${sbn.key}," + - " limiting to first $MAX_NOTIFICATION_ACTIONS" - ) - break - } - if (action.getIcon() == null) { - if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}") - actionsToShowCollapsed.remove(index) - continue - } - val runnable = - if (action.actionIntent != null) { - Runnable { - if (action.actionIntent.isActivity) { - activityStarter.startPendingIntentDismissingKeyguard( - action.actionIntent - ) - } else if (action.isAuthenticationRequired()) { - activityStarter.dismissKeyguardThenExecute( - { - var result = sendPendingIntent(action.actionIntent) - result - }, - {}, - true - ) - } else { - sendPendingIntent(action.actionIntent) - } - } - } else { - null - } - val mediaActionIcon = - if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) { - Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId()) - } else { - action.getIcon() - } - .setTint(themeText) - .loadDrawable(context) - val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null) - actionIcons.add(mediaAction) - } - } - return Pair(actionIcons, actionsToShowCollapsed) - } - - /** - * Generates action button info for this media session based on the PlaybackState - * - * @param packageName Package name for the media app - * @param controller MediaController for the current session - * @return a Pair consisting of a list of media actions, and a list of ints representing which - * - * ``` - * of those actions should be shown in the compact player - * ``` - */ private fun createActionsFromState( packageName: String, controller: MediaController, user: UserHandle ): MediaButton? { - val state = controller.playbackState - if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) { - return null - } - - // First, check for standard actions - val playOrPause = - if (isConnectingState(state.state)) { - // Spinner needs to be animating to render anything. Start it here. - val drawable = - context.getDrawable(com.android.internal.R.drawable.progress_small_material) - (drawable as Animatable).start() - MediaAction( - drawable, - null, // no action to perform when clicked - context.getString(R.string.controls_media_button_connecting), - context.getDrawable(R.drawable.ic_media_connecting_container), - // Specify a rebind id to prevent the spinner from restarting on later binds. - com.android.internal.R.drawable.progress_small_material - ) - } else if (isPlayingState(state.state)) { - getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE) - } else { - getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY) - } - val prevButton = - getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS) - val nextButton = - getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT) - - // Then, create a way to build any custom actions that will be needed - val customActions = - state.customActions - .asSequence() - .filterNotNull() - .map { getCustomAction(state, packageName, controller, it) } - .iterator() - fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null - - // Finally, assign the remaining button slots: play/pause A B C D - // A = previous, else custom action (if not reserved) - // B = next, else custom action (if not reserved) - // C and D are always custom actions - val reservePrev = - controller.extras?.getBoolean( - MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV - ) == true - val reserveNext = - controller.extras?.getBoolean( - MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT - ) == true - - val prevOrCustom = - if (prevButton != null) { - prevButton - } else if (!reservePrev) { - nextCustomAction() - } else { - null - } - - val nextOrCustom = - if (nextButton != null) { - nextButton - } else if (!reserveNext) { - nextCustomAction() - } else { - null - } - - return MediaButton( - playOrPause, - nextOrCustom, - prevOrCustom, - nextCustomAction(), - nextCustomAction(), - reserveNext, - reservePrev - ) - } - - /** - * Create a [MediaAction] for a given action and media session - * - * @param controller MediaController for the session - * @param stateActions The actions included with the session's [PlaybackState] - * @param action A [PlaybackState.Actions] value representing what action to generate. One of: - * ``` - * [PlaybackState.ACTION_PLAY] - * [PlaybackState.ACTION_PAUSE] - * [PlaybackState.ACTION_SKIP_TO_PREVIOUS] - * [PlaybackState.ACTION_SKIP_TO_NEXT] - * @return - * ``` - * - * A [MediaAction] with correct values set, or null if the state doesn't support it - */ - private fun getStandardAction( - controller: MediaController, - stateActions: Long, - @PlaybackState.Actions action: Long - ): MediaAction? { - if (!includesAction(stateActions, action)) { + if (!mediaFlags.areMediaSessionActionsEnabled(packageName, user)) { return null } - - return when (action) { - PlaybackState.ACTION_PLAY -> { - MediaAction( - context.getDrawable(R.drawable.ic_media_play), - { controller.transportControls.play() }, - context.getString(R.string.controls_media_button_play), - context.getDrawable(R.drawable.ic_media_play_container) - ) - } - PlaybackState.ACTION_PAUSE -> { - MediaAction( - context.getDrawable(R.drawable.ic_media_pause), - { controller.transportControls.pause() }, - context.getString(R.string.controls_media_button_pause), - context.getDrawable(R.drawable.ic_media_pause_container) - ) - } - PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { - MediaAction( - context.getDrawable(R.drawable.ic_media_prev), - { controller.transportControls.skipToPrevious() }, - context.getString(R.string.controls_media_button_prev), - null - ) - } - PlaybackState.ACTION_SKIP_TO_NEXT -> { - MediaAction( - context.getDrawable(R.drawable.ic_media_next), - { controller.transportControls.skipToNext() }, - context.getString(R.string.controls_media_button_next), - null - ) - } - else -> null - } - } - - /** Check whether the actions from a [PlaybackState] include a specific action */ - private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean { - if ( - (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) && - (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L) - ) { - return true - } - return (stateActions and action != 0L) - } - - /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */ - private fun getCustomAction( - state: PlaybackState, - packageName: String, - controller: MediaController, - customAction: PlaybackState.CustomAction - ): MediaAction { - return MediaAction( - Icon.createWithResource(packageName, customAction.icon).loadDrawable(context), - { controller.transportControls.sendCustomAction(customAction, customAction.extras) }, - customAction.name, - null - ) + return createActionsFromState(context, packageName, controller) } /** Load a bitmap from the various Art metadata URIs */ + @Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up") private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? { for (uri in ART_URIS) { val uriString = metadata.getString(uri) @@ -1269,21 +1212,6 @@ class LegacyMediaDataManagerImpl( return null } - private fun sendPendingIntent(intent: PendingIntent): Boolean { - return try { - val options = BroadcastOptions.makeBasic() - options.setInteractive(true) - options.setPendingIntentBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED - ) - intent.send(options.toBundle()) - true - } catch (e: PendingIntent.CanceledException) { - Log.d(TAG, "Intent canceled", e) - false - } - } - /** Returns a bitmap if the user can access the given URI, else null */ private fun loadBitmapFromUriForUser( uri: Uri, @@ -1364,6 +1292,7 @@ class LegacyMediaDataManagerImpl( ) } + @MainThread fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) = traceSection("MediaDataManager#onMediaDataLoaded") { Assert.isMainThread() @@ -1619,6 +1548,7 @@ class LegacyMediaDataManagerImpl( * - If resumption is disabled, we only want to show active players */ override fun hasAnyMedia() = mediaDataFilter.hasAnyMedia() + override fun isRecommendationActive() = smartspaceMediaData.isActive /** diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt new file mode 100644 index 000000000000..70189b79ea58 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt @@ -0,0 +1,311 @@ +/* + * 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.domain.pipeline + +import android.app.ActivityOptions +import android.app.BroadcastOptions +import android.app.Notification +import android.app.PendingIntent +import android.content.Context +import android.graphics.drawable.Animatable +import android.graphics.drawable.Icon +import android.media.session.MediaController +import android.media.session.PlaybackState +import android.service.notification.StatusBarNotification +import android.util.Log +import androidx.media.utils.MediaConstants +import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_COMPACT_ACTIONS +import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_NOTIFICATION_ACTIONS +import com.android.systemui.media.controls.shared.model.MediaAction +import com.android.systemui.media.controls.shared.model.MediaButton +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R +import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState +import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState +import com.android.systemui.util.kotlin.logI + +private const val TAG = "MediaActions" + +/** + * Generates action button info for this media session based on the PlaybackState + * + * @param packageName Package name for the media app + * @param controller MediaController for the current session + * @return a Pair consisting of a list of media actions, and a list of ints representing which of + * those actions should be shown in the compact player + */ +fun createActionsFromState( + context: Context, + packageName: String, + controller: MediaController, +): MediaButton? { + val state = controller.playbackState ?: return null + // First, check for standard actions + val playOrPause = + if (isConnectingState(state.state)) { + // Spinner needs to be animating to render anything. Start it here. + val drawable = + context.getDrawable(com.android.internal.R.drawable.progress_small_material) + (drawable as Animatable).start() + MediaAction( + drawable, + null, // no action to perform when clicked + context.getString(R.string.controls_media_button_connecting), + context.getDrawable(R.drawable.ic_media_connecting_container), + // Specify a rebind id to prevent the spinner from restarting on later binds. + com.android.internal.R.drawable.progress_small_material + ) + } else if (isPlayingState(state.state)) { + getStandardAction(context, controller, state.actions, PlaybackState.ACTION_PAUSE) + } else { + getStandardAction(context, controller, state.actions, PlaybackState.ACTION_PLAY) + } + val prevButton = + getStandardAction(context, controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS) + val nextButton = + getStandardAction(context, controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT) + + // Then, create a way to build any custom actions that will be needed + val customActions = + state.customActions + .asSequence() + .filterNotNull() + .map { getCustomAction(context, packageName, controller, it) } + .iterator() + fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null + + // Finally, assign the remaining button slots: play/pause A B C D + // A = previous, else custom action (if not reserved) + // B = next, else custom action (if not reserved) + // C and D are always custom actions + val reservePrev = + controller.extras?.getBoolean( + MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV + ) == true + val reserveNext = + controller.extras?.getBoolean( + MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT + ) == true + + val prevOrCustom = + if (prevButton != null) { + prevButton + } else if (!reservePrev) { + nextCustomAction() + } else { + null + } + + val nextOrCustom = + if (nextButton != null) { + nextButton + } else if (!reserveNext) { + nextCustomAction() + } else { + null + } + + return MediaButton( + playOrPause, + nextOrCustom, + prevOrCustom, + nextCustomAction(), + nextCustomAction(), + reserveNext, + reservePrev + ) +} + +/** + * Create a [MediaAction] for a given action and media session + * + * @param controller MediaController for the session + * @param stateActions The actions included with the session's [PlaybackState] + * @param action A [PlaybackState.Actions] value representing what action to generate. One of: + * [PlaybackState.ACTION_PLAY] [PlaybackState.ACTION_PAUSE] + * [PlaybackState.ACTION_SKIP_TO_PREVIOUS] [PlaybackState.ACTION_SKIP_TO_NEXT] + * @return A [MediaAction] with correct values set, or null if the state doesn't support it + */ +private fun getStandardAction( + context: Context, + controller: MediaController, + stateActions: Long, + @PlaybackState.Actions action: Long +): MediaAction? { + if (!includesAction(stateActions, action)) { + return null + } + + return when (action) { + PlaybackState.ACTION_PLAY -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_play), + { controller.transportControls.play() }, + context.getString(R.string.controls_media_button_play), + context.getDrawable(R.drawable.ic_media_play_container) + ) + } + PlaybackState.ACTION_PAUSE -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_pause), + { controller.transportControls.pause() }, + context.getString(R.string.controls_media_button_pause), + context.getDrawable(R.drawable.ic_media_pause_container) + ) + } + PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_prev), + { controller.transportControls.skipToPrevious() }, + context.getString(R.string.controls_media_button_prev), + null + ) + } + PlaybackState.ACTION_SKIP_TO_NEXT -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_next), + { controller.transportControls.skipToNext() }, + context.getString(R.string.controls_media_button_next), + null + ) + } + else -> null + } +} + +/** Get a [MediaAction] representing a [PlaybackState.CustomAction] */ +private fun getCustomAction( + context: Context, + packageName: String, + controller: MediaController, + customAction: PlaybackState.CustomAction +): MediaAction { + return MediaAction( + Icon.createWithResource(packageName, customAction.icon).loadDrawable(context), + { controller.transportControls.sendCustomAction(customAction, customAction.extras) }, + customAction.name, + null + ) +} + +/** Check whether the actions from a [PlaybackState] include a specific action */ +private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean { + if ( + (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) && + (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L) + ) { + return true + } + return (stateActions and action != 0L) +} + +/** Generate action buttons based on notification actions */ +fun createActionsFromNotification( + context: Context, + activityStarter: ActivityStarter, + sbn: StatusBarNotification +): Pair<List<MediaAction>, List<Int>> { + val notif = sbn.notification + val actionIcons: MutableList<MediaAction> = ArrayList() + val actions = notif.actions + var actionsToShowCollapsed = + notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() + ?: mutableListOf() + if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) { + Log.e( + TAG, + "Too many compact actions for ${sbn.key}, limiting to first $MAX_COMPACT_ACTIONS" + ) + actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS) + } + + actions?.let { + if (it.size > MAX_NOTIFICATION_ACTIONS) { + Log.w( + TAG, + "Too many notification actions for ${sbn.key}, " + + "limiting to first $MAX_NOTIFICATION_ACTIONS" + ) + } + + for ((index, action) in it.take(MAX_NOTIFICATION_ACTIONS).withIndex()) { + if (action.getIcon() == null) { + logI(TAG) { "No icon for action $index ${action.title}" } + actionsToShowCollapsed.remove(index) + continue + } + + val runnable = + action.actionIntent?.let { actionIntent -> + Runnable { + when { + actionIntent.isActivity -> + activityStarter.startPendingIntentDismissingKeyguard( + action.actionIntent + ) + action.isAuthenticationRequired -> + activityStarter.dismissKeyguardThenExecute( + { sendPendingIntent(action.actionIntent) }, + {}, + true + ) + else -> sendPendingIntent(actionIntent) + } + } + } + + val themeText = + com.android.settingslib.Utils.getColorAttr( + context, + com.android.internal.R.attr.textColorPrimary + ) + .defaultColor + + val mediaActionIcon = + when (action.getIcon().type) { + Icon.TYPE_RESOURCE -> + Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId()) + else -> action.getIcon() + } + .setTint(themeText) + .loadDrawable(context) + + val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null) + actionIcons.add(mediaAction) + } + } + return Pair(actionIcons, actionsToShowCollapsed) +} + +private fun sendPendingIntent(intent: PendingIntent): Boolean { + return try { + intent.send( + BroadcastOptions.makeBasic() + .apply { + setInteractive(true) + setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ) + } + .toBundle() + ) + true + } catch (e: PendingIntent.CanceledException) { + Log.d(TAG, "Intent canceled", e) + false + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt new file mode 100644 index 000000000000..f9fef8eac815 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt @@ -0,0 +1,530 @@ +/* + * 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.domain.pipeline + +import android.annotation.WorkerThread +import android.app.Notification +import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME +import android.app.PendingIntent +import android.app.StatusBarManager +import android.app.UriGrantsManager +import android.content.ContentProvider +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.ImageDecoder +import android.graphics.drawable.Icon +import android.media.MediaDescription +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSession +import android.net.Uri +import android.os.Process +import android.os.UserHandle +import android.service.notification.StatusBarNotification +import android.support.v4.media.MediaMetadataCompat +import android.text.TextUtils +import android.util.Log +import android.util.Pair +import androidx.media.utils.MediaConstants +import com.android.app.tracing.coroutines.traceCoroutine +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.graphics.ImageLoader +import com.android.systemui.media.controls.shared.model.MediaAction +import com.android.systemui.media.controls.shared.model.MediaButton +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.MediaDeviceData +import com.android.systemui.media.controls.util.MediaControllerFactory +import com.android.systemui.media.controls.util.MediaDataUtils +import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R +import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState +import com.android.systemui.statusbar.notification.row.HybridGroupManager +import com.android.systemui.util.kotlin.logD +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import kotlin.coroutines.coroutineContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel +import kotlinx.coroutines.ensureActive + +/** Loads media information from media style [StatusBarNotification] classes. */ +@SysUISingleton +class MediaDataLoader +@Inject +constructor( + @Application val context: Context, + @Main val mainDispatcher: CoroutineDispatcher, + @Background val backgroundScope: CoroutineScope, + private val activityStarter: ActivityStarter, + private val mediaControllerFactory: MediaControllerFactory, + private val mediaFlags: MediaFlags, + private val imageLoader: ImageLoader, + private val statusBarManager: StatusBarManager, +) { + private val mediaProcessingJobs = ConcurrentHashMap<JobKey, Job>() + + private val artworkWidth: Int = + context.resources.getDimensionPixelSize( + com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize + ) + private val artworkHeight: Int = + context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded) + + private val themeText = + com.android.settingslib.Utils.getColorAttr( + context, + com.android.internal.R.attr.textColorPrimary + ) + .defaultColor + + /** + * Loads media data for a given [StatusBarNotification]. It does the loading on the background + * thread. + * + * Returns a [MediaDataLoaderResult] if loaded data or `null` if loading failed. The method + * suspends until loading has completed or failed. + * + * If a new [loadMediaData] is issued while existing load is in progress, the existing (old) + * load will be cancelled. + */ + suspend fun loadMediaData(key: String, sbn: StatusBarNotification): MediaDataLoaderResult? { + logD(TAG) { "Loading media data for $key..." } + val jobKey = JobKey(key, sbn) + val loadMediaJob = backgroundScope.async { loadMediaDataInBackground(key, sbn) } + loadMediaJob.invokeOnCompletion { mediaProcessingJobs.remove(jobKey) } + val existingJob = mediaProcessingJobs.put(jobKey, loadMediaJob) + existingJob?.cancel("New processing job incoming.") + return loadMediaJob.await() + } + + /** Loads media data, should be called from [backgroundScope]. */ + @WorkerThread + private suspend fun loadMediaDataInBackground( + key: String, + sbn: StatusBarNotification, + ): MediaDataLoaderResult? = + traceCoroutine("MediaDataLoader#loadMediaData") { + val token = + sbn.notification.extras.getParcelable( + Notification.EXTRA_MEDIA_SESSION, + MediaSession.Token::class.java + ) + if (token == null) { + Log.i(TAG, "Token was null, not loading media info") + return null + } + val mediaController = mediaControllerFactory.create(token) + val metadata = mediaController.metadata + val notification: Notification = sbn.notification + + val appInfo = + notification.extras.getParcelable( + Notification.EXTRA_BUILDER_APPLICATION_INFO, + ApplicationInfo::class.java + ) ?: getAppInfoFromPackage(sbn.packageName) + + // App name + val appName = getAppName(sbn, appInfo) + + // Song name + var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) + if (song.isNullOrBlank()) { + song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) + } + if (song.isNullOrBlank()) { + song = HybridGroupManager.resolveTitle(notification) + } + if (song.isNullOrBlank()) { + // For apps that don't include a title, log and add a placeholder + song = context.getString(R.string.controls_media_empty_title, appName) + try { + statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier) + } catch (e: RuntimeException) { + Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}") + } + } + + // Don't attempt to load bitmaps if the job was cancelled. + coroutineContext.ensureActive() + + // Album art + var artworkBitmap = metadata?.let { loadBitmapFromUri(it) } + if (artworkBitmap == null) { + artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART) + } + if (artworkBitmap == null) { + artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) + } + val artworkIcon = + if (artworkBitmap == null) { + notification.getLargeIcon() + } else { + Icon.createWithBitmap(artworkBitmap) + } + + // Don't continue if we were cancelled during slow bitmap load. + coroutineContext.ensureActive() + + // App Icon + val smallIcon = sbn.notification.smallIcon + + // Explicit Indicator + val isExplicit = + MediaMetadataCompat.fromMediaMetadata(metadata) + ?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + + // Artist name + var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) + if (artist.isNullOrBlank()) { + artist = HybridGroupManager.resolveText(notification) + } + + // Device name (used for remote cast notifications) + val device: MediaDeviceData? = getDeviceInfoForRemoteCast(key, sbn) + + // Control buttons + // If flag is enabled and controller has a PlaybackState, create actions from session + // info + // Otherwise, use the notification actions + var actionIcons: List<MediaAction> = emptyList() + var actionsToShowCollapsed: List<Int> = emptyList() + val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user) + logD(TAG) { "Semantic actions: $semanticActions" } + if (semanticActions == null) { + val actions = createActionsFromNotification(context, activityStarter, sbn) + actionIcons = actions.first + actionsToShowCollapsed = actions.second + logD(TAG) { "[!!] Semantic actions: $semanticActions" } + } + + val playbackLocation = getPlaybackLocation(sbn, mediaController) + val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } + + val appUid = appInfo?.uid ?: Process.INVALID_UID + return MediaDataLoaderResult( + appName = appName, + appIcon = smallIcon, + artist = artist, + song = song, + artworkIcon = artworkIcon, + actionIcons = actionIcons, + actionsToShowInCompact = actionsToShowCollapsed, + semanticActions = semanticActions, + token = token, + clickIntent = notification.contentIntent, + device = device, + playbackLocation = playbackLocation, + isPlaying = isPlaying, + appUid = appUid, + isExplicit = isExplicit + ) + } + + /** + * Loads media data in background for a given set of resumption parameters. The method suspends + * until loading is complete or fails. + * + * Returns a [MediaDataLoaderResult] if loaded data or `null` if loading failed. + */ + suspend fun loadMediaDataForResumption( + userId: Int, + desc: MediaDescription, + resumeAction: Runnable, + currentEntry: MediaData?, + token: MediaSession.Token, + appName: String, + appIntent: PendingIntent, + packageName: String + ): MediaDataLoaderResult? { + val mediaData = + backgroundScope.async { + loadMediaDataForResumptionInBackground( + userId, + desc, + resumeAction, + currentEntry, + token, + appName, + appIntent, + packageName + ) + } + return mediaData.await() + } + + /** Loads media data for resumption, should be called from [backgroundScope]. */ + @WorkerThread + private suspend fun loadMediaDataForResumptionInBackground( + userId: Int, + desc: MediaDescription, + resumeAction: Runnable, + currentEntry: MediaData?, + token: MediaSession.Token, + appName: String, + appIntent: PendingIntent, + packageName: String + ): MediaDataLoaderResult? = + traceCoroutine("MediaDataLoader#loadMediaDataForResumption") { + if (desc.title.isNullOrBlank()) { + Log.e(TAG, "Description incomplete") + return null + } + + logD(TAG) { "adding track for $userId from browser: $desc" } + + val appUid = currentEntry?.appUid ?: Process.INVALID_UID + + // Album art + var artworkBitmap = desc.iconBitmap + if (artworkBitmap == null && desc.iconUri != null) { + artworkBitmap = + loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName) + } + val artworkIcon = + if (artworkBitmap != null) { + Icon.createWithBitmap(artworkBitmap) + } else { + null + } + + val isExplicit = + desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + + val progress = + if (mediaFlags.isResumeProgressEnabled()) { + MediaDataUtils.getDescriptionProgress(desc.extras) + } else null + + val mediaAction = getResumeMediaAction(resumeAction) + return MediaDataLoaderResult( + appName = appName, + appIcon = null, + artist = desc.subtitle, + song = desc.title, + artworkIcon = artworkIcon, + actionIcons = listOf(mediaAction), + actionsToShowInCompact = listOf(0), + semanticActions = MediaButton(playOrPause = mediaAction), + token = token, + clickIntent = appIntent, + device = null, + playbackLocation = 0, + isPlaying = null, + appUid = appUid, + isExplicit = isExplicit, + resumeAction = resumeAction, + resumeProgress = progress + ) + } + + private fun createActionsFromState( + packageName: String, + controller: MediaController, + user: UserHandle + ): MediaButton? { + if (!mediaFlags.areMediaSessionActionsEnabled(packageName, user)) { + return null + } + + return createActionsFromState(context, packageName, controller) + } + + private fun getPlaybackLocation(sbn: StatusBarNotification, mediaController: MediaController) = + when { + isRemoteCastNotification(sbn) -> MediaData.PLAYBACK_CAST_REMOTE + mediaController.playbackInfo?.playbackType == + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> MediaData.PLAYBACK_LOCAL + else -> MediaData.PLAYBACK_CAST_LOCAL + } + + /** + * Returns [MediaDeviceData] if the [StatusBarNotification] is a remote cast notification. + * `null` otherwise. + */ + private fun getDeviceInfoForRemoteCast( + key: String, + sbn: StatusBarNotification + ): MediaDeviceData? { + val extras = sbn.notification.extras + val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null) + val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1) + val deviceIntent = + extras.getParcelable(Notification.EXTRA_MEDIA_REMOTE_INTENT, PendingIntent::class.java) + logD(TAG) { "$key is RCN for $deviceName" } + + if (deviceName != null && deviceIcon > -1) { + // Name and icon must be present, but intent may be null + val enabled = deviceIntent != null && deviceIntent.isActivity + val deviceDrawable = + Icon.createWithResource(sbn.packageName, deviceIcon) + .loadDrawable(sbn.getPackageContext(context)) + return MediaDeviceData( + enabled, + deviceDrawable, + deviceName, + deviceIntent, + showBroadcastButton = false + ) + } + return null + } + + private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? { + try { + return context.packageManager.getApplicationInfo(packageName, 0) + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Could not get app info for $packageName", e) + return null + } + } + + private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String { + val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME) + return when { + name != null -> name + appInfo != null -> context.packageManager.getApplicationLabel(appInfo).toString() + else -> sbn.packageName + } + } + + /** Load a bitmap from the various Art metadata URIs */ + private suspend fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? { + for (uri in ART_URIS) { + val uriString = metadata.getString(uri) + if (!TextUtils.isEmpty(uriString)) { + val albumArt = loadBitmapFromUri(Uri.parse(uriString)) + // If we got cancelled during slow album art load, cancel the rest of + // the process. + coroutineContext.ensureActive() + if (albumArt != null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "loaded art from $uri") + } + return albumArt + } + } + } + return null + } + + private suspend fun loadBitmapFromUri(uri: Uri): Bitmap? { + // ImageDecoder requires a scheme of the following types + if ( + uri.scheme !in + listOf( + ContentResolver.SCHEME_CONTENT, + ContentResolver.SCHEME_ANDROID_RESOURCE, + ContentResolver.SCHEME_FILE + ) + ) { + Log.w(TAG, "Invalid album art uri $uri") + return null + } + + val source = ImageLoader.Uri(uri) + return imageLoader.loadBitmap( + source, + artworkWidth, + artworkHeight, + allocator = ImageDecoder.ALLOCATOR_SOFTWARE + ) + } + + private suspend fun loadBitmapFromUriForUser( + uri: Uri, + userId: Int, + appUid: Int, + packageName: String + ): Bitmap? { + try { + val ugm = UriGrantsManager.getService() + ugm.checkGrantUriPermission_ignoreNonSystem( + appUid, + packageName, + ContentProvider.getUriWithoutUserId(uri), + Intent.FLAG_GRANT_READ_URI_PERMISSION, + ContentProvider.getUserIdFromUri(uri, userId) + ) + return loadBitmapFromUri(uri) + } catch (e: SecurityException) { + Log.e(TAG, "Failed to get URI permission: $e") + } + return null + } + + /** Check whether this notification is an RCN */ + private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean = + sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE) + + private fun getResumeMediaAction(action: Runnable): MediaAction { + return MediaAction( + Icon.createWithResource(context, R.drawable.ic_media_play) + .setTint(themeText) + .loadDrawable(context), + action, + context.getString(R.string.controls_media_resume), + context.getDrawable(R.drawable.ic_media_play_container) + ) + } + + private data class JobKey(val key: String, val sbn: StatusBarNotification) : + Pair<String, StatusBarNotification>(key, sbn) + + companion object { + private const val TAG = "MediaDataLoader" + private val ART_URIS = + arrayOf( + MediaMetadata.METADATA_KEY_ALBUM_ART_URI, + MediaMetadata.METADATA_KEY_ART_URI, + MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI + ) + } + + /** Returned data from loader. */ + data class MediaDataLoaderResult( + val appName: String?, + val appIcon: Icon?, + val artist: CharSequence?, + val song: CharSequence?, + val artworkIcon: Icon?, + val actionIcons: List<MediaAction>, + val actionsToShowInCompact: List<Int>, + val semanticActions: MediaButton?, + val token: MediaSession.Token?, + val clickIntent: PendingIntent?, + val device: MediaDeviceData?, + val playbackLocation: Int, + val isPlaying: Boolean?, + val appUid: Int, + val isExplicit: Boolean, + val resumeAction: Runnable? = null, + val resumeProgress: Double? = null + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt index adcfba75f498..916f8b2e1730 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt @@ -73,6 +73,7 @@ import com.android.systemui.media.controls.data.repository.MediaDataRepository import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser +import com.android.systemui.media.controls.shared.MediaControlDrawables import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC import com.android.systemui.media.controls.shared.model.MediaAction @@ -90,6 +91,7 @@ import com.android.systemui.media.controls.util.SmallHash import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.BcSmartspaceDataPlugin import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.statusbar.notification.row.HybridGroupManager @@ -271,7 +273,7 @@ class MediaDataProcessor( } override fun start() { - if (!mediaFlags.isSceneContainerEnabled()) { + if (!SceneContainerFlag.isEnabled) { return } @@ -1043,14 +1045,13 @@ class MediaDataProcessor( val playOrPause = if (isConnectingState(state.state)) { // Spinner needs to be animating to render anything. Start it here. - val drawable = - context.getDrawable(com.android.internal.R.drawable.progress_small_material) + val drawable = MediaControlDrawables.getProgress(context) (drawable as Animatable).start() MediaAction( drawable, null, // no action to perform when clicked context.getString(R.string.controls_media_button_connecting), - context.getDrawable(R.drawable.ic_media_connecting_container), + MediaControlDrawables.getConnecting(context), // Specify a rebind id to prevent the spinner from restarting on later binds. com.android.internal.R.drawable.progress_small_material ) @@ -1143,23 +1144,23 @@ class MediaDataProcessor( return when (action) { PlaybackState.ACTION_PLAY -> { MediaAction( - context.getDrawable(R.drawable.ic_media_play), + MediaControlDrawables.getPlayIcon(context), { controller.transportControls.play() }, context.getString(R.string.controls_media_button_play), - context.getDrawable(R.drawable.ic_media_play_container) + MediaControlDrawables.getPlayBackground(context) ) } PlaybackState.ACTION_PAUSE -> { MediaAction( - context.getDrawable(R.drawable.ic_media_pause), + MediaControlDrawables.getPauseIcon(context), { controller.transportControls.pause() }, context.getString(R.string.controls_media_button_pause), - context.getDrawable(R.drawable.ic_media_pause_container) + MediaControlDrawables.getPauseBackground(context) ) } PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { MediaAction( - context.getDrawable(R.drawable.ic_media_prev), + MediaControlDrawables.getPrevIcon(context), { controller.transportControls.skipToPrevious() }, context.getString(R.string.controls_media_button_prev), null @@ -1167,7 +1168,7 @@ class MediaDataProcessor( } PlaybackState.ACTION_SKIP_TO_NEXT -> { MediaAction( - context.getDrawable(R.drawable.ic_media_next), + MediaControlDrawables.getNextIcon(context), { controller.transportControls.skipToNext() }, context.getString(R.string.controls_media_button_next), null @@ -1308,7 +1309,7 @@ class MediaDataProcessor( .loadDrawable(context), action, context.getString(R.string.controls_media_resume), - context.getDrawable(R.drawable.ic_media_play_container) + MediaControlDrawables.getPlayBackground(context) ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt index eab0d483c3e5..a193f7f8f498 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt @@ -39,6 +39,7 @@ import com.android.settingslib.media.PhoneMediaDevice import com.android.settingslib.media.flags.Flags import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.controls.shared.MediaControlDrawables import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.MediaDeviceData import com.android.systemui.media.controls.util.LocalMediaManagerFactory @@ -142,6 +143,7 @@ constructor( interface Listener { /** Called when the route has changed for a given notification. */ fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?) + /** Called when the notification was removed. */ fun onKeyRemoved(key: String, userInitiated: Boolean) } @@ -159,6 +161,7 @@ constructor( val token get() = controller?.sessionToken + private var started = false private var playbackType = PLAYBACK_TYPE_UNKNOWN private var playbackVolumeControlId: String? = null @@ -170,6 +173,7 @@ constructor( fgExecutor.execute { processDevice(key, oldKey, value) } } } + // A device that is not yet connected but is expected to connect imminently. Because it's // expected to connect imminently, it should be displayed as the current device. private var aboutToConnectDeviceOverride: AboutToConnectDevice? = null @@ -354,12 +358,12 @@ constructor( activeDevice = routingSession?.let { - val icon = if (it.selectedRoutes.size > 1) { - context.getDrawable( - com.android.settingslib.R.drawable.ic_media_group_device) - } else { - connectedDevice?.icon // Single route. We don't change the icon. - } + val icon = + if (it.selectedRoutes.size > 1) { + MediaControlDrawables.getGroupDevice(context) + } else { + connectedDevice?.icon // Single route. We don't change the icon. + } // For a remote session, always use the current device from // LocalMediaManager. Override with routing session information if // available: @@ -367,14 +371,16 @@ constructor( // - Icon: To show the group icon if there's more than one selected // route. connectedDevice?.copy( - name = it.name ?: connectedDevice.name, - icon = icon) - } ?: MediaDeviceData( - enabled = false, - icon = context.getDrawable(R.drawable.ic_media_home_devices), - name = context.getString(R.string.media_seamless_other_device), - showBroadcastButton = false - ) + name = it.name ?: connectedDevice.name, + icon = icon + ) + } + ?: MediaDeviceData( + enabled = false, + icon = MediaControlDrawables.getHomeDevices(context), + name = context.getString(R.string.media_seamless_other_device), + showBroadcastButton = false + ) } else { // Prefer SASS if available when playback is local. activeDevice = getSassDevice() ?: connectedDevice @@ -434,10 +440,7 @@ constructor( return if (enableLeAudioSharing()) { MediaDeviceData( enabled = false, - icon = - context.getDrawable( - com.android.settingslib.R.drawable.ic_bt_le_audio_sharing - ), + icon = MediaControlDrawables.getLeAudioSharing(context), name = context.getString(R.string.audio_sharing_description), intent = null, showBroadcastButton = false @@ -445,13 +448,14 @@ constructor( } else { MediaDeviceData( enabled = true, - icon = context.getDrawable(R.drawable.settings_input_antenna), + icon = MediaControlDrawables.getAntenna(context), name = broadcastDescription, intent = null, showBroadcastButton = true ) } } + /** Return a display name for the current device / route, or null if not possible */ private fun getDeviceName( device: MediaDevice?, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt index 9d7160cbaffc..270ab72e291d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt @@ -105,7 +105,7 @@ constructor( val currentMedia: StateFlow<List<MediaCommonModel>> = mediaFilterRepository.currentMedia override fun start() { - if (!mediaFlags.isSceneContainerEnabled()) { + if (!SceneContainerFlag.isEnabled) { return } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaControlDrawables.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaControlDrawables.kt index 28ee668088c1..c78220e42d1a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaControlDrawables.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaControlDrawables.kt @@ -45,128 +45,139 @@ object MediaControlDrawables { private var solid: Drawable? = null fun getProgress(context: Context): Drawable? { - return progress + if (!mediaControlsDrawablesReuse()) { + return context.getDrawable(com.android.internal.R.drawable.progress_small_material) + } + return progress?.mutate() ?: context.getDrawable(com.android.internal.R.drawable.progress_small_material).also { - if (!mediaControlsDrawablesReuse()) return@also progress = it } } fun getConnecting(context: Context): Drawable? { - return connecting + if (!mediaControlsDrawablesReuse()) { + return context.getDrawable(R.drawable.ic_media_connecting_container) + } + return connecting?.mutate() ?: context.getDrawable(R.drawable.ic_media_connecting_container).also { - if (!mediaControlsDrawablesReuse()) return@also connecting = it } } fun getPlayIcon(context: Context): AnimatedVectorDrawable? { + if (!mediaControlsDrawablesReuse()) { + return context.getDrawable(R.drawable.ic_media_play) as AnimatedVectorDrawable? + } return playIcon?.let { it.reset() - it + it.mutate() as AnimatedVectorDrawable } ?: (context.getDrawable(R.drawable.ic_media_play) as AnimatedVectorDrawable?).also { - if (!mediaControlsDrawablesReuse()) return@also playIcon = it } } fun getPlayBackground(context: Context): AnimatedVectorDrawable? { + if (!mediaControlsDrawablesReuse()) { + return context.getDrawable(R.drawable.ic_media_play_container) + as AnimatedVectorDrawable? + } return playBackground?.let { it.reset() - it + it.mutate() as AnimatedVectorDrawable } ?: (context.getDrawable(R.drawable.ic_media_play_container) as AnimatedVectorDrawable?) - .also { - if (!mediaControlsDrawablesReuse()) return@also - playBackground = it - } + .also { playBackground = it } } fun getPauseIcon(context: Context): AnimatedVectorDrawable? { + if (!mediaControlsDrawablesReuse()) { + return context.getDrawable(R.drawable.ic_media_pause) as AnimatedVectorDrawable? + } return pauseIcon?.let { it.reset() - it + it.mutate() as AnimatedVectorDrawable } ?: (context.getDrawable(R.drawable.ic_media_pause) as AnimatedVectorDrawable?).also { - if (!mediaControlsDrawablesReuse()) return@also pauseIcon = it } } fun getPauseBackground(context: Context): AnimatedVectorDrawable? { + if (!mediaControlsDrawablesReuse()) { + return context.getDrawable(R.drawable.ic_media_pause_container) + as AnimatedVectorDrawable? + } return pauseBackground?.let { it.reset() - it + it.mutate() as AnimatedVectorDrawable } ?: (context.getDrawable(R.drawable.ic_media_pause_container) as AnimatedVectorDrawable?) - .also { - if (!mediaControlsDrawablesReuse()) return@also - pauseBackground = it - } + .also { pauseBackground = it } } fun getNextIcon(context: Context): Drawable? { - return nextIcon - ?: context.getDrawable(R.drawable.ic_media_next).also { - if (!mediaControlsDrawablesReuse()) return@also - nextIcon = it - } + if (!mediaControlsDrawablesReuse()) { + return context.getDrawable(R.drawable.ic_media_next) + } + return nextIcon ?: context.getDrawable(R.drawable.ic_media_next).also { nextIcon = it } } fun getPrevIcon(context: Context): Drawable? { - return prevIcon - ?: context.getDrawable(R.drawable.ic_media_prev).also { - if (!mediaControlsDrawablesReuse()) return@also - prevIcon = it - } + if (!mediaControlsDrawablesReuse()) { + return context.getDrawable(R.drawable.ic_media_prev) + } + return prevIcon ?: context.getDrawable(R.drawable.ic_media_prev).also { prevIcon = it } } fun getLeAudioSharing(context: Context): Drawable? { + if (!mediaControlsDrawablesReuse()) { + return context.getDrawable(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing) + } return leAudioSharing ?: context.getDrawable(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing).also { - if (!mediaControlsDrawablesReuse()) return@also leAudioSharing = it } } fun getAntenna(context: Context): Drawable? { + if (!mediaControlsDrawablesReuse()) { + return context.getDrawable(R.drawable.settings_input_antenna) + } return antenna - ?: context.getDrawable(R.drawable.settings_input_antenna).also { - if (!mediaControlsDrawablesReuse()) return@also - antenna = it - } + ?: context.getDrawable(R.drawable.settings_input_antenna).also { antenna = it } } fun getGroupDevice(context: Context): Drawable? { + if (!mediaControlsDrawablesReuse()) { + return context.getDrawable(com.android.settingslib.R.drawable.ic_media_group_device) + } return groupDevice ?: context.getDrawable(com.android.settingslib.R.drawable.ic_media_group_device).also { - if (!mediaControlsDrawablesReuse()) return@also groupDevice = it } } fun getHomeDevices(context: Context): Drawable? { + if (!mediaControlsDrawablesReuse()) { + return context.getDrawable(R.drawable.ic_media_home_devices) + } return homeDevices - ?: context.getDrawable(R.drawable.ic_media_home_devices).also { - if (!mediaControlsDrawablesReuse()) return@also - homeDevices = it - } + ?: context.getDrawable(R.drawable.ic_media_home_devices).also { homeDevices = it } } fun getOutline(context: Context): Drawable? { + if (!mediaControlsDrawablesReuse()) { + return context.getDrawable(R.drawable.qs_media_outline_button) + } return outline - ?: context.getDrawable(R.drawable.qs_media_outline_button).also { - if (!mediaControlsDrawablesReuse()) return@also - outline = it - } + ?: context.getDrawable(R.drawable.qs_media_outline_button).also { outline = it } } fun getSolid(context: Context): Drawable? { - return solid - ?: context.getDrawable(R.drawable.qs_media_solid_button).also { - if (!mediaControlsDrawablesReuse()) return@also - solid = it - } + if (!mediaControlsDrawablesReuse()) { + return context.getDrawable(R.drawable.qs_media_solid_button) + } + return solid ?: context.getDrawable(R.drawable.qs_media_solid_button).also { solid = it } } } 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 fb2bbde37a18..19cdee7befdd 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 @@ -220,7 +220,7 @@ constructor( private val animationScaleObserver: ContentObserver = object : ContentObserver(executor, 0) { override fun onChange(selfChange: Boolean) { - if (!mediaFlags.isSceneContainerEnabled()) { + if (!SceneContainerFlag.isEnabled) { MediaPlayerData.players().forEach { it.updateAnimatorDurationScale() } } else { controllerById.values.forEach { it.updateAnimatorDurationScale() } @@ -350,7 +350,7 @@ constructor( inflateSettingsButton() mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) configurationController.addCallback(configListener) - if (!mediaFlags.isSceneContainerEnabled()) { + if (!SceneContainerFlag.isEnabled) { setUpListeners() } else { val visualStabilityCallback = OnReorderingAllowedListener { @@ -391,7 +391,7 @@ constructor( listenForAnyStateToGoneKeyguardTransition(this) listenForAnyStateToLockscreenTransition(this) - if (!mediaFlags.isSceneContainerEnabled()) return@repeatOnLifecycle + if (!SceneContainerFlag.isEnabled) return@repeatOnLifecycle listenForMediaItemsChanges(this) } } @@ -733,7 +733,7 @@ constructor( when (commonViewModel) { is MediaCommonViewModel.MediaControl -> { val viewHolder = MediaViewHolder.create(LayoutInflater.from(context), mediaContent) - if (mediaFlags.isSceneContainerEnabled()) { + if (SceneContainerFlag.isEnabled) { viewController.widthInSceneContainerPx = widthInSceneContainerPx viewController.heightInSceneContainerPx = heightInSceneContainerPx } @@ -965,7 +965,7 @@ constructor( .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) if (existingPlayer == null) { val newPlayer = mediaControlPanelFactory.get() - if (mediaFlags.isSceneContainerEnabled()) { + if (SceneContainerFlag.isEnabled) { newPlayer.mediaViewController.widthInSceneContainerPx = widthInSceneContainerPx newPlayer.mediaViewController.heightInSceneContainerPx = heightInSceneContainerPx @@ -1140,7 +1140,7 @@ constructor( } private fun updatePlayers(recreateMedia: Boolean) { - if (mediaFlags.isSceneContainerEnabled()) { + if (SceneContainerFlag.isEnabled) { updateMediaPlayers(recreateMedia) return } @@ -1240,7 +1240,7 @@ constructor( currentStartLocation = startLocation currentEndLocation = endLocation currentTransitionProgress = progress - if (!mediaFlags.isSceneContainerEnabled()) { + if (!SceneContainerFlag.isEnabled) { for (mediaPlayer in MediaPlayerData.players()) { updateViewControllerToState(mediaPlayer.mediaViewController, immediately) } @@ -1300,7 +1300,7 @@ constructor( /** Update listening to seekbar. */ private fun updateSeekbarListening(visibleToUser: Boolean) { - if (!mediaFlags.isSceneContainerEnabled()) { + if (!SceneContainerFlag.isEnabled) { for (player in MediaPlayerData.players()) { player.setListening(visibleToUser && currentlyExpanded) } @@ -1313,7 +1313,7 @@ constructor( private fun updateCarouselDimensions() { var width = 0 var height = 0 - if (!mediaFlags.isSceneContainerEnabled()) { + if (!SceneContainerFlag.isEnabled) { for (mediaPlayer in MediaPlayerData.players()) { val controller = mediaPlayer.mediaViewController // When transitioning the view to gone, the view gets smaller, but the translation @@ -1405,7 +1405,7 @@ constructor( !mediaManager.hasActiveMediaOrRecommendation() && desiredHostState.showsOnlyActiveMedia - if (!mediaFlags.isSceneContainerEnabled()) { + if (!SceneContainerFlag.isEnabled) { for (mediaPlayer in MediaPlayerData.players()) { if (animate) { mediaPlayer.mediaViewController.animatePendingStateChange( @@ -1445,7 +1445,7 @@ constructor( } fun closeGuts(immediate: Boolean = true) { - if (!mediaFlags.isSceneContainerEnabled()) { + if (!SceneContainerFlag.isEnabled) { MediaPlayerData.players().forEach { it.closeGuts(immediate) } } else { controllerById.values.forEach { it.closeGuts(immediate) } @@ -1596,7 +1596,7 @@ constructor( @VisibleForTesting fun onSwipeToDismiss() { - if (mediaFlags.isSceneContainerEnabled()) { + if (SceneContainerFlag.isEnabled) { mediaCarouselViewModel.onSwipeToDismiss(currentEndLocation) return } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java index addb0147889f..87610cf774a3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java @@ -111,7 +111,6 @@ import com.android.systemui.media.controls.ui.view.MediaViewHolder; import com.android.systemui.media.controls.ui.view.RecommendationViewHolder; import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel; import com.android.systemui.media.controls.util.MediaDataUtils; -import com.android.systemui.media.controls.util.MediaFlags; import com.android.systemui.media.controls.util.MediaUiEventLogger; import com.android.systemui.media.controls.util.SmallHash; import com.android.systemui.media.dialog.MediaOutputDialogManager; @@ -120,6 +119,7 @@ import com.android.systemui.monet.Style; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.res.R; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -209,7 +209,6 @@ public class MediaControlPanel { static final long TURBULENCE_NOISE_PLAY_DURATION = 7500L; private final SeekBarViewModel mSeekBarViewModel; - private final MediaFlags mMediaFlags; private final CommunalSceneInteractor mCommunalSceneInteractor; private SeekBarObserver mSeekBarObserver; protected final Executor mBackgroundExecutor; @@ -323,8 +322,7 @@ public class MediaControlPanel { CommunalSceneInteractor communalSceneInteractor, NotificationLockscreenUserManager lockscreenUserManager, BroadcastDialogController broadcastDialogController, - GlobalSettings globalSettings, - MediaFlags mediaFlags + GlobalSettings globalSettings ) { mContext = context; mBackgroundExecutor = backgroundExecutor; @@ -343,7 +341,6 @@ public class MediaControlPanel { mActivityIntentHelper = activityIntentHelper; mLockscreenUserManager = lockscreenUserManager; mBroadcastDialogController = broadcastDialogController; - mMediaFlags = mediaFlags; mCommunalSceneInteractor = communalSceneInteractor; mSeekBarViewModel.setLogSeek(() -> { @@ -641,7 +638,7 @@ public class MediaControlPanel { // State refresh interferes with the translation animation, only run it if it's not running. if (!mMetadataAnimationHandler.isRunning()) { // Don't refresh in scene framework, because it will calculate with invalid layout sizes - if (!mMediaFlags.isSceneContainerEnabled()) { + if (!SceneContainerFlag.isEnabled()) { mMediaViewController.refreshState(); } } @@ -909,7 +906,7 @@ public class MediaControlPanel { // Capture width & height from views in foreground for artwork scaling in background int width = mMediaViewHolder.getAlbumView().getMeasuredWidth(); int height = mMediaViewHolder.getAlbumView().getMeasuredHeight(); - if (mMediaFlags.isSceneContainerEnabled() && (width <= 0 || height <= 0)) { + if (SceneContainerFlag.isEnabled() && (width <= 0 || height <= 0)) { // TODO(b/312714128): ensure we have a valid size before setting background width = mMediaViewController.getWidthInSceneContainerPx(); height = mMediaViewController.getHeightInSceneContainerPx(); diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt index 091b886c7ba4..a9d2a541a241 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt @@ -46,10 +46,10 @@ import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.media.controls.ui.view.MediaHost -import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.media.dream.MediaDreamComplication import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.CrossFadeHelper import com.android.systemui.statusbar.StatusBarState @@ -119,7 +119,6 @@ constructor( @Application private val coroutineScope: CoroutineScope, private val splitShadeStateController: SplitShadeStateController, private val logger: MediaViewLogger, - private val mediaFlags: MediaFlags, ) { /** Track the media player setting status on lock screen. */ @@ -1111,7 +1110,7 @@ constructor( private fun updateHostAttachment() = traceSection("MediaHierarchyManager#updateHostAttachment") { - if (mediaFlags.isSceneContainerEnabled()) { + if (SceneContainerFlag.isEnabled) { // No need to manage transition states - just update the desired location directly logger.logMediaHostAttachment(desiredLocation) mediaCarouselController.onDesiredLocationChanged( diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt index 584908ff2aad..e57de09f1063 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt @@ -46,8 +46,8 @@ import com.android.systemui.media.controls.ui.view.MediaViewHolder import com.android.systemui.media.controls.ui.view.RecommendationViewHolder import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel -import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.surfaceeffects.PaintDrawCallback import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect @@ -82,7 +82,6 @@ constructor( private val logger: MediaViewLogger, private val seekBarViewModel: SeekBarViewModel, @Main private val mainExecutor: DelayableExecutor, - private val mediaFlags: MediaFlags, private val globalSettings: GlobalSettings, ) { @@ -125,7 +124,7 @@ constructor( set(value) { if (field != value) { field = value - if (!mediaFlags.isSceneContainerEnabled()) return + if (!SceneContainerFlag.isEnabled) return locationChangeListener(value) } } @@ -212,7 +211,7 @@ constructor( private val scrubbingChangeListener = object : SeekBarViewModel.ScrubbingChangeListener { override fun onScrubbingChanged(scrubbing: Boolean) { - if (!mediaFlags.isSceneContainerEnabled()) return + if (!SceneContainerFlag.isEnabled) return if (isScrubbing == scrubbing) return isScrubbing = scrubbing updateDisplayForScrubbingChange() @@ -222,7 +221,7 @@ constructor( private val enabledChangeListener = object : SeekBarViewModel.EnabledChangeListener { override fun onEnabledChanged(enabled: Boolean) { - if (!mediaFlags.isSceneContainerEnabled()) return + if (!SceneContainerFlag.isEnabled) return if (isSeekBarEnabled == enabled) return isSeekBarEnabled = enabled MediaControlViewBinder.updateSeekBarVisibility(expandedLayout, isSeekBarEnabled) @@ -238,7 +237,7 @@ constructor( * @param listening True when player should be active. Otherwise, false. */ fun setListening(listening: Boolean) { - if (!mediaFlags.isSceneContainerEnabled()) return + if (!SceneContainerFlag.isEnabled) return seekBarViewModel.listening = listening } @@ -272,7 +271,7 @@ constructor( ) ) } - if (mediaFlags.isSceneContainerEnabled()) { + if (SceneContainerFlag.isEnabled) { if ( this@MediaViewController::recsConfigurationChangeListener.isInitialized ) { @@ -344,7 +343,7 @@ constructor( * Notify this controller that the view has been removed and all listeners should be destroyed */ fun onDestroy() { - if (mediaFlags.isSceneContainerEnabled()) { + if (SceneContainerFlag.isEnabled) { if (this::seekBarObserver.isInitialized) { seekBarViewModel.progress.removeObserver(seekBarObserver) } @@ -565,7 +564,7 @@ constructor( state: MediaHostState?, isGutsAnimation: Boolean = false ): TransitionViewState? { - if (mediaFlags.isSceneContainerEnabled()) { + if (SceneContainerFlag.isEnabled) { return obtainSceneContainerViewState() } @@ -667,7 +666,7 @@ constructor( } fun attachPlayer(mediaViewHolder: MediaViewHolder) { - if (!mediaFlags.isSceneContainerEnabled()) return + if (!SceneContainerFlag.isEnabled) return this.mediaViewHolder = mediaViewHolder // Setting up seek bar. @@ -741,7 +740,7 @@ constructor( } fun updateAnimatorDurationScale() { - if (!mediaFlags.isSceneContainerEnabled()) return + if (!SceneContainerFlag.isEnabled) return if (this::seekBarObserver.isInitialized) { seekBarObserver.animationEnabled = globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) > 0f @@ -801,7 +800,7 @@ constructor( } fun attachRecommendations(recommendationViewHolder: RecommendationViewHolder) { - if (!mediaFlags.isSceneContainerEnabled()) return + if (!SceneContainerFlag.isEnabled) return this.recommendationViewHolder = recommendationViewHolder attach(recommendationViewHolder.recommendations, TYPE.RECOMMENDATION) @@ -810,13 +809,13 @@ constructor( } fun bindSeekBar(onSeek: () -> Unit, onBindSeekBar: (SeekBarViewModel) -> Unit) { - if (!mediaFlags.isSceneContainerEnabled()) return + if (!SceneContainerFlag.isEnabled) return seekBarViewModel.logSeek = onSeek onBindSeekBar(seekBarViewModel) } fun setUpTurbulenceNoise() { - if (!mediaFlags.isSceneContainerEnabled()) return + if (!SceneContainerFlag.isEnabled) return mediaViewHolder!!.let { if (!this::turbulenceNoiseAnimationConfig.isInitialized) { turbulenceNoiseAnimationConfig = @@ -1049,7 +1048,7 @@ constructor( */ private fun obtainViewStateForLocation(@MediaLocation location: Int): TransitionViewState? { val mediaHostState = mediaHostStatesManager.mediaHostStates[location] ?: return null - if (mediaFlags.isSceneContainerEnabled()) { + if (SceneContainerFlag.isEnabled) { return obtainSceneContainerViewState() } @@ -1080,7 +1079,7 @@ constructor( /** Clear all existing measurements and refresh the state to match the view. */ fun refreshState() = traceSection("MediaViewController#refreshState") { - if (mediaFlags.isSceneContainerEnabled()) { + if (SceneContainerFlag.isEnabled) { // We don't need to recreate measurements for scene container, since it's a known // size. Just get the view state and update the layout controller obtainSceneContainerViewState()?.let { @@ -1169,13 +1168,13 @@ constructor( } fun setUpPrevButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) { - if (!mediaFlags.isSceneContainerEnabled()) return + if (!SceneContainerFlag.isEnabled) return isPrevButtonAvailable = isAvailable prevNotVisibleValue = notVisibleValue } fun setUpNextButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) { - if (!mediaFlags.isSceneContainerEnabled()) return + if (!SceneContainerFlag.isEnabled) return isNextButtonAvailable = isAvailable nextNotVisibleValue = notVisibleValue } 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 64820e0d0ced..f4601340ee42 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 @@ -30,6 +30,7 @@ import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.media.controls.domain.pipeline.interactor.MediaControlInteractor +import com.android.systemui.media.controls.shared.MediaControlDrawables import com.android.systemui.media.controls.shared.model.MediaAction import com.android.systemui.media.controls.shared.model.MediaButton import com.android.systemui.media.controls.shared.model.MediaControlModel @@ -284,9 +285,9 @@ class MediaControlViewModel( }, cancelTextBackground = if (model.isDismissible) { - applicationContext.getDrawable(R.drawable.qs_media_outline_button) + MediaControlDrawables.getOutline(applicationContext) } else { - applicationContext.getDrawable(R.drawable.qs_media_solid_button) + MediaControlDrawables.getSolid(applicationContext) }, onSettingsClicked = { logger.logLongPressSettings(model.uid, model.packageName, model.instanceId) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt index 21c311191710..a65243dfe315 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt @@ -21,7 +21,6 @@ import android.os.UserHandle import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags -import com.android.systemui.scene.shared.flag.SceneContainerFlag import javax.inject.Inject @SysUISingleton @@ -49,7 +48,4 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlagsClass /** Check whether we allow remote media to generate resume controls */ fun isRemoteResumeAllowed() = featureFlags.isEnabled(Flags.MEDIA_REMOTE_RESUME) - - /** Check whether to use scene framework */ - fun isSceneContainerEnabled() = SceneContainerFlag.isEnabled } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt index 2dbe2aa4ef2d..bf2aa7efc0c4 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt @@ -20,7 +20,7 @@ import android.annotation.ColorInt import android.annotation.UserIdInt import android.app.ActivityManager.RecentTaskInfo import android.content.ComponentName -import com.android.wm.shell.util.SplitBounds +import com.android.wm.shell.shared.split.SplitBounds data class RecentTask( val taskId: Int, diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt index 01b1be9d02b7..82e58cc7f1d9 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt @@ -23,7 +23,7 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.settings.UserTracker import com.android.systemui.util.kotlin.getOrNull import com.android.wm.shell.recents.RecentTasks -import com.android.wm.shell.util.GroupedRecentTaskInfo +import com.android.wm.shell.shared.GroupedRecentTaskInfo import java.util.Optional import java.util.concurrent.Executor import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt index dd1fa76c65c9..bb4d89457b58 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt @@ -36,10 +36,10 @@ import com.android.systemui.mediaprojection.appselector.view.RecentTasksAdapter. import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider.TaskPreviewSizeListener import com.android.systemui.res.R import com.android.systemui.util.recycler.HorizontalSpacerItemDecoration +import com.android.wm.shell.shared.split.SplitBounds 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.splitscreen.SplitScreen -import com.android.wm.shell.util.SplitBounds import java.util.Optional import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt index 42f66cca2522..7d2a1e178dfc 100644 --- a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt +++ b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt @@ -18,7 +18,6 @@ package com.android.systemui.model import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey -import com.android.systemui.Flags.glanceableHubBackGesture import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor @@ -107,10 +106,7 @@ constructor( { it.scene == Scenes.Lockscreen && it.invisibleDueToOcclusion }, - SYSUI_STATE_COMMUNAL_HUB_SHOWING to - { - glanceableHubBackGesture() && it.scene == Scenes.Communal - } + SYSUI_STATE_COMMUNAL_HUB_SHOWING to { it.scene == Scenes.Communal } ) } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java index ac878c2d698d..6f82d5dfff15 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java @@ -19,19 +19,24 @@ package com.android.systemui.navigationbar; import static android.app.StatusBarManager.WINDOW_NAVIGATION_BAR; import static android.app.StatusBarManager.WindowVisibleState; import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU; +import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR; import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS; import static android.view.WindowInsetsController.APPEARANCE_OPAQUE_NAVIGATION_BARS; import static android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_NAVIGATION_BARS; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.DEFAULT; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; import static com.android.systemui.accessibility.SystemActions.SYSTEM_ACTION_ID_ACCESSIBILITY_BUTTON; import static com.android.systemui.accessibility.SystemActions.SYSTEM_ACTION_ID_ACCESSIBILITY_BUTTON_CHOOSER; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE; import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT; import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT; import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_OPAQUE; import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT; import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_TRANSPARENT; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE; import android.content.ContentResolver; import android.content.Context; @@ -60,10 +65,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; -import com.android.internal.accessibility.common.ShortcutConstants; +import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.Dumpable; import com.android.systemui.accessibility.AccessibilityButtonModeObserver; import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver; +import com.android.systemui.accessibility.AccessibilityGestureTargetsObserver; import com.android.systemui.accessibility.SystemActions; import com.android.systemui.assist.AssistManager; import com.android.systemui.dagger.SysUISingleton; @@ -107,6 +113,7 @@ public final class NavBarHelper implements AccessibilityManager.AccessibilityServicesStateChangeListener, AccessibilityButtonModeObserver.ModeChangedListener, AccessibilityButtonTargetsObserver.TargetsChangedListener, + AccessibilityGestureTargetsObserver.TargetsChangedListener, OverviewProxyService.OverviewProxyListener, NavigationModeController.ModeChangedListener, Dumpable, CommandQueue.Callbacks, ConfigurationController.ConfigurationListener { private static final String TAG = NavBarHelper.class.getSimpleName(); @@ -122,6 +129,7 @@ public final class NavBarHelper implements private final SystemActions mSystemActions; private final AccessibilityButtonModeObserver mAccessibilityButtonModeObserver; private final AccessibilityButtonTargetsObserver mAccessibilityButtonTargetsObserver; + private final AccessibilityGestureTargetsObserver mAccessibilityGestureTargetsObserver; private final List<NavbarTaskbarStateUpdater> mStateListeners = new ArrayList<>(); private final Context mContext; private final NotificationShadeWindowController mNotificationShadeWindowController; @@ -188,6 +196,7 @@ public final class NavBarHelper implements public NavBarHelper(Context context, AccessibilityManager accessibilityManager, AccessibilityButtonModeObserver accessibilityButtonModeObserver, AccessibilityButtonTargetsObserver accessibilityButtonTargetsObserver, + AccessibilityGestureTargetsObserver accessibilityGestureTargetsObserver, SystemActions systemActions, OverviewProxyService overviewProxyService, Lazy<AssistManager> assistManagerLazy, @@ -220,6 +229,7 @@ public final class NavBarHelper implements mSystemActions = systemActions; mAccessibilityButtonModeObserver = accessibilityButtonModeObserver; mAccessibilityButtonTargetsObserver = accessibilityButtonTargetsObserver; + mAccessibilityGestureTargetsObserver = accessibilityGestureTargetsObserver; mWm = wm; mDefaultDisplayId = displayTracker.getDefaultDisplayId(); mEdgeBackGestureHandler = edgeBackGestureHandlerFactory.create(context); @@ -249,6 +259,7 @@ public final class NavBarHelper implements mAccessibilityManager.addAccessibilityServicesStateChangeListener(this); mAccessibilityButtonModeObserver.addListener(this); mAccessibilityButtonTargetsObserver.addListener(this); + mAccessibilityGestureTargetsObserver.addListener(this); // Setup assistant listener mContentResolver.registerContentObserver( @@ -291,6 +302,7 @@ public final class NavBarHelper implements mAccessibilityManager.removeAccessibilityServicesStateChangeListener(this); mAccessibilityButtonModeObserver.removeListener(this); mAccessibilityButtonTargetsObserver.removeListener(this); + mAccessibilityGestureTargetsObserver.removeListener(this); // Clean up assistant listeners mContentResolver.unregisterContentObserver(mAssistContentObserver); @@ -380,43 +392,50 @@ public final class NavBarHelper implements } @Override + public void onAccessibilityGestureTargetsChanged(String targets) { + updateA11yState(); + } + + @Override public void onConfigChanged(Configuration newConfig) { mEdgeBackGestureHandler.onConfigurationChanged(newConfig); } + private int getNumOfA11yShortcutTargetsForNavSystem() { + final int buttonMode = mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode(); + final int shortcutType; + if (!android.provider.Flags.a11yStandaloneGestureEnabled()) { + shortcutType = buttonMode + != ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU ? SOFTWARE : DEFAULT; + // If accessibility button is floating menu mode, there are no clickable targets. + } else { + if (mNavBarMode == NAV_BAR_MODE_GESTURAL) { + shortcutType = GESTURE; + } else { + shortcutType = buttonMode == ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR + ? SOFTWARE : DEFAULT; + } + } + return mAccessibilityManager.getAccessibilityShortcutTargets(shortcutType).size(); + } + /** * Updates the current accessibility button state. The accessibility button state is only * used for {@link Secure#ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR} and * {@link Secure#ACCESSIBILITY_BUTTON_MODE_GESTURE}, otherwise it is reset to 0. */ - private void updateA11yState() { + @VisibleForTesting + void updateA11yState() { final long prevState = mA11yButtonState; final boolean clickable; final boolean longClickable; - if (mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode() - == ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU) { - // If accessibility button is floating menu mode, click and long click state should be - // disabled. - clickable = false; - longClickable = false; - mA11yButtonState = 0; - } else { - // AccessibilityManagerService resolves services for the current user since the local - // AccessibilityManager is created from a Context with the INTERACT_ACROSS_USERS - // permission - final List<String> a11yButtonTargets = - mAccessibilityManager.getAccessibilityShortcutTargets( - ShortcutConstants.UserShortcutType.SOFTWARE); - final int requestingServices = a11yButtonTargets.size(); - - clickable = requestingServices >= 1; - - // `longClickable` is used to determine whether to pop up the accessibility chooser - // dialog or not, and it’s also only for multiple services. - longClickable = requestingServices >= 2; - mA11yButtonState = (clickable ? SYSUI_STATE_A11Y_BUTTON_CLICKABLE : 0) - | (longClickable ? SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE : 0); - } + int clickableServices = getNumOfA11yShortcutTargetsForNavSystem(); + clickable = clickableServices >= 1; + // `longClickable` is used to determine whether to pop up the accessibility chooser + // dialog or not, and it’s also only for multiple services. + longClickable = clickableServices >= 2; + mA11yButtonState = (clickable ? SYSUI_STATE_A11Y_BUTTON_CLICKABLE : 0) + | (longClickable ? SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE : 0); // Update the system actions if the state has changed if (prevState != mA11yButtonState) { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java index cb0bb4abb423..e44069f78ee3 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java @@ -563,10 +563,6 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, } @Override - public void onRecentsAnimationStateChanged(boolean running) { - } - - @Override public void onNavigationModeChanged(int mode) { mNavigationMode = mode; mEdgeBackGestureHandler.onNavigationModeChanged(mode); 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 9f9c8e910f20..c3274b7ca28b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -37,12 +37,18 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CustomAccessibilityAction +import androidx.compose.ui.semantics.customActions +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.round import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -50,6 +56,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.compose.modifiers.height import com.android.compose.modifiers.padding +import com.android.compose.modifiers.thenIf import com.android.compose.theme.PlatformTheme import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.lifecycle.repeatWhenAttached @@ -59,6 +66,7 @@ import com.android.systemui.media.dagger.MediaModule.QS_PANEL import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL import com.android.systemui.plugins.qs.QS import com.android.systemui.plugins.qs.QSContainerController +import com.android.systemui.qs.composefragment.ui.notificationScrimClip import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel import com.android.systemui.qs.flags.QSComposeFragment import com.android.systemui.qs.footer.ui.compose.FooterActions @@ -100,6 +108,17 @@ constructor( private val qqsPositionOnRoot = Rect() private val composeViewPositionOnScreen = Rect() + // Inside object for namespacing + private val notificationScrimClippingParams = + object { + var isEnabled by mutableStateOf(false) + var leftInset by mutableStateOf(0) + var rightInset by mutableStateOf(0) + var top by mutableStateOf(0) + var bottom by mutableStateOf(0) + var radius by mutableStateOf(0) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -126,7 +145,18 @@ constructor( AnimatedVisibility( visible = visible, - modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars) + modifier = + Modifier.windowInsetsPadding(WindowInsets.navigationBars).thenIf( + notificationScrimClippingParams.isEnabled + ) { + Modifier.notificationScrimClip( + notificationScrimClippingParams.leftInset, + notificationScrimClippingParams.top, + notificationScrimClippingParams.rightInset, + notificationScrimClippingParams.bottom, + notificationScrimClippingParams.radius, + ) + } ) { AnimatedContent(targetState = qsState) { when (it) { @@ -239,7 +269,7 @@ constructor( } override fun setCollapseExpandAction(action: Runnable?) { - // Nothing to do yet. But this should be wired to a11y + viewModel.collapseExpandAccessibilityAction = action } override fun getHeightDiff(): Int { @@ -280,7 +310,16 @@ constructor( cornerRadius: Int, visible: Boolean, fullWidth: Boolean - ) {} + ) { + notificationScrimClippingParams.isEnabled = visible + notificationScrimClippingParams.top = top + notificationScrimClippingParams.bottom = bottom + // Full width means that QS will show in the entire width allocated to it (for example + // phone) vs. showing in a narrower column (for example, tablet portrait). + notificationScrimClippingParams.leftInset = if (fullWidth) 0 else leftInset + notificationScrimClippingParams.rightInset = if (fullWidth) 0 else rightInset + notificationScrimClippingParams.radius = cornerRadius + } override fun isFullyCollapsed(): Boolean { return viewModel.qsExpansionValue <= 0f @@ -384,6 +423,9 @@ constructor( layout(placeable.width, placeable.height) { placeable.place(0, 0) } } .padding(top = { qqsPadding }) + .collapseExpandSemanticAction( + stringResource(id = R.string.accessibility_quick_settings_expand) + ) ) Spacer(modifier = Modifier.weight(1f)) } @@ -393,7 +435,12 @@ constructor( private fun QuickSettingsElement() { val qqsPadding by viewModel.qqsHeaderHeight.collectAsStateWithLifecycle() val qsExtraPadding = dimensionResource(R.dimen.qs_panel_padding_top) - Column { + Column( + modifier = + Modifier.collapseExpandSemanticAction( + stringResource(id = R.string.accessibility_quick_settings_collapse) + ) + ) { Box(modifier = Modifier.fillMaxSize().weight(1f)) { Column { Spacer(modifier = Modifier.height { qqsPadding + qsExtraPadding.roundToPx() }) @@ -409,6 +456,20 @@ constructor( } } } + + private fun Modifier.collapseExpandSemanticAction(label: String): Modifier { + return viewModel.collapseExpandAccessibilityAction?.let { + semantics { + customActions = + listOf( + CustomAccessibilityAction(label) { + it.run() + true + } + ) + } + } ?: this + } } private fun View.setBackPressedDispatcher() { 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 new file mode 100644 index 000000000000..93c6445b78ef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt @@ -0,0 +1,117 @@ +/* + * 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.graphics.ClipOp +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.asAndroidPath +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.platform.InspectorInfo + +/** + * 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( + leftInset: Int, + top: Int, + rightInset: Int, + bottom: Int, + radius: Int +): Modifier { + return this then NotificationScrimClipElement(leftInset, top, rightInset, bottom, radius) +} + +private class NotificationScrimClipNode( + var leftInset: Float, + var top: Float, + var rightInset: Float, + var bottom: Float, + var radius: Float, +) : DrawModifierNode, Modifier.Node() { + private val path = Path() + + var invalidated = true + + override fun ContentDrawScope.draw() { + if (invalidated) { + path.rewind() + path + .asAndroidPath() + .addRoundRect( + -leftInset, + top, + size.width + rightInset, + bottom, + radius, + radius, + android.graphics.Path.Direction.CW + ) + invalidated = false + } + clipPath(path, ClipOp.Difference) { this@draw.drawContent() } + } +} + +private data class NotificationScrimClipElement( + val leftInset: Int, + val top: Int, + val rightInset: Int, + val bottom: Int, + val radius: Int, +) : ModifierNodeElement<NotificationScrimClipNode>() { + override fun create(): NotificationScrimClipNode { + return NotificationScrimClipNode( + leftInset.toFloat(), + top.toFloat(), + rightInset.toFloat(), + bottom.toFloat(), + radius.toFloat(), + ) + } + + override fun update(node: NotificationScrimClipNode) { + val changed = + node.leftInset != leftInset.toFloat() || + node.top != top.toFloat() || + node.rightInset != rightInset.toFloat() || + node.bottom != bottom.toFloat() || + node.radius != radius.toFloat() + if (changed) { + node.leftInset = leftInset.toFloat() + node.top = top.toFloat() + node.rightInset = rightInset.toFloat() + node.bottom = bottom.toFloat() + node.radius = radius.toFloat() + node.invalidated = true + } + } + + override fun InspectorInfo.inspectableProperties() { + name = "notificationScrimClip" + properties["leftInset"] = leftInset + properties["top"] = top + properties["rightInset"] = rightInset + properties["bottom"] = bottom + properties["radius"] = radius + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt index 7d52216a4d2e..df77878b88d9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt @@ -19,21 +19,26 @@ package com.android.systemui.qs.composefragment.viewmodel import android.content.res.Resources import android.graphics.Rect import androidx.annotation.FloatRange +import androidx.annotation.VisibleForTesting import androidx.lifecycle.LifecycleCoroutineScope import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.FooterActionsController import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.transition.LargeScreenShadeInterpolator +import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.util.LargeScreenUtils +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -140,7 +145,34 @@ constructor( private val _keyguardAndExpanded = MutableStateFlow(false) - private val _statusBarState = MutableStateFlow(-1) + /** + * Tracks the current [StatusBarState]. It will switch early if the upcoming state is + * [StatusBarState.KEYGUARD] + */ + @get:VisibleForTesting + val statusBarState = + conflatedCallbackFlow { + val callback = + object : StatusBarStateController.StateListener { + override fun onStateChanged(newState: Int) { + trySend(newState) + } + + override fun onUpcomingStateChanged(upcomingState: Int) { + if (upcomingState == StatusBarState.KEYGUARD) { + trySend(upcomingState) + } + } + } + sysuiStatusBarStateController.addCallback(callback) + + awaitClose { sysuiStatusBarStateController.removeCallback(callback) } + } + .stateIn( + lifecycleScope, + SharingStarted.WhileSubscribed(), + sysuiStatusBarStateController.state, + ) private val _viewHeight = MutableStateFlow(0) @@ -186,6 +218,12 @@ constructor( } .stateIn(lifecycleScope, SharingStarted.WhileSubscribed(), QSExpansionState.QQS) + /** + * Accessibility action for collapsing/expanding QS. The provided runnable is responsible for + * determining the correct action based on the expansion state. + */ + var collapseExpandAccessibilityAction: Runnable? = null + @AssistedFactory interface Factory { fun create(lifecycleScope: LifecycleCoroutineScope): QSFragmentComposeViewModel diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt index 6dc101a63f09..b0d4fa26225e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt @@ -42,6 +42,8 @@ import javax.inject.Inject import javax.inject.Named import javax.inject.Provider import kotlin.math.max +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -145,10 +147,11 @@ class FooterActionsViewModel( ) } + @OptIn(ExperimentalCoroutinesApi::class) fun create(lifecycleCoroutineScope: LifecycleCoroutineScope): FooterActionsViewModel { val globalActionsDialogLite = globalActionsDialogLiteProvider.get() if (lifecycleCoroutineScope.isActive) { - lifecycleCoroutineScope.launch { + lifecycleCoroutineScope.launch(start = CoroutineStart.ATOMIC) { try { awaitCancellation() } finally { 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 0b9cd96670b1..9a2315be29a2 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 @@ -158,18 +158,20 @@ private fun insertAfter(item: LazyGridItemInfo, offset: Offset): Boolean { return item.span != 1 && offset.x > itemCenter.x } +@Composable fun Modifier.dragAndDropTileSource( sizedTile: SizedTile<EditTileViewModel>, onTap: (TileSpec) -> Unit, onDoubleTap: (TileSpec) -> Unit, dragAndDropState: DragAndDropState ): Modifier { + val state by rememberUpdatedState(dragAndDropState) return dragAndDropSource { detectTapGestures( onTap = { onTap(sizedTile.tile.tileSpec) }, onDoubleTap = { onDoubleTap(sizedTile.tile.tileSpec) }, onLongPress = { - dragAndDropState.onStarted(sizedTile) + state.onStarted(sizedTile) // The tilespec from the ClipData transferred isn't actually needed as we're moving // a tile within the same application. We're using a custom MIME type to limit the diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt index 6eacb2ef9f14..79c2eb90af20 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt @@ -24,6 +24,7 @@ import android.service.quicksettings.Tile.STATE_INACTIVE import android.text.TextUtils import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi @@ -81,6 +82,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.LocalContext @@ -132,11 +134,16 @@ fun Tile( val uiState = remember(state) { state.toUiState() } val colors = TileDefaults.getColorForState(uiState) + // TODO(b/361789146): Draw the shapes instead of clipping + val tileShape = TileDefaults.animateTileShape(uiState.state) + val iconShape = TileDefaults.animateIconShape(uiState.state) + TileContainer( colors = colors, showLabels = showLabels, label = uiState.label, iconOnly = iconOnly, + shape = if (iconOnly) iconShape else tileShape, clickEnabled = true, onClick = tile::onClick, onLongClick = tile::onLongClick, @@ -151,6 +158,7 @@ fun Tile( secondaryLabel = uiState.secondaryLabel, icon = icon, colors = colors, + iconShape = iconShape, toggleClickSupported = state.handlesSecondaryClick, onClick = { if (state.handlesSecondaryClick) { @@ -169,6 +177,7 @@ private fun TileContainer( showLabels: Boolean, label: String, iconOnly: Boolean, + shape: Shape, clickEnabled: Boolean = false, onClick: (Expandable) -> Unit = {}, onLongClick: (Expandable) -> Unit = {}, @@ -189,10 +198,8 @@ private fun TileContainer( } Expandable( color = backgroundColor, - shape = TileDefaults.TileShape, - modifier = - Modifier.height(dimensionResource(id = R.dimen.qs_tile_height)) - .clip(TileDefaults.TileShape) + shape = shape, + modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height)).clip(shape) ) { Box( modifier = @@ -227,6 +234,7 @@ private fun LargeTileContent( secondaryLabel: String?, icon: Icon, colors: TileColors, + iconShape: Shape, toggleClickSupported: Boolean = false, onClick: () -> Unit = {}, onLongClick: () -> Unit = {}, @@ -239,7 +247,7 @@ private fun LargeTileContent( Box( modifier = Modifier.fillMaxHeight().aspectRatio(1f).thenIf(toggleClickSupported) { - Modifier.clip(TileDefaults.TileShape) + Modifier.clip(iconShape) .background(colors.iconBackground, { 1f }) .combinedClickable(onClick = onClick, onLongClick = onLongClick) } @@ -391,7 +399,7 @@ private fun RemoveTileTarget() { horizontalArrangement = tileHorizontalArrangement(), modifier = Modifier.fillMaxHeight() - .border(1.dp, LocalContentColor.current, shape = TileDefaults.TileShape) + .border(1.dp, LocalContentColor.current, shape = CircleShape) .padding(10.dp) ) { Icon(imageVector = Icons.Default.Clear, contentDescription = null) @@ -533,7 +541,7 @@ fun LazyGridScope.editTiles( Modifier.background( color = MaterialTheme.colorScheme.secondary, alpha = { EditModeTileDefaults.PLACEHOLDER_ALPHA }, - shape = TileDefaults.TileShape + shape = RoundedCornerShape(TileDefaults.InactiveCornerRadius) ) .animateItem() ) @@ -619,6 +627,7 @@ fun EditTile( showLabels = showLabels, label = label, iconOnly = iconOnly, + shape = RoundedCornerShape(TileDefaults.InactiveCornerRadius), modifier = modifier, ) { if (iconOnly) { @@ -633,6 +642,7 @@ fun EditTile( secondaryLabel = tileViewModel.appName?.load(), icon = tileViewModel.icon, colors = colors, + iconShape = RoundedCornerShape(TileDefaults.InactiveCornerRadius), ) } } @@ -736,7 +746,9 @@ private object EditModeTileDefaults { } private object TileDefaults { - val TileShape = CircleShape + val InactiveCornerRadius = 50.dp + val ActiveIconCornerRadius = 16.dp + val ActiveTileCornerRadius = 24.dp val IconTileWithLabelHeight = 140.dp /** An active tile without dual target uses the active color as background */ @@ -795,6 +807,39 @@ private object TileDefaults { else -> unavailableTileColors() } } + + @Composable + fun animateIconShape(state: Int): Shape { + return animateShape( + state = state, + activeCornerRadius = ActiveTileCornerRadius, + label = "QSTileCornerRadius", + ) + } + + @Composable + fun animateTileShape(state: Int): Shape { + return animateShape( + state = state, + activeCornerRadius = ActiveIconCornerRadius, + label = "QSTileIconCornerRadius", + ) + } + + @Composable + fun animateShape(state: Int, activeCornerRadius: Dp, label: String): Shape { + val animatedCornerRadius by + animateDpAsState( + targetValue = + if (state == STATE_ACTIVE) { + activeCornerRadius + } else { + InactiveCornerRadius + }, + label = label + ) + return RoundedCornerShape(animatedCornerRadius) + } } private const val CURRENT_TILES_GRID_TEST_TAG = "CurrentTilesGrid" diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java index 8887f5857baf..9abc494e56e6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java @@ -71,6 +71,7 @@ import com.android.systemui.qs.SideLabelTileLayout; import com.android.systemui.qs.logging.QSLogger; import java.io.PrintWriter; +import java.util.Objects; /** * Base quick-settings tile, extend this to create a new tile. @@ -350,6 +351,7 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy public void userSwitch(int newUserId) { mHandler.obtainMessage(H.USER_SWITCH, newUserId, 0).sendToTarget(); + postStale(); } public void destroy() { @@ -667,6 +669,18 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy public String toString() { return "DrawableIcon"; } + + @Override + public boolean equals(@Nullable Object other) { + // No need to compare equality of the mInvisibleDrawable as that's generated from + // mDrawable's constant state. + return other instanceof DrawableIcon && ((DrawableIcon) other).mDrawable == mDrawable; + } + + @Override + public int hashCode() { + return Objects.hash(mDrawable); + } } public static class DrawableIconWithRes extends DrawableIcon { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt index 5f5b26514de8..3f18fc2066eb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt @@ -57,7 +57,7 @@ constructor( isActivated = modes.isNotEmpty(), icon = if (Flags.modesApi() && Flags.modesUi() && Flags.modesUiIcons()) - zenModeInteractor.getActiveModeIcon(context, modes) + zenModeInteractor.getActiveModeIcon(modes) else null, activeModes = modes.map { it.name } ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt index 0e127e3f6bb4..83c3335ebffb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt @@ -41,10 +41,11 @@ constructor( if (Flags.modesApi() && Flags.modesUi() && Flags.modesUiIcons() && data.icon != null) { icon = { data.icon } } else { - val defaultIconRes = + val iconRes = if (data.isActivated) R.drawable.qs_dnd_icon_on else R.drawable.qs_dnd_icon_off - iconRes = defaultIconRes - icon = { resources.getDrawable(defaultIconRes, theme).asIcon() } + val icon = resources.getDrawable(iconRes, theme).asIcon() + this.iconRes = iconRes + this.icon = { icon } } activationState = if (data.isActivated) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt index 55b8f5f0ca98..12f3c9cd0b1b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt @@ -44,7 +44,7 @@ constructor( private val footerActionsViewModelFactory: FooterActionsViewModel.Factory, private val footerActionsController: FooterActionsController, val mediaCarouselInteractor: MediaCarouselInteractor, -) : SysUiViewModel() { +) : SysUiViewModel { val isMediaVisible: StateFlow<Boolean> = mediaCarouselInteractor.hasAnyMediaOrRecommendation diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt index abfca4b9aa4a..cb99be48912e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt @@ -35,7 +35,7 @@ class QuickSettingsShadeSceneContentViewModel constructor( val overlayShadeViewModelFactory: OverlayShadeViewModel.Factory, val quickSettingsContainerViewModel: QuickSettingsContainerViewModel, -) : SysUiViewModel() { +) : SysUiViewModel { @AssistedFactory interface Factory { diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index ecf816b263ff..a37a722e13e0 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -26,7 +26,6 @@ import static android.view.MotionEvent.ACTION_UP; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME; -import static com.android.systemui.Flags.glanceableHubBackGesture; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER; @@ -86,10 +85,10 @@ import com.android.internal.util.ScreenshotHelper; import com.android.internal.util.ScreenshotRequest; import com.android.systemui.Dumpable; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.contextualeducation.GestureType; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; -import com.android.systemui.contextualeducation.GestureType; import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardWmStateRefactor; @@ -837,8 +836,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis .setFlag(SYSUI_STATE_BOUNCER_SHOWING, bouncerShowing) .setFlag(SYSUI_STATE_DEVICE_DOZING, isDozing) .setFlag(SYSUI_STATE_DEVICE_DREAMING, isDreaming) - .setFlag(SYSUI_STATE_COMMUNAL_HUB_SHOWING, - glanceableHubBackGesture() && communalShowing) + .setFlag(SYSUI_STATE_COMMUNAL_HUB_SHOWING, communalShowing) .commitUpdate(mContext.getDisplayId()); } diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt index 863a899b6f4c..3d6d00eb3cc0 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt @@ -24,6 +24,7 @@ import android.content.res.Resources import android.net.Uri import android.os.Handler import android.os.UserHandle +import android.provider.Settings import android.util.Log import com.android.internal.logging.UiEventLogger import com.android.systemui.animation.DialogTransitionAnimator @@ -90,7 +91,16 @@ constructor( // ViewCapture needs to save it's data before it is disabled, or else the data will // be lost. This is expected to change in the near future, and when that happens // this line should be removed. - bgExecutor.execute { traceurMessageSender.stopTracing() } + bgExecutor.execute { + if (issueRecordingState.traceConfig.longTrace) { + Settings.Global.putInt( + contentResolver, + NOTIFY_SESSION_ENDED_SETTING, + DISABLED + ) + } + traceurMessageSender.stopTracing() + } issueRecordingState.isRecording = false } ACTION_SHARE -> { @@ -125,6 +135,8 @@ constructor( companion object { private const val TAG = "IssueRecordingService" private const val CHANNEL_ID = "issue_record" + private const val NOTIFY_SESSION_ENDED_SETTING = "should_notify_trace_session_ended" + private const val DISABLED = 0 /** * Get an intent to stop the issue recording service. diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt index 2d510e1cb659..ea61bd32c1f2 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt @@ -118,8 +118,11 @@ constructor( get() = when (this) { is ObservableTransitionState.Idle -> currentScene.canBeOccluded - is ObservableTransitionState.Transition -> + is ObservableTransitionState.Transition.ChangeCurrentScene -> fromScene.canBeOccluded && toScene.canBeOccluded + is ObservableTransitionState.Transition.ReplaceOverlay, + is ObservableTransitionState.Transition.ShowOrHideOverlay -> + TODO("b/359173565: Handle overlay transitions") } /** diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index 75cb017db5c1..1b9c346129c6 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -109,7 +109,15 @@ constructor( */ val transitioningTo: StateFlow<SceneKey?> = transitionState - .map { state -> (state as? ObservableTransitionState.Transition)?.toScene } + .map { state -> + when (state) { + is ObservableTransitionState.Idle -> null + is ObservableTransitionState.Transition.ChangeCurrentScene -> state.toScene + is ObservableTransitionState.Transition.ShowOrHideOverlay, + is ObservableTransitionState.Transition.ReplaceOverlay -> + TODO("b/359173565: Handle overlay transitions") + } + } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt index c6f51b384a0a..ec743ba5c91e 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt @@ -112,7 +112,7 @@ constructor( // It // happens only when unlocking or when dismissing a dismissible lockscreen. val isTransitioningAwayFromKeyguard = - transitionState is ObservableTransitionState.Transition && + transitionState is ObservableTransitionState.Transition.ChangeCurrentScene && transitionState.fromScene.isKeyguard() && transitionState.toScene == Scenes.Gone @@ -120,7 +120,7 @@ constructor( val isCurrentSceneShade = currentScene.isShade() // This is true when moving into one of the shade scenes when a non-shade scene. val isTransitioningToShade = - transitionState is ObservableTransitionState.Transition && + transitionState is ObservableTransitionState.Transition.ChangeCurrentScene && !transitionState.fromScene.isShade() && transitionState.toScene.isShade() diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scene.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scene.kt index 61a06dbff18f..8e2e8a1d521b 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scene.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scene.kt @@ -20,7 +20,6 @@ import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.lifecycle.Activatable -import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.Flow /** @@ -36,10 +35,6 @@ interface Scene : Activatable { /** Uniquely-identifying key for this scene. The key must be unique within its container. */ val key: SceneKey - override suspend fun activate(): Nothing { - awaitCancellation() - } - /** * The mapping between [UserAction] and destination [UserActionResult]s. * diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt index b5de1b6209bb..9144f16d9251 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.scene.ui.viewmodel import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.SysUiViewModel import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.MutableStateFlow @@ -32,7 +33,7 @@ import kotlinx.coroutines.flow.asStateFlow * need to worry about resetting the value of [actions] when the view-model is deactivated/canceled, * this base class takes care of it. */ -abstract class SceneActionsViewModel : SysUiViewModel() { +abstract class SceneActionsViewModel : SysUiViewModel, ExclusiveActivatable() { private val _actions = MutableStateFlow<Map<UserAction, UserActionResult>>(emptyMap()) /** diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt index f8a9f8c5a279..9dfb7450fd3f 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt @@ -24,6 +24,8 @@ import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.classifier.Classifier import com.android.systemui.classifier.domain.interactor.FalsingInteractor +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor @@ -32,7 +34,6 @@ import com.android.systemui.scene.shared.model.Scenes import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -45,7 +46,7 @@ constructor( private val powerInteractor: PowerInteractor, private val logger: SceneLogger, @Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit, -) : SysUiViewModel() { +) : SysUiViewModel, ExclusiveActivatable() { /** * Keys of all scenes in the container. * @@ -57,8 +58,10 @@ constructor( /** The scene that should be rendered. */ val currentScene: StateFlow<SceneKey> = sceneInteractor.currentScene + private val hydrator = Hydrator() + /** Whether the container is visible. */ - val isVisible: Boolean by hydratedStateOf(sceneInteractor.isVisible) + val isVisible: Boolean by hydrator.hydratedStateOf(sceneInteractor.isVisible) override suspend fun onActivated(): Nothing { try { @@ -75,7 +78,8 @@ constructor( } } ) - awaitCancellation() + + hydrator.activate() } finally { // Clears the previously-sent MotionEventHandler so the owner of the view-model releases // their reference to it. diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt index 7f8c1463ed1f..706797d9bbd2 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt @@ -37,7 +37,7 @@ constructor( private val brightnessMirrorShowingInteractor: BrightnessMirrorShowingInteractor, @Main private val resources: Resources, val sliderControllerFactory: BrightnessSliderController.Factory, -) : SysUiViewModel(), MirrorController { +) : SysUiViewModel, MirrorController { private val tempPosition = IntArray(2) diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index 181c3df92222..4639e2235346 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -17,7 +17,6 @@ package com.android.systemui.shade import android.content.Context -import android.graphics.Insets import android.graphics.Rect import android.os.PowerManager import android.os.SystemClock @@ -26,13 +25,14 @@ import android.view.GestureDetector import android.view.MotionEvent import android.view.View import android.view.ViewGroup -import android.view.WindowInsets import android.widget.FrameLayout import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcherOwner import androidx.activity.setViewTreeOnBackPressedDispatcherOwner import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.lifecycleScope @@ -40,7 +40,6 @@ import androidx.lifecycle.repeatOnLifecycle import com.android.compose.theme.PlatformTheme import com.android.internal.annotations.VisibleForTesting import com.android.systemui.Flags -import com.android.systemui.Flags.glanceableHubBackGesture import com.android.systemui.ambient.touch.TouchMonitor import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent import com.android.systemui.communal.dagger.Communal @@ -55,6 +54,9 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInterac import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalTouchLog import com.android.systemui.media.controls.ui.controller.KeyguardMediaController import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -91,8 +93,10 @@ constructor( @Communal private val dataSourceDelegator: SceneDataSourceDelegator, private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController, private val keyguardMediaController: KeyguardMediaController, - private val lockscreenSmartspaceController: LockscreenSmartspaceController + private val lockscreenSmartspaceController: LockscreenSmartspaceController, + @CommunalTouchLog logBuffer: LogBuffer, ) : LifecycleOwner { + private val logger = Logger(logBuffer, "GlanceableHubContainerController") private class CommunalWrapper(context: Context) : FrameLayout(context) { private val consumers: MutableSet<Consumer<Boolean>> = ArraySet() @@ -143,6 +147,17 @@ constructor( private var isTrackingHubTouch = false /** + * True if a touch gesture on the lock screen has been consumed by the shade/bouncer and thus + * should be ignored by the hub. + * + * This is necessary on the lock screen as gestures on an empty spot go through special touch + * handling logic in [NotificationShadeWindowViewController] that decides if they should go to + * the shade or bouncer. Once the shade or bouncer are moving, we don't get the typical cancel + * event so to play nice, we ignore touches once we see the shade or bouncer are opening. + */ + private var touchTakenByKeyguardGesture = false + + /** * True if the hub UI is fully open, meaning it should receive touch input. * * Tracks [CommunalInteractor.isCommunalShowing]. @@ -206,6 +221,21 @@ constructor( */ private var isDreaming = false + /** Observes and logs state when the lifecycle that controls the [touchMonitor] updates. */ + private val touchLifecycleLogger: LifecycleObserver = LifecycleEventObserver { _, event -> + logger.d({ + "Touch handler lifecycle changed to $str1. hubShowing: $bool1, " + + "shadeShowingAndConsumingTouches: $bool2, " + + "anyBouncerShowing: $bool3, inEditModeTransition: $bool4" + }) { + str1 = event.toString() + bool1 = hubShowing + bool2 = shadeShowingAndConsumingTouches + bool3 = anyBouncerShowing + bool4 = inEditModeTransition + } + } + /** Returns a flow that tracks whether communal hub is available. */ fun communalAvailable(): Flow<Boolean> = anyOf(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen) @@ -268,6 +298,7 @@ constructor( init() } } + lifecycleRegistry.addObserver(touchLifecycleLogger) lifecycleRegistry.currentState = Lifecycle.State.CREATED communalContainerView = containerView @@ -288,21 +319,13 @@ constructor( // Run when the touch handling lifecycle is RESUMED, meaning the hub is visible and not // occluded. lifecycleRegistry.repeatOnLifecycle(Lifecycle.State.RESUMED) { - // Avoid adding exclusion to end/start edges to allow back gestures. - val insets = - if (glanceableHubBackGesture()) { - containerView.rootWindowInsets.getInsets(WindowInsets.Type.systemGestures()) - } else { - Insets.NONE - } - val ltr = containerView.layoutDirection == View.LAYOUT_DIRECTION_LTR val backGestureInset = Rect( - if (ltr) 0 else insets.left, 0, - if (ltr) insets.right else containerView.right, + 0, + if (ltr) 0 else containerView.right, containerView.bottom, ) @@ -318,9 +341,9 @@ constructor( // Only allow swipe up to bouncer and swipe down to shade in the very // top/bottom to avoid conflicting with widgets in the hub grid. Rect( - insets.left, + 0, topEdgeSwipeRegionWidth, - containerView.right - insets.right, + containerView.right, containerView.bottom - bottomEdgeSwipeRegionWidth ), // Disable back gestures on the left side of the screen, to avoid @@ -328,6 +351,9 @@ constructor( backGestureInset ) } + logger.d({ "Insets updated: $str1" }) { + str1 = containerView.systemGestureExclusionRects.toString() + } } } @@ -343,6 +369,9 @@ constructor( ), { anyBouncerShowing = it + if (hubShowing) { + logger.d({ "New value for anyBouncerShowing: $bool1" }) { bool1 = it } + } updateTouchHandlingState() } ) @@ -396,7 +425,13 @@ constructor( // If the shade reaches full expansion without interaction, then we should allow it // to consume touches rather than handling it here until it disappears. shadeShowingAndConsumingTouches = - userNotInteractiveAtShadeFullyExpanded || expandedAndNotInteractive + (userNotInteractiveAtShadeFullyExpanded || expandedAndNotInteractive).also { + if (it != shadeShowingAndConsumingTouches && hubShowing) { + logger.d({ "New value for shadeShowingAndConsumingTouches: $bool1" }) { + bool1 = it + } + } + } updateTouchHandlingState() } ) @@ -404,6 +439,7 @@ constructor( communalContainerWrapper = CommunalWrapper(containerView.context) communalContainerWrapper?.addView(communalContainerView) + logger.d("Hub container initialized") return communalContainerWrapper!! } @@ -446,6 +482,10 @@ constructor( (it.parent as ViewGroup).removeView(it) communalContainerWrapper = null } + + lifecycleRegistry.removeObserver(touchLifecycleLogger) + + logger.d("Hub container disposed") } /** @@ -463,15 +503,20 @@ constructor( // In the case that we are handling full swipes on the lockscreen, are on the lockscreen, // and the touch is within the horizontal notification band on the screen, do not process // the touch. - if ( - !hubShowing && - (!notificationStackScrollLayoutController.isBelowLastNotification(ev.x, ev.y) || - keyguardMediaController.isWithinMediaViewBounds(ev.x.toInt(), ev.y.toInt()) || - lockscreenSmartspaceController.isWithinSmartspaceBounds( - ev.x.toInt(), - ev.y.toInt() - )) - ) { + val touchOnNotifications = + !notificationStackScrollLayoutController.isBelowLastNotification(ev.x, ev.y) + val touchOnUmo = keyguardMediaController.isWithinMediaViewBounds(ev.x.toInt(), ev.y.toInt()) + val touchOnSmartspace = + lockscreenSmartspaceController.isWithinSmartspaceBounds(ev.x.toInt(), ev.y.toInt()) + if (!hubShowing && (touchOnNotifications || touchOnUmo || touchOnSmartspace)) { + logger.d({ + "Lockscreen touch ignored: touchOnNotifications: $bool1, touchOnUmo: $bool2, " + + "touchOnSmartspace: $bool3" + }) { + bool1 = touchOnNotifications + bool2 = touchOnUmo + bool3 = touchOnSmartspace + } return false } @@ -487,12 +532,56 @@ constructor( val hubOccluded = anyBouncerShowing || shadeShowingAndConsumingTouches if ((isDown || isMove) && !hubOccluded) { + if (isDown) { + logger.d({ + "Touch started. x: $int1, y: $int2, hubShowing: $bool1, isDreaming: $bool2, " + + "onLockscreen: $bool3" + }) { + int1 = ev.x.toInt() + int2 = ev.y.toInt() + bool1 = hubShowing + bool2 = isDreaming + bool3 = onLockscreen + } + } isTrackingHubTouch = true } if (isTrackingHubTouch) { + // On the lock screen, our touch handlers are not active and we rely on the NSWVC's + // touch handling for gestures on blank areas, which can go up to show the bouncer or + // down to show the notification shade. We see the touches first and they are not + // consumed and cancelled like on the dream or hub so we have to gracefully ignore them + // if the shade or bouncer are handling them. This issue only applies to touches on the + // keyguard itself, once the bouncer or shade are fully open, our logic stops us from + // taking touches. + touchTakenByKeyguardGesture = + (onLockscreen && (shadeConsumingTouches || anyBouncerShowing)).also { + if (it != touchTakenByKeyguardGesture && it) { + logger.d( + "Lock screen touch consumed by shade or bouncer, ignoring " + + "subsequent touches" + ) + } + } if (isUp || isCancel) { + logger.d({ + val endReason = if (bool1) "up" else "cancel" + "Touch ended with $endReason. x: $int1, y: $int2, " + + "shadeConsumingTouches: $bool2, anyBouncerShowing: $bool3" + }) { + int1 = ev.x.toInt() + int2 = ev.y.toInt() + bool1 = isUp + bool2 = shadeConsumingTouches + bool3 = anyBouncerShowing + } isTrackingHubTouch = false + + // Clear out touch taken state to ensure the up/cancel event still gets dispatched + // to the hub. This is necessary as the hub always receives at least the initial + // down even if the shade or bouncer end up handling the touch. + touchTakenByKeyguardGesture = false } return dispatchTouchEvent(ev) } @@ -513,21 +602,8 @@ constructor( return true } try { - // On the lock screen, our touch handlers are not active and we rely on the NSWVC's - // touch handling for gestures on blank areas, which can go up to show the bouncer or - // down to show the notification shade. We see the touches first and they are not - // consumed and cancelled like on the dream or hub so we have to gracefully ignore them - // if the shade or bouncer are handling them. This issue only applies to touches on the - // keyguard itself, once the bouncer or shade are fully open, our logic stops us from - // taking touches. - val touchTaken = onLockscreen && (shadeConsumingTouches || anyBouncerShowing) - - // Only dispatch touches to communal if not already handled or the touch is ending, - // meaning the event is an up or cancel. This is necessary as the hub always receives at - // least the initial down even if the shade or bouncer end up handling the touch. - val dispatchToCommunal = !touchTaken || !isTrackingHubTouch var handled = false - if (dispatchToCommunal) { + if (!touchTakenByKeyguardGesture) { communalContainerWrapper?.dispatchTouchEvent(ev) { if (it) { handled = true diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt index 8006e9421f5c..7d6712166a21 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt @@ -64,7 +64,7 @@ constructor( 0f } ) - is ObservableTransitionState.Transition -> + is ObservableTransitionState.Transition.ChangeCurrentScene -> when { state.fromScene == Scenes.Gone -> if (state.toScene.isExpandable()) { @@ -88,6 +88,9 @@ constructor( } else -> flowOf(1f) } + is ObservableTransitionState.Transition.ShowOrHideOverlay, + is ObservableTransitionState.Transition.ReplaceOverlay -> + TODO("b/359173565: Handle overlay transitions") } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt index 00c023540da3..25ae44ef8cfe 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.shade.ui.viewmodel import com.android.compose.animation.scene.SceneKey +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.SceneFamilies @@ -37,7 +38,7 @@ import kotlinx.coroutines.flow.collectLatest class OverlayShadeViewModel @AssistedInject constructor(private val sceneInteractor: SceneInteractor, shadeInteractor: ShadeInteractor) : - SysUiViewModel() { + SysUiViewModel, ExclusiveActivatable() { private val _backgroundScene = MutableStateFlow(Scenes.Lockscreen) /** The scene to show in the background when the overlay shade is open. */ val backgroundScene: StateFlow<SceneKey> = _backgroundScene.asStateFlow() 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 f0e9d41c0fe7..edfe79ad91b8 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 @@ -24,6 +24,7 @@ import android.icu.text.DisplayContext import android.os.UserHandle import android.provider.Settings import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.plugins.ActivityStarter import com.android.systemui.privacy.OngoingPrivacyChip @@ -65,7 +66,7 @@ constructor( private val privacyChipInteractor: PrivacyChipInteractor, private val clockInteractor: ShadeHeaderClockInteractor, private val broadcastDispatcher: BroadcastDispatcher, -) : SysUiViewModel() { +) : SysUiViewModel, ExclusiveActivatable() { /** True if there is exactly one mobile connection. */ val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt index fe3bcb5c326c..f0f2a65d9abb 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt @@ -20,6 +20,7 @@ package com.android.systemui.shade.ui.viewmodel import androidx.lifecycle.LifecycleOwner import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.qs.FooterActionsController @@ -61,7 +62,7 @@ constructor( private val unfoldTransitionInteractor: UnfoldTransitionInteractor, private val deviceEntryInteractor: DeviceEntryInteractor, private val sceneInteractor: SceneInteractor, -) : SysUiViewModel() { +) : SysUiViewModel, ExclusiveActivatable() { val shadeMode: StateFlow<ShadeMode> = shadeInteractor.shadeMode diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index a1477b57a22a..f88fd7d00a2b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -1174,7 +1174,7 @@ public class CommandQueue extends IStatusBar.Stub implements } } - @Override + // This was previously called from WM, but is now called from WMShell public void onRecentsAnimationStateChanged(boolean running) { synchronized (mLock) { mHandler.obtainMessage(MSG_RECENTS_ANIMATION_STATE_CHANGED, running ? 1 : 0, 0) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java index 3068460f1cc5..6eadd2627399 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java @@ -471,17 +471,7 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi */ private Drawable getIcon(Context sysuiContext, Context context, StatusBarIcon statusBarIcon) { - int userId = statusBarIcon.user.getIdentifier(); - if (userId == UserHandle.USER_ALL) { - userId = UserHandle.USER_SYSTEM; - } - - // Try to load the monochrome app icon if applicable - Drawable icon = maybeGetMonochromeAppIcon(context, statusBarIcon); - // Otherwise, just use the icon normally - if (icon == null) { - icon = statusBarIcon.icon.loadDrawableAsUser(context, userId); - } + Drawable icon = loadDrawable(context, statusBarIcon); TypedValue typedValue = new TypedValue(); sysuiContext.getResources().getValue(R.dimen.status_bar_icon_scale_factor, @@ -509,6 +499,26 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi } @Nullable + private Drawable loadDrawable(Context context, StatusBarIcon statusBarIcon) { + if (usesModeIcons() && statusBarIcon.preloadedIcon != null) { + return statusBarIcon.preloadedIcon.mutate(); + } else { + int userId = statusBarIcon.user.getIdentifier(); + if (userId == UserHandle.USER_ALL) { + userId = UserHandle.USER_SYSTEM; + } + + // Try to load the monochrome app icon if applicable + Drawable icon = maybeGetMonochromeAppIcon(context, statusBarIcon); + // Otherwise, just use the icon normally + if (icon == null) { + icon = statusBarIcon.icon.loadDrawableAsUser(context, userId); + } + return icon; + } + } + + @Nullable private Drawable maybeGetMonochromeAppIcon(Context context, StatusBarIcon statusBarIcon) { if (android.app.Flags.notificationsUseMonochromeAppIcon() @@ -1020,4 +1030,9 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi public boolean showsConversation() { return mShowsConversation; } + + private static boolean usesModeIcons() { + return android.app.Flags.modesApi() && android.app.Flags.modesUi() + && android.app.Flags.modesUiIcons(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt index f74c9a6a209f..e9292f8c3cb8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt @@ -79,6 +79,7 @@ internal constructor( // NOTE: NotificationEntry.isClearable will internally check group children to ensure // the group itself definitively clearable. val isClearable = !isSensitiveContentProtectionActive && entry.isClearable + && !entry.isSensitive.value when { isSilent && isClearable -> hasClearableSilentNotifs = true isSilent && !isClearable -> hasNonClearableSilentNotifs = true diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt index 17f401ac0dde..0efd5f15cb09 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt @@ -45,7 +45,6 @@ import android.provider.Settings.Global.HEADS_UP_OFF import android.service.notification.Flags import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger -import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID import com.android.internal.messages.nano.SystemMessageProto.SystemMessage import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -279,7 +278,8 @@ class AvalancheSuppressor( private val packageManager: PackageManager, private val uiEventLogger: UiEventLogger, private val context: Context, - private val notificationManager: NotificationManager + private val notificationManager: NotificationManager, + private val logger: VisualInterruptionDecisionLogger ) : VisualInterruptionFilter( types = setOf(PEEK, PULSE), @@ -354,15 +354,18 @@ class AvalancheSuppressor( override fun shouldSuppress(entry: NotificationEntry): Boolean { if (!isCooldownEnabled()) { + logger.logAvalancheAllow("cooldown OFF") return false } val timeSinceAvalancheMs = systemClock.currentTimeMillis() - avalancheProvider.startTime val timedOut = timeSinceAvalancheMs >= avalancheProvider.timeoutMs if (timedOut) { + logger.logAvalancheAllow("timedOut! timeSinceAvalancheMs=$timeSinceAvalancheMs") return false } val state = calculateState(entry) if (state != State.SUPPRESS) { + logger.logAvalancheAllow("state=$state") return false } if (shouldShowEdu()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt index c204ea9097de..b83259dce298 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt @@ -93,6 +93,15 @@ constructor(@NotificationInterruptLog val buffer: LogBuffer) { } ) } + + fun logAvalancheAllow(info: String) { + buffer.log( + TAG, + INFO, + { str1 = info }, + { "AvalancheSuppressor: $str1" } + ) + } } private const val TAG = "VisualInterruptionDecisionProvider" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt index 8e8d9b69ac58..2f8711a586ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt @@ -194,7 +194,8 @@ constructor( packageManager, uiEventLogger, context, - notificationManager + notificationManager, + logger ) ) avalancheProvider.register() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 7119145a1fa8..48c974a33f12 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -117,6 +117,8 @@ public class NotificationContentView extends FrameLayout implements Notification protected HybridNotificationView mSingleLineView; @Nullable public DisposableHandle mContractedBinderHandle; + @Nullable public DisposableHandle mExpandedBinderHandle; + @Nullable public DisposableHandle mHeadsUpBinderHandle; private RemoteInputView mExpandedRemoteInput; private RemoteInputView mHeadsUpRemoteInput; 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 a5cd2a2de085..c342bcd2706b 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 @@ -46,6 +46,9 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.notification.ConversationNotificationProcessor import com.android.systemui.statusbar.notification.InflationException import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.InflatedContentViewHolder +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.KeepExistingView +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.NullContentView 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 @@ -286,11 +289,15 @@ constructor( } FLAG_CONTENT_VIEW_EXPANDED -> row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_EXPANDED) { + row.privateLayout.mExpandedBinderHandle?.dispose() + row.privateLayout.mExpandedBinderHandle = null row.privateLayout.setExpandedChild(null) remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED) } FLAG_CONTENT_VIEW_HEADS_UP -> row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_HEADSUP) { + row.privateLayout.mHeadsUpBinderHandle?.dispose() + row.privateLayout.mHeadsUpBinderHandle = null row.privateLayout.setHeadsUpChild(null) remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP) row.privateLayout.setHeadsUpInflatedSmartReplies(null) @@ -499,17 +506,87 @@ constructor( } } - if (reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0) { + val richOngoingContentModel = inflationProgress.contentModel.richOngoingContentModel + + if ( + richOngoingContentModel != null && + reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0 + ) { logger.logAsyncTaskProgress(entry, "inflating RON view") - inflationProgress.richOngoingNotificationViewHolder = - inflationProgress.contentModel.richOngoingContentModel?.let { + val inflateContractedView = reInflateFlags and FLAG_CONTENT_VIEW_CONTRACTED != 0 + val inflateExpandedView = reInflateFlags and FLAG_CONTENT_VIEW_EXPANDED != 0 + val inflateHeadsUpView = reInflateFlags and FLAG_CONTENT_VIEW_HEADS_UP != 0 + + inflationProgress.contractedRichOngoingNotificationViewHolder = + if (inflateContractedView) { ronInflater.inflateView( - contentModel = it, + contentModel = richOngoingContentModel, existingView = row.privateLayout.contractedChild, entry = entry, systemUiContext = context, - parentView = row.privateLayout + parentView = row.privateLayout, + viewType = RichOngoingNotificationViewType.Contracted + ) + } else { + if ( + ronInflater.canKeepView( + contentModel = richOngoingContentModel, + existingView = row.privateLayout.contractedChild, + viewType = RichOngoingNotificationViewType.Contracted + ) + ) { + KeepExistingView + } else { + NullContentView + } + } + + inflationProgress.expandedRichOngoingNotificationViewHolder = + if (inflateExpandedView) { + ronInflater.inflateView( + contentModel = richOngoingContentModel, + existingView = row.privateLayout.expandedChild, + entry = entry, + systemUiContext = context, + parentView = row.privateLayout, + viewType = RichOngoingNotificationViewType.Expanded ) + } else { + if ( + ronInflater.canKeepView( + contentModel = richOngoingContentModel, + existingView = row.privateLayout.expandedChild, + viewType = RichOngoingNotificationViewType.Expanded + ) + ) { + KeepExistingView + } else { + NullContentView + } + } + + inflationProgress.headsUpRichOngoingNotificationViewHolder = + if (inflateHeadsUpView) { + ronInflater.inflateView( + contentModel = richOngoingContentModel, + existingView = row.privateLayout.headsUpChild, + entry = entry, + systemUiContext = context, + parentView = row.privateLayout, + viewType = RichOngoingNotificationViewType.HeadsUp + ) + } else { + if ( + ronInflater.canKeepView( + contentModel = richOngoingContentModel, + existingView = row.privateLayout.headsUpChild, + viewType = RichOngoingNotificationViewType.HeadsUp + ) + ) { + KeepExistingView + } else { + NullContentView + } } } @@ -618,7 +695,9 @@ constructor( var inflatedSmartReplyState: InflatedSmartReplyState? = null var expandedInflatedSmartReplies: InflatedSmartReplyViewHolder? = null var headsUpInflatedSmartReplies: InflatedSmartReplyViewHolder? = null - var richOngoingNotificationViewHolder: InflatedContentViewHolder? = null + var contractedRichOngoingNotificationViewHolder: ContentViewInflationResult? = null + var expandedRichOngoingNotificationViewHolder: ContentViewInflationResult? = null + var headsUpRichOngoingNotificationViewHolder: ContentViewInflationResult? = null // Inflated SingleLineView that lacks the UI State var inflatedSingleLineView: HybridNotificationView? = null @@ -1428,14 +1507,21 @@ constructor( logger.logAsyncTaskProgress(entry, "finishing") // before updating the content model, stop existing binding if necessary - val hasRichOngoingContentModel = result.contentModel.richOngoingContentModel != null - val requestedRichOngoing = reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0 - val rejectedRichOngoing = requestedRichOngoing && !hasRichOngoingContentModel - if (result.richOngoingNotificationViewHolder != null || rejectedRichOngoing) { + if (result.contractedRichOngoingNotificationViewHolder.shouldDisposeViewBinder()) { row.privateLayout.mContractedBinderHandle?.dispose() row.privateLayout.mContractedBinderHandle = null } + if (result.expandedRichOngoingNotificationViewHolder.shouldDisposeViewBinder()) { + row.privateLayout.mExpandedBinderHandle?.dispose() + row.privateLayout.mExpandedBinderHandle = null + } + + if (result.headsUpRichOngoingNotificationViewHolder.shouldDisposeViewBinder()) { + row.privateLayout.mHeadsUpBinderHandle?.dispose() + row.privateLayout.mHeadsUpBinderHandle = null + } + // set the content model after disposal and before setting new rich ongoing view entry.setContentModel(result.contentModel) result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) } @@ -1477,19 +1563,53 @@ constructor( } } - // after updating the content model, set the view, then start the new binder - result.richOngoingNotificationViewHolder?.let { viewHolder -> - row.privateLayout.contractedChild = viewHolder.view - row.privateLayout.expandedChild = null - row.privateLayout.headsUpChild = null - row.privateLayout.setExpandedInflatedSmartReplies(null) - row.privateLayout.setHeadsUpInflatedSmartReplies(null) - row.privateLayout.mContractedBinderHandle = - viewHolder.binder.setupContentViewBinder() - row.setExpandable(false) + val hasRichOngoingViewHolder = + result.contractedRichOngoingNotificationViewHolder != null || + result.expandedRichOngoingNotificationViewHolder != null || + result.headsUpRichOngoingNotificationViewHolder != null + + if (hasRichOngoingViewHolder) { + // after updating the content model, set the view, then start the new binder + result.contractedRichOngoingNotificationViewHolder?.let { contractedViewHolder -> + if (contractedViewHolder is InflatedContentViewHolder) { + row.privateLayout.contractedChild = contractedViewHolder.view + row.privateLayout.mContractedBinderHandle = + contractedViewHolder.binder.setupContentViewBinder() + } else if (contractedViewHolder == NullContentView) { + row.privateLayout.contractedChild = null + } + } + + result.expandedRichOngoingNotificationViewHolder?.let { expandedViewHolder -> + if (expandedViewHolder is InflatedContentViewHolder) { + row.privateLayout.expandedChild = expandedViewHolder.view + row.privateLayout.mExpandedBinderHandle = + expandedViewHolder.binder.setupContentViewBinder() + } else if (expandedViewHolder == NullContentView) { + row.privateLayout.expandedChild = null + } + } + + result.headsUpRichOngoingNotificationViewHolder?.let { headsUpViewHolder -> + if (headsUpViewHolder is InflatedContentViewHolder) { + row.privateLayout.headsUpChild = headsUpViewHolder.view + row.privateLayout.mHeadsUpBinderHandle = + headsUpViewHolder.binder.setupContentViewBinder() + } else if (headsUpViewHolder == NullContentView) { + row.privateLayout.headsUpChild = null + } + } + + // clean remoteViewCache when we don't keep existing views. remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED) remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED) remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP) + + // Since RONs don't support smart reply, remove them from HUNs and Expanded. + row.privateLayout.setExpandedInflatedSmartReplies(null) + row.privateLayout.setHeadsUpInflatedSmartReplies(null) + + row.setExpandable(row.privateLayout.expandedChild != null) } Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row)) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt index bf5b3a34afb6..da29b0fd0dc7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt @@ -22,6 +22,7 @@ import android.content.Context import android.util.Log import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.row.shared.EnRouteContentModel import com.android.systemui.statusbar.notification.row.shared.IconModel import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag @@ -68,12 +69,13 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : builder: Notification.Builder, systemUIContext: Context, packageContext: Context - ): RichOngoingContentModel? = + ): RichOngoingContentModel? { + val sbn = entry.sbn + val notification = sbn.notification + val icon = IconModel(notification.smallIcon) + try { - val sbn = entry.sbn - val notification = sbn.notification - val icon = IconModel(notification.smallIcon) - if (sbn.packageName == "com.google.android.deskclock") { + return if (sbn.packageName == "com.google.android.deskclock") { when (notification.channelId) { "Timers v2" -> { parseTimerNotification(notification, icon) @@ -87,11 +89,14 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : null } } + } else if (builder.style is Notification.EnRouteStyle) { + parseEnRouteNotification(notification, icon) } else null } catch (e: Exception) { Log.e("RONs", "Error parsing RON", e) - null + return null } + } /** * FOR PROTOTYPING ONLY: create a RON TimerContentModel using the time information available @@ -199,4 +204,15 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : .plusMinutes(minute.toLong()) .plusSeconds(second.toLong()) } + + private fun parseEnRouteNotification( + notification: Notification, + icon: IconModel, + ): EnRouteContentModel { + return EnRouteContentModel( + smallIcon = icon, + title = notification.extras.getCharSequence(Notification.EXTRA_TITLE), + text = notification.extras.getCharSequence(Notification.EXTRA_TEXT), + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt index e9c4960a4011..2c462b7329b4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt @@ -24,12 +24,18 @@ import android.view.ViewGroup import com.android.systemui.dagger.SysUISingleton import com.android.systemui.res.R import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.InflatedContentViewHolder +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.KeepExistingView +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.NullContentView +import com.android.systemui.statusbar.notification.row.shared.EnRouteContentModel import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag -import com.android.systemui.statusbar.notification.row.shared.StopwatchContentModel import com.android.systemui.statusbar.notification.row.shared.TimerContentModel +import com.android.systemui.statusbar.notification.row.ui.view.EnRouteView import com.android.systemui.statusbar.notification.row.ui.view.TimerView +import com.android.systemui.statusbar.notification.row.ui.viewbinder.EnRouteViewBinder import com.android.systemui.statusbar.notification.row.ui.viewbinder.TimerViewBinder +import com.android.systemui.statusbar.notification.row.ui.viewmodel.EnRouteViewModel import com.android.systemui.statusbar.notification.row.ui.viewmodel.RichOngoingViewModelComponent import com.android.systemui.statusbar.notification.row.ui.viewmodel.TimerViewModel import javax.inject.Inject @@ -39,7 +45,35 @@ fun interface DeferredContentViewBinder { fun setupContentViewBinder(): DisposableHandle } -class InflatedContentViewHolder(val view: View, val binder: DeferredContentViewBinder) +enum class RichOngoingNotificationViewType { + Contracted, + Expanded, + HeadsUp, +} + +/** + * * Supertype of the 3 different possible result types of + * [RichOngoingNotificationViewInflater.inflateView]. + */ +sealed interface ContentViewInflationResult { + + /** Indicates that the content view should be removed if present. */ + data object NullContentView : ContentViewInflationResult + + /** + * Indicates that the content view (which *must be* present) should be unmodified during this + * inflation. + */ + data object KeepExistingView : ContentViewInflationResult + + /** + * Contains the new view and binder that should replace any existing content view for this slot. + */ + data class InflatedContentViewHolder(val view: View, val binder: DeferredContentViewBinder) : + ContentViewInflationResult +} + +fun ContentViewInflationResult?.shouldDisposeViewBinder() = this !is KeepExistingView /** * Interface which provides a [RichOngoingContentModel] for a given [Notification] when one is @@ -52,7 +86,14 @@ interface RichOngoingNotificationViewInflater { entry: NotificationEntry, systemUiContext: Context, parentView: ViewGroup, - ): InflatedContentViewHolder? + viewType: RichOngoingNotificationViewType, + ): ContentViewInflationResult + + fun canKeepView( + contentModel: RichOngoingContentModel, + existingView: View?, + viewType: RichOngoingNotificationViewType + ): Boolean } @SysUISingleton @@ -68,8 +109,9 @@ constructor( entry: NotificationEntry, systemUiContext: Context, parentView: ViewGroup, - ): InflatedContentViewHolder? { - if (RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()) return null + viewType: RichOngoingNotificationViewType, + ): ContentViewInflationResult { + if (RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()) return NullContentView val component = viewModelComponentFactory.create(entry) return when (contentModel) { is TimerContentModel -> @@ -77,9 +119,31 @@ constructor( existingView, component::createTimerViewModel, systemUiContext, - parentView + parentView, + viewType ) - is StopwatchContentModel -> TODO("Not yet implemented") + is EnRouteContentModel -> + inflateEnRouteView( + existingView, + component::createEnRouteViewModel, + systemUiContext, + parentView, + viewType + ) + else -> TODO("Not yet implemented") + } + } + + override fun canKeepView( + contentModel: RichOngoingContentModel, + existingView: View?, + viewType: RichOngoingNotificationViewType + ): Boolean { + if (RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()) return false + return when (contentModel) { + is TimerContentModel -> canKeepTimerView(contentModel, existingView, viewType) + is EnRouteContentModel -> canKeepEnRouteView(contentModel, existingView, viewType) + else -> TODO("Not yet implemented") } } @@ -88,17 +152,65 @@ constructor( createViewModel: () -> TimerViewModel, systemUiContext: Context, parentView: ViewGroup, - ): InflatedContentViewHolder? { - if (existingView is TimerView && !existingView.isReinflateNeeded()) return null - val newView = - LayoutInflater.from(systemUiContext) - .inflate( - R.layout.rich_ongoing_timer_notification, - parentView, - /* attachToRoot= */ false - ) as TimerView - return InflatedContentViewHolder(newView) { - TimerViewBinder.bindWhileAttached(newView, createViewModel()) + viewType: RichOngoingNotificationViewType, + ): ContentViewInflationResult { + if (existingView is TimerView && !existingView.isReinflateNeeded()) return KeepExistingView + + return when (viewType) { + RichOngoingNotificationViewType.Contracted -> { + val newView = + LayoutInflater.from(systemUiContext) + .inflate( + R.layout.rich_ongoing_timer_notification, + parentView, + /* attachToRoot= */ false + ) as TimerView + InflatedContentViewHolder(newView) { + TimerViewBinder.bindWhileAttached(newView, createViewModel()) + } + } + RichOngoingNotificationViewType.Expanded, + RichOngoingNotificationViewType.HeadsUp -> NullContentView + } + } + + private fun canKeepTimerView( + contentModel: TimerContentModel, + existingView: View?, + viewType: RichOngoingNotificationViewType + ): Boolean = true + + private fun inflateEnRouteView( + existingView: View?, + createViewModel: () -> EnRouteViewModel, + systemUiContext: Context, + parentView: ViewGroup, + viewType: RichOngoingNotificationViewType, + ): ContentViewInflationResult { + if (existingView is EnRouteView && !existingView.isReinflateNeeded()) + return KeepExistingView + return when (viewType) { + RichOngoingNotificationViewType.Contracted -> { + val newView = + LayoutInflater.from(systemUiContext) + .inflate( + R.layout.notification_template_en_route_contracted, + parentView, + /* attachToRoot= */ false + ) as EnRouteView + + InflatedContentViewHolder(newView) { + EnRouteViewBinder.bindWhileAttached(newView, createViewModel()) + } + } + RichOngoingNotificationViewType.Expanded, + RichOngoingNotificationViewType.HeadsUp -> NullContentView } } + + private fun canKeepEnRouteView( + contentModel: EnRouteContentModel, + existingView: View?, + viewType: RichOngoingNotificationViewType + ): Boolean = true } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt index 4705ace7fac6..72823a760197 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.row.domain.interactor import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository +import com.android.systemui.statusbar.notification.row.shared.EnRouteContentModel import com.android.systemui.statusbar.notification.row.shared.TimerContentModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -26,4 +27,8 @@ class NotificationRowInteractor @Inject constructor(repository: NotificationRowR /** Content of a rich ongoing timer notification. */ val timerContentModel: Flow<TimerContentModel> = repository.richOngoingContentModel.filterIsInstance<TimerContentModel>() + + /** Content of a rich ongoing timer notification. */ + val enRouteContentModel: Flow<EnRouteContentModel> = + repository.richOngoingContentModel.filterIsInstance<EnRouteContentModel>() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/EnRouteContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/EnRouteContentModel.kt new file mode 100644 index 000000000000..7e78cca028ef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/EnRouteContentModel.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row.shared + +/** + * Represents something en route. + * + * @param smallIcon the main small icon of the EnRoute notification. + * @param title the title of the EnRoute notification. + * @param text the text of the EnRoute notification. + */ +data class EnRouteContentModel( + val smallIcon: IconModel, + val title: CharSequence?, + val text: CharSequence?, +) : RichOngoingContentModel diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/EnRouteView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/EnRouteView.kt new file mode 100644 index 000000000000..e5c2b5fff5e9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/EnRouteView.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row.ui.view + +import android.content.Context +import android.graphics.drawable.Icon +import android.util.AttributeSet +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import com.android.internal.R +import com.android.internal.widget.NotificationExpandButton + +class EnRouteView +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0, +) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) { + + private val configTracker = ConfigurationTracker(resources) + + private lateinit var icon: ImageView + private lateinit var title: TextView + private lateinit var text: TextView + private lateinit var expandButton: NotificationExpandButton + + override fun onFinishInflate() { + super.onFinishInflate() + icon = requireViewById(R.id.icon) + title = requireViewById(R.id.title) + text = requireViewById(R.id.text) + + expandButton = requireViewById(R.id.expand_button) + expandButton.setExpanded(false) + } + + /** the resources configuration has changed such that the view needs to be reinflated */ + fun isReinflateNeeded(): Boolean = configTracker.hasUnhandledConfigChange() + + fun setIcon(icon: Icon?) { + this.icon.setImageIcon(icon) + } + + fun setTitle(title: CharSequence?) { + this.title.text = title + } + + fun setText(text: CharSequence?) { + this.text.text = text + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/EnRouteViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/EnRouteViewBinder.kt new file mode 100644 index 000000000000..3b8957c092f2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/EnRouteViewBinder.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row.ui.viewbinder + +import androidx.lifecycle.lifecycleScope +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.notification.row.ui.view.EnRouteView +import com.android.systemui.statusbar.notification.row.ui.viewmodel.EnRouteViewModel +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch + +/** Binds a [EnRouteView] to its [view model][EnRouteViewModel]. */ +object EnRouteViewBinder { + fun bindWhileAttached( + view: EnRouteView, + viewModel: EnRouteViewModel, + ): DisposableHandle { + return view.repeatWhenAttached { lifecycleScope.launch { bind(view, viewModel) } } + } + + suspend fun bind( + view: EnRouteView, + viewModel: EnRouteViewModel, + ) = coroutineScope { + launch { viewModel.icon.collect { view.setIcon(it) } } + launch { viewModel.title.collect { view.setTitle(it) } } + launch { viewModel.text.collect { view.setText(it) } } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModel.kt new file mode 100644 index 000000000000..307a9834ccc9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModel.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row.ui.viewmodel + +import android.graphics.drawable.Icon +import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.notification.row.domain.interactor.NotificationRowInteractor +import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag +import com.android.systemui.util.kotlin.FlowDumperImpl +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull + +/** A view model for EnRoute notifications. */ +class EnRouteViewModel +@Inject +constructor( + dumpManager: DumpManager, + rowInteractor: NotificationRowInteractor, +) : FlowDumperImpl(dumpManager) { + init { + /* check if */ RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode() + } + + val icon: Flow<Icon?> = rowInteractor.enRouteContentModel.mapNotNull { it.smallIcon.icon } + + val title: Flow<CharSequence?> = rowInteractor.enRouteContentModel.map { it.title } + + val text: Flow<CharSequence?> = rowInteractor.enRouteContentModel.map { it.text } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt index dad52a3b2c45..5552d89b6f9d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt @@ -33,4 +33,6 @@ interface RichOngoingViewModelComponent { } fun createTimerViewModel(): TimerViewModel + + fun createEnRouteViewModel(): EnRouteViewModel } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java index 6a2c60276186..a16129b076f8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java @@ -511,10 +511,12 @@ public class AmbientState implements Dumpable { } public int getTopPadding() { + SceneContainerFlag.assertInLegacyMode(); return mTopPadding; } public void setTopPadding(int topPadding) { + SceneContainerFlag.assertInLegacyMode(); mTopPadding = topPadding; } 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 f26f8405e299..d0c51bc28126 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 @@ -851,7 +851,7 @@ public class NotificationStackScrollLayout return; // the rest of the fields are not important in Flexiglass } - y = getTopPadding(); + y = mAmbientState.getTopPadding(); drawDebugInfo(canvas, y, Color.RED, /* label= */ "getTopPadding() = " + y); y = getLayoutHeight(); @@ -1231,9 +1231,11 @@ public class NotificationStackScrollLayout @Override public void setStackTop(float stackTop) { - mAmbientState.setStackTop(stackTop); - // TODO(b/332574413): replace the following with using stackTop - updateTopPadding(stackTop, isAddOrRemoveAnimationPending()); + if (mAmbientState.getStackTop() != stackTop) { + mAmbientState.setStackTop(stackTop); + onTopPaddingChanged(/* animate = */ isAddOrRemoveAnimationPending()); + setExpandedHeight(mExpandedHeight); + } } @Override @@ -1386,28 +1388,30 @@ public class NotificationStackScrollLayout } public int getTopPadding() { - return mAmbientState.getTopPadding(); + // TODO(b/332574413) replace all usages of getTopPadding() + if (SceneContainerFlag.isEnabled()) { + return (int) mAmbientState.getStackTop(); + } else { + return mAmbientState.getTopPadding(); + } } - private void setTopPadding(int topPadding, boolean animate) { - if (getTopPadding() != topPadding) { - mAmbientState.setTopPadding(topPadding); - boolean shouldAnimate = animate || mAnimateNextTopPaddingChange; - updateAlgorithmHeightAndPadding(); - updateContentHeight(); - if (mAmbientState.isOnKeyguard() - && !mShouldUseSplitNotificationShade - && mShouldSkipTopPaddingAnimationAfterFold) { - mShouldSkipTopPaddingAnimationAfterFold = false; - } else if (shouldAnimate && mAnimationsEnabled && mIsExpanded) { - mTopPaddingNeedsAnimation = true; - mNeedsAnimation = true; - } - updateStackPosition(); - requestChildrenUpdate(); - notifyHeightChangeListener(null, shouldAnimate); - mAnimateNextTopPaddingChange = false; + private void onTopPaddingChanged(boolean animate) { + boolean shouldAnimate = animate || mAnimateNextTopPaddingChange; + updateAlgorithmHeightAndPadding(); + updateContentHeight(); + if (mAmbientState.isOnKeyguard() + && !mShouldUseSplitNotificationShade + && mShouldSkipTopPaddingAnimationAfterFold) { + mShouldSkipTopPaddingAnimationAfterFold = false; + } else if (shouldAnimate && mAnimationsEnabled && mIsExpanded) { + mTopPaddingNeedsAnimation = true; + mNeedsAnimation = true; } + updateStackPosition(); + requestChildrenUpdate(); + notifyHeightChangeListener(null, shouldAnimate); + mAnimateNextTopPaddingChange = false; } /** @@ -1435,6 +1439,11 @@ public class NotificationStackScrollLayout * @param listenerNeedsAnimation does the listener need to animate? */ private void updateStackPosition(boolean listenerNeedsAnimation) { + // When scene container is active, we only want to recalculate stack heights. + if (SceneContainerFlag.isEnabled()) { + updateStackEndHeightAndStackHeight(mAmbientState.getExpansionFraction()); + return; + } float topOverscrollAmount = mShouldUseSplitNotificationShade ? getCurrentOverScrollAmount(true /* top */) : 0f; final float endTopPosition = getTopPadding() + mExtraTopInsetForFullShadeTransition @@ -1447,10 +1456,8 @@ public class NotificationStackScrollLayout if (mAmbientState.isBouncerInTransit() && mQsExpansionFraction > 0f) { fraction = BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(fraction); } - if (!SceneContainerFlag.isEnabled()) { - final float stackY = MathUtils.lerp(0, endTopPosition, fraction); - mAmbientState.setStackY(stackY); - } + final float stackY = MathUtils.lerp(0, endTopPosition, fraction); + mAmbientState.setStackY(stackY); if (mOnStackYChanged != null) { mOnStackYChanged.accept(listenerNeedsAnimation); @@ -2708,6 +2715,7 @@ public class NotificationStackScrollLayout * @param animate whether to animate the change */ public void updateTopPadding(float qsHeight, boolean animate) { + SceneContainerFlag.assertInLegacyMode(); int topPadding = (int) qsHeight; int minStackHeight = getLayoutMinHeightInternal(); if (topPadding + minStackHeight > getHeight()) { @@ -2715,7 +2723,10 @@ public class NotificationStackScrollLayout } else { mTopPaddingOverflow = 0; } - setTopPadding(topPadding, animate && !mKeyguardBypassEnabled); + if (mAmbientState.getTopPadding() != topPadding) { + mAmbientState.setTopPadding(topPadding); + onTopPaddingChanged(/* animate = */ animate && !mKeyguardBypassEnabled); + } setExpandedHeight(mExpandedHeight); } @@ -4829,6 +4840,7 @@ public class NotificationStackScrollLayout } public boolean isBelowLastNotification(float touchX, float touchY) { + SceneContainerFlag.assertInLegacyMode(); int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { ExpandableView child = getChildAtIndex(i); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 55f05662f0c6..c25b30db7754 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -1235,6 +1235,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public boolean isBelowLastNotification(float x, float y) { + SceneContainerFlag.assertInLegacyMode(); return mView.isBelowLastNotification(x, y); } @@ -1328,6 +1329,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public int getTopPadding() { + SceneContainerFlag.assertInLegacyMode(); return mView.getTopPadding(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt index 288924dd4000..1289cec3a282 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt @@ -86,6 +86,9 @@ interface NotificationScrollView { /** Sets whether the view is displayed in doze mode. */ fun setDozing(dozing: Boolean) + /** Sets whether the view is displayed in pulsing mode. */ + fun setPulsing(pulsing: Boolean, animated: Boolean) + /** Gets the inset for HUNs when they are not visible */ fun getHeadsUpInset(): Int diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt index 40761e07e33c..c044f6f6a9b1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt @@ -89,6 +89,11 @@ constructor( launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } } launch { viewModel.isDozing.collect { isDozing -> view.setDozing(isDozing) } } launch { + viewModel.isPulsing.collect { isPulsing -> + view.setPulsing(isPulsing, viewModel.shouldAnimatePulse.value) + } + } + launch { viewModel.shouldResetStackTop .filter { it } .collect { view.setStackTop(-(view.getHeadsUpInset().toFloat())) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt index 6489264fc2a7..b2045fe7569a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt @@ -17,10 +17,13 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel -import com.android.compose.animation.scene.ObservableTransitionState +import com.android.compose.animation.scene.ObservableTransitionState.Idle +import com.android.compose.animation.scene.ObservableTransitionState.Transition +import com.android.compose.animation.scene.ObservableTransitionState.Transition.ChangeCurrentScene import com.android.compose.animation.scene.SceneKey import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -39,6 +42,8 @@ import dagger.Lazy import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf @@ -58,42 +63,47 @@ constructor( keyguardInteractor: Lazy<KeyguardInteractor>, ) : ActivatableFlowDumper by ActivatableFlowDumperImpl(dumpManager, "NotificationScrollViewModel"), - SysUiViewModel() { + SysUiViewModel, + ExclusiveActivatable() { override suspend fun onActivated(): Nothing { activateFlowDumper() } - private fun expandFractionForScene(scene: SceneKey, shadeExpansion: Float): Float = - when (scene) { + private fun expandedInScene(scene: SceneKey): Boolean { + return when (scene) { Scenes.Lockscreen, - Scenes.QuickSettings -> 1f - else -> shadeExpansion + Scenes.Shade, + Scenes.QuickSettings -> true + else -> false } + } + + private fun fullyExpandedDuringSceneChange(change: ChangeCurrentScene): Boolean { + // The lockscreen stack is visible during all transitions away from the lockscreen, so keep + // the stack expanded until those transitions finish. + return (expandedInScene(change.fromScene) && expandedInScene(change.toScene)) || + change.isBetween({ it == Scenes.Lockscreen }, { true }) + } - private fun expandFractionForTransition( - state: ObservableTransitionState.Transition, + private fun expandFractionDuringSceneChange( + change: ChangeCurrentScene, shadeExpansion: Float, - shadeMode: ShadeMode, qsExpansion: Float, - quickSettingsScene: SceneKey - ): Float = - if ( - state.isBetween({ it == Scenes.Lockscreen }, { it in SceneFamilies.NotifShade }) || - state.isBetween({ it in SceneFamilies.NotifShade }, { it == quickSettingsScene }) - ) { + ): Float { + return if (fullyExpandedDuringSceneChange(change)) { 1f - } else if ( - shadeMode != ShadeMode.Split && - state.isBetween({ it in SceneFamilies.Home }, { it == quickSettingsScene }) - ) { + } else if (change.isBetween({ it == Scenes.Gone }, { it in SceneFamilies.NotifShade })) { + shadeExpansion + } else if (change.isBetween({ it == Scenes.Gone }, { it == Scenes.QuickSettings })) { // during QS expansion, increase fraction at same rate as scrim alpha, // but start when scrim alpha is at EXPANSION_FOR_DELAYED_STACK_FADE_IN. (qsExpansion / EXPANSION_FOR_MAX_SCRIM_ALPHA - EXPANSION_FOR_DELAYED_STACK_FADE_IN) .coerceIn(0f, 1f) } else { - shadeExpansion + 0f } + } /** * The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning @@ -107,18 +117,17 @@ constructor( shadeInteractor.qsExpansion, sceneInteractor.transitionState, sceneInteractor.resolveSceneFamily(SceneFamilies.QuickSettings), - ) { shadeExpansion, shadeMode, qsExpansion, transitionState, quickSettingsScene -> + ) { shadeExpansion, _, qsExpansion, transitionState, _ -> when (transitionState) { - is ObservableTransitionState.Idle -> - expandFractionForScene(transitionState.currentScene, shadeExpansion) - is ObservableTransitionState.Transition -> - expandFractionForTransition( + is Idle -> if (expandedInScene(transitionState.currentScene)) 1f else 0f + is ChangeCurrentScene -> + expandFractionDuringSceneChange( transitionState, shadeExpansion, - shadeMode, qsExpansion, - quickSettingsScene ) + is Transition.ShowOrHideOverlay, + is Transition.ReplaceOverlay -> TODO("b/359173565: Handle overlay transitions") } } .distinctUntilChanged() @@ -129,9 +138,7 @@ constructor( val shouldResetStackTop: Flow<Boolean> = sceneInteractor.transitionState - .mapNotNull { state -> - state is ObservableTransitionState.Idle && state.currentScene == Scenes.Gone - } + .mapNotNull { state -> state is Idle && state.currentScene == Scenes.Gone } .distinctUntilChanged() .dumpWhileCollecting("shouldResetStackTop") @@ -213,13 +220,30 @@ constructor( } } + /** Whether the notification stack is displayed in pulsing mode. */ + val isPulsing: Flow<Boolean> by lazy { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { + flowOf(false) + } else { + keyguardInteractor.get().isPulsing.dumpWhileCollecting("isPulsing") + } + } + + val shouldAnimatePulse: StateFlow<Boolean> by lazy { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { + MutableStateFlow(false) + } else { + keyguardInteractor.get().isAodAvailable + } + } + @AssistedFactory interface Factory { fun create(): NotificationScrollViewModel } } -private fun ObservableTransitionState.Transition.isBetween( +private fun ChangeCurrentScene.isBetween( a: (SceneKey) -> Boolean, b: (SceneKey) -> Boolean ): Boolean = (a(fromScene) && b(toScene)) || (b(fromScene) && a(toScene)) 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 ffa1de79b7e4..d891f62b4563 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 @@ -20,6 +20,7 @@ import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -52,7 +53,8 @@ constructor( featureFlags: FeatureFlagsClassic, dumpManager: DumpManager, ) : - SysUiViewModel(), + SysUiViewModel, + ExclusiveActivatable(), ActivatableFlowDumper by ActivatableFlowDumperImpl( dumpManager = dumpManager, tag = "NotificationsPlaceholderViewModel", 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 3dd265bb92da..e3242d13077f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -2365,9 +2365,14 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // lock screen where users can use the UDFPS affordance to enter the device mStatusBarKeyguardViewManager.reset(true); } else if (mState == StatusBarState.KEYGUARD - && !mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing() - && mStatusBarKeyguardViewManager.isSecure()) { - if (!relockWithPowerButtonImmediately()) { + && !mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()) { + boolean needsBouncer = mStatusBarKeyguardViewManager.isSecure(); + if (relockWithPowerButtonImmediately()) { + // Only request if SIM bouncer is needed + needsBouncer = mStatusBarKeyguardViewManager.needsFullscreenBouncer(); + } + + if (needsBouncer) { Log.d(TAG, "showBouncerOrLockScreenIfKeyguard, showingBouncer"); if (SceneContainerFlag.isEnabled()) { mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java index 00673169d036..f64941842a8f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java @@ -25,6 +25,7 @@ import com.android.systemui.doze.DozeHost; import com.android.systemui.doze.DozeLog; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import javax.inject.Inject; @@ -124,6 +125,11 @@ public class DozeScrimController implements StateListener { // Begin pulse. Note that it's very important that the pulse finished callback // be invoked when we're done so that the caller can drop the pulse wakelock. + if (SceneContainerFlag.isEnabled()) { + // ScrimController.Callback#onDisplayBlanked is no longer triggered when flexiglass is + // on, but we still need to signal that pulsing has started. + callback.onPulseStarted(); + } mPulseCallback = callback; mPulseReason = reason; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java index 3ba62b16a748..d1b5160fd490 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -44,6 +44,8 @@ import android.view.View; import androidx.lifecycle.Observer; +import com.android.settingslib.notification.modes.ZenIconLoader; +import com.android.settingslib.notification.modes.ZenMode; import com.android.systemui.Flags; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.DisplayId; @@ -78,6 +80,7 @@ import com.android.systemui.statusbar.policy.RotationLockController.RotationLock import com.android.systemui.statusbar.policy.SensorPrivacyController; import com.android.systemui.statusbar.policy.UserInfoController; import com.android.systemui.statusbar.policy.ZenModeController; +import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor; import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.time.DateFormatUtil; @@ -99,7 +102,6 @@ public class PhoneStatusBarPolicy CommandQueue.Callbacks, RotationLockControllerCallback, Listener, - ZenModeController.Callback, DeviceProvisionedListener, KeyguardStateController.Callback, PrivacyItemController.Callback, @@ -161,6 +163,7 @@ public class PhoneStatusBarPolicy private final RecordingController mRecordingController; private final RingerModeTracker mRingerModeTracker; private final PrivacyLogger mPrivacyLogger; + private final ZenModeInteractor mZenModeInteractor; private boolean mZenVisible; private boolean mVibrateVisible; @@ -193,6 +196,7 @@ public class PhoneStatusBarPolicy PrivacyItemController privacyItemController, PrivacyLogger privacyLogger, ConnectedDisplayInteractor connectedDisplayInteractor, + ZenModeInteractor zenModeInteractor, JavaAdapter javaAdapter ) { mIconController = iconController; @@ -224,6 +228,7 @@ public class PhoneStatusBarPolicy mTelecomManager = telecomManager; mRingerModeTracker = ringerModeTracker; mPrivacyLogger = privacyLogger; + mZenModeInteractor = zenModeInteractor; mJavaAdapter = javaAdapter; mSlotCast = resources.getString(com.android.internal.R.string.status_bar_cast); @@ -355,7 +360,13 @@ public class PhoneStatusBarPolicy mBluetooth.addCallback(this); mProvisionedController.addCallback(this); mCurrentUserSetup = mProvisionedController.isCurrentUserSetup(); - mZenController.addCallback(this); + if (usesModeIcons()) { + // Note that we're not fully replacing ZenModeController with ZenModeInteractor, so + // we listen for the extra event here but still add the ZMC callback. + mJavaAdapter.alwaysCollectFlow(mZenModeInteractor.getMainActiveMode(), + this::onActiveModeChanged); + } + mZenController.addCallback(mZenControllerCallback); if (!Flags.statusBarScreenSharingChips()) { // If the flag is enabled, the cast icon is handled in the new screen sharing chips // instead of here so we don't need to listen for events here. @@ -385,16 +396,45 @@ public class PhoneStatusBarPolicy () -> mResources.getString(R.string.accessibility_managed_profile)); } - @Override - public void onZenChanged(int zen) { - updateVolumeZen(); - } + private void onActiveModeChanged(@Nullable ZenMode mode) { + if (!usesModeIcons()) { + Log.wtf(TAG, "onActiveModeChanged shouldn't be called if MODES_UI_ICONS is disabled"); + return; + } + boolean visible = mode != null; + if (visible) { + // TODO: b/360399800 - Get the resource id, package, and cached drawable from the mode; + // this is a shortcut for testing (there should be no direct dependency on + // ZenIconLoader here). + String resPackage = mode.isSystemOwned() ? null : mode.getRule().getPackageName(); + int iconResId = mode.getRule().getIconResId(); + if (iconResId == 0) { + iconResId = ZenIconLoader.getIconResourceIdFromType(mode.getType()); + } - @Override - public void onConsolidatedPolicyChanged(NotificationManager.Policy policy) { - updateVolumeZen(); + mIconController.setResourceIcon(mSlotZen, resPackage, iconResId, + /* preloadedIcon= */ null, mode.getName()); + } + if (visible != mZenVisible) { + mIconController.setIconVisibility(mSlotZen, visible); + mZenVisible = visible; + } } + // TODO: b/308591859 - Should be removed and use the ZenModeInteractor only. + private final ZenModeController.Callback mZenControllerCallback = + new ZenModeController.Callback() { + @Override + public void onZenChanged(int zen) { + updateVolumeZen(); + } + + @Override + public void onConsolidatedPolicyChanged(NotificationManager.Policy policy) { + updateVolumeZen(); + } + }; + private void updateAlarm() { final AlarmClockInfo alarm = mAlarmManager.getNextAlarmClock(mUserTracker.getUserId()); final boolean hasAlarm = alarm != null && alarm.getTriggerTime() > 0; @@ -417,15 +457,24 @@ public class PhoneStatusBarPolicy return mResources.getString(R.string.accessibility_quick_settings_alarm, dateString); } - private final void updateVolumeZen() { + private void updateVolumeZen() { + int zen = mZenController.getZen(); + if (!usesModeIcons()) { + updateZenIcon(zen); + } + updateRingerAndAlarmIcons(zen); + } + + private void updateZenIcon(int zen) { + if (usesModeIcons()) { + Log.wtf(TAG, "updateZenIcon shouldn't be called if MODES_UI_ICONS is enabled"); + return; + } + boolean zenVisible = false; int zenIconId = 0; String zenDescription = null; - boolean vibrateVisible = false; - boolean muteVisible = false; - int zen = mZenController.getZen(); - if (DndTile.isVisible(mSharedPreferences) || DndTile.isCombinedIcon(mSharedPreferences)) { zenVisible = zen != Global.ZEN_MODE_OFF; zenIconId = R.drawable.stat_sys_dnd; @@ -440,7 +489,21 @@ public class PhoneStatusBarPolicy zenDescription = mResources.getString(R.string.interruption_level_priority); } - if (!ZenModeConfig.isZenOverridingRinger(zen, mZenController.getConsolidatedPolicy())) { + if (zenVisible) { + mIconController.setIcon(mSlotZen, zenIconId, zenDescription); + } + if (zenVisible != mZenVisible) { + mIconController.setIconVisibility(mSlotZen, zenVisible); + mZenVisible = zenVisible; + } + } + + private void updateRingerAndAlarmIcons(int zen) { + boolean vibrateVisible = false; + boolean muteVisible = false; + + NotificationManager.Policy consolidatedPolicy = mZenController.getConsolidatedPolicy(); + if (!ZenModeConfig.isZenOverridingRinger(zen, consolidatedPolicy)) { final Integer ringerModeInternal = mRingerModeTracker.getRingerModeInternal().getValue(); if (ringerModeInternal != null) { @@ -452,14 +515,6 @@ public class PhoneStatusBarPolicy } } - if (zenVisible) { - mIconController.setIcon(mSlotZen, zenIconId, zenDescription); - } - if (zenVisible != mZenVisible) { - mIconController.setIconVisibility(mSlotZen, zenVisible); - mZenVisible = zenVisible; - } - if (vibrateVisible != mVibrateVisible) { mIconController.setIconVisibility(mSlotVibrate, vibrateVisible); mVibrateVisible = vibrateVisible; @@ -888,4 +943,9 @@ public class PhoneStatusBarPolicy mIconController.setIconVisibility(mSlotConnectedDisplay, visible); } + + private static boolean usesModeIcons() { + return android.app.Flags.modesApi() && android.app.Flags.modesUi() + && android.app.Flags.modesUiIcons(); + } } 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 0f93ff2b70ed..f11fd7b29c18 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -68,7 +68,6 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor; import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor; import com.android.systemui.dock.DockManager; import com.android.systemui.dreams.DreamOverlayStateController; -import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.KeyguardWmStateRefactor; import com.android.systemui.keyguard.domain.interactor.KeyguardDismissActionInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardSurfaceBehindInteractor; @@ -104,6 +103,7 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.unfold.FoldAodAnimationController; import com.android.systemui.unfold.SysUIUnfoldComponent; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; +import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.kotlin.JavaAdapter; import dagger.Lazy; @@ -179,6 +179,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private float mFraction = -1f; private boolean mTracking = false; private boolean mBouncerShowingOverDream; + private int mAttemptsToShowBouncer = 0; + private DelayableExecutor mExecutor; private final PrimaryBouncerExpansionCallback mExpansionCallback = new PrimaryBouncerExpansionCallback() { @@ -315,8 +317,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private boolean mLastScreenOffAnimationPlaying; private float mQsExpansion; - private FeatureFlags mFlags; - final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>(); private boolean mIsBackAnimationEnabled; private final UdfpsOverlayInteractor mUdfpsOverlayInteractor; @@ -399,9 +399,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb JavaAdapter javaAdapter, Lazy<SceneInteractor> sceneInteractorLazy, StatusBarKeyguardViewManagerInteractor statusBarKeyguardViewManagerInteractor, + @Main DelayableExecutor executor, Lazy<DeviceEntryInteractor> deviceEntryInteractorLazy ) { mContext = context; + mExecutor = executor; mViewMediatorCallback = callback; mLockPatternUtils = lockPatternUtils; mConfigurationController = configurationController; @@ -711,13 +713,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * {@link #needsFullscreenBouncer()}. */ protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) { - boolean isDozing = mDozing; - if (Flags.simPinRaceConditionOnRestart()) { - KeyguardState toState = mKeyguardTransitionInteractor.getTransitionState().getValue() - .getTo(); - isDozing = mDozing || toState == KeyguardState.DOZING || toState == KeyguardState.AOD; - } - if (needsFullscreenBouncer() && !isDozing) { + if (needsFullscreenBouncer() && !mDozing) { // The keyguard might be showing (already). So we need to hide it. if (!primaryBouncerIsShowing()) { if (SceneContainerFlag.isEnabled()) { @@ -727,9 +723,22 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } else { if (Flags.simPinRaceConditionOnRestart()) { if (mPrimaryBouncerInteractor.show(/* isScrimmed= */ true)) { + mAttemptsToShowBouncer = 0; mCentralSurfaces.hideKeyguard(); } else { - mCentralSurfaces.showKeyguard(); + if (mAttemptsToShowBouncer > 6) { + mAttemptsToShowBouncer = 0; + Log.e(TAG, "Too many failed attempts to show bouncer, showing " + + "keyguard instead"); + mCentralSurfaces.showKeyguard(); + } else { + Log.v(TAG, "Failed to show bouncer, attempt #: " + + mAttemptsToShowBouncer++); + mExecutor.executeDelayed(() -> + showBouncerOrKeyguard(hideBouncerWhenShowing, + isFalsingReset), + 500); + } } } else { mCentralSurfaces.hideKeyguard(); @@ -1874,6 +1883,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb || mode == KeyguardSecurityModel.SecurityMode.SimPuk; } + @VisibleForTesting + void setAttemptsToShowBouncer(int attempts) { + mAttemptsToShowBouncer = attempts; + } + /** * Delegate used to send show and hide events to an alternate authentication method instead of * the regular pin/pattern/password bouncer. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconController.java index 1ada30e3518a..ee528e915079 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconController.java @@ -18,9 +18,12 @@ package com.android.systemui.statusbar.phone.ui; import android.annotation.Nullable; import android.content.Context; +import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.util.ArraySet; +import androidx.annotation.DrawableRes; + import com.android.internal.statusbar.StatusBarIcon; import com.android.systemui.res.R; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState; @@ -58,6 +61,18 @@ public interface StatusBarIconController { void setIcon(String slot, int resourceId, CharSequence contentDescription); /** + * Adds or updates an icon for the given slot. + * + * @param resPackage the package name containing the resource in question. Can be null if the + * icon is a system icon (e.g. a resource from {@code android.R.drawable} or + * {@code com.android.internal.R.drawable}). + * @param iconResId id of the drawable resource + * @param preloadedIcon optional drawable corresponding to {@code iconResId}, if known + */ + void setResourceIcon(String slot, @Nullable String resPackage, @DrawableRes int iconResId, + @Nullable Drawable preloadedIcon, CharSequence contentDescription); + + /** * Sets up a wifi icon using the new data pipeline. No effect if the wifi icon has already been * set up (inflated and added to the view hierarchy). */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImpl.java index 85213cb0ebff..ad3a9e350c4b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImpl.java @@ -18,16 +18,22 @@ package com.android.systemui.statusbar.phone.ui; import static com.android.systemui.statusbar.phone.ui.StatusBarIconList.Slot; +import static com.google.common.base.Preconditions.checkArgument; + import android.annotation.NonNull; import android.content.Context; +import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.Bundle; import android.os.UserHandle; +import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.view.ViewGroup; +import androidx.annotation.DrawableRes; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.internal.statusbar.StatusBarIcon; @@ -221,19 +227,66 @@ public class StatusBarIconControllerImpl implements Tunable, } } - /** */ @Override public void setIcon(String slot, int resourceId, CharSequence contentDescription) { + setResourceIconInternal( + slot, + Icon.createWithResource(mContext, resourceId), + /* preloadedIcon= */ null, + contentDescription, + StatusBarIcon.Type.SystemIcon); + } + + @Override + public void setResourceIcon(String slot, @Nullable String resPackage, + @DrawableRes int iconResId, @Nullable Drawable preloadedIcon, + CharSequence contentDescription) { + if (!usesModeIcons()) { + Log.wtf("TAG", + "StatusBarIconController.setResourceIcon() should not be called without " + + "MODES_UI & MODES_UI_ICONS!"); + // Fall back to old implementation, although it will not load the icon if it's from a + // different package. + setIcon(slot, iconResId, contentDescription); + return; + } + + Icon icon = resPackage != null + ? Icon.createWithResource(resPackage, iconResId) + : Icon.createWithResource(mContext, iconResId); + + setResourceIconInternal( + slot, + icon, + preloadedIcon, + contentDescription, + StatusBarIcon.Type.ResourceIcon); + } + + private void setResourceIconInternal(String slot, Icon resourceIcon, + @Nullable Drawable preloadedIcon, CharSequence contentDescription, + StatusBarIcon.Type type) { + checkArgument(resourceIcon.getType() == Icon.TYPE_RESOURCE, + "Expected Icon of TYPE_RESOURCE, but got " + resourceIcon.getType()); + String resPackage = resourceIcon.getResPackage(); + if (TextUtils.isEmpty(resPackage)) { + resPackage = mContext.getPackageName(); + } + StatusBarIconHolder holder = mStatusBarIconList.getIconHolder(slot, 0); if (holder == null) { - StatusBarIcon icon = new StatusBarIcon(UserHandle.SYSTEM, mContext.getPackageName(), - Icon.createWithResource(mContext, resourceId), 0, 0, - contentDescription, StatusBarIcon.Type.SystemIcon); + StatusBarIcon icon = new StatusBarIcon(UserHandle.SYSTEM, resPackage, + resourceIcon, /* iconLevel= */ 0, /* number=*/ 0, + contentDescription, type); + icon.preloadedIcon = preloadedIcon; holder = StatusBarIconHolder.fromIcon(icon); setIcon(slot, holder); } else { - holder.getIcon().icon = Icon.createWithResource(mContext, resourceId); + holder.getIcon().pkg = resPackage; + holder.getIcon().icon = resourceIcon; holder.getIcon().contentDescription = contentDescription; + holder.getIcon().type = type; + holder.getIcon().preloadedIcon = preloadedIcon; handleSet(slot, holder); } } @@ -524,4 +577,9 @@ public class StatusBarIconControllerImpl implements Tunable, return slot + EXTERNAL_SLOT_SUFFIX; } } + + private static boolean usesModeIcons() { + return android.app.Flags.modesApi() && android.app.Flags.modesUi() + && android.app.Flags.modesUiIcons(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt index 199b5b672140..37f2f195ebf6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt @@ -36,14 +36,12 @@ import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn /** @@ -76,37 +74,10 @@ constructor( @DeviceBasedSatelliteInputLog logBuffer: LogBuffer, @DeviceBasedSatelliteTableLog tableLog: TableLogBuffer, ) : DeviceBasedSatelliteViewModel { - private val shouldShowIcon: Flow<Boolean> = - interactor.areAllConnectionsOutOfService - .flatMapLatest { allOos -> - if (!allOos) { - flowOf(false) - } else { - combine( - interactor.isSatelliteAllowed, - interactor.isSatelliteProvisioned, - interactor.isWifiActive, - airplaneModeRepository.isAirplaneMode - ) { isSatelliteAllowed, isSatelliteProvisioned, isWifiActive, isAirplaneMode -> - isSatelliteAllowed && - isSatelliteProvisioned && - !isWifiActive && - !isAirplaneMode - } - } - } - .distinctUntilChanged() - .logDiffsForTable( - tableLog, - columnPrefix = "vm", - columnName = COL_VISIBLE_CONDITION, - initialValue = false, - ) // This adds a 10 seconds delay before showing the icon - private val shouldActuallyShowIcon: StateFlow<Boolean> = - shouldShowIcon - .distinctUntilChanged() + private val shouldShowIconForOosAfterHysteresis: StateFlow<Boolean> = + interactor.areAllConnectionsOutOfService .flatMapLatest { shouldShow -> if (shouldShow) { logBuffer.log( @@ -125,6 +96,45 @@ constructor( .logDiffsForTable( tableLog, columnPrefix = "vm", + columnName = COL_VISIBLE_FOR_OOS, + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + private val canShowIcon = + combine( + interactor.isSatelliteAllowed, + interactor.isSatelliteProvisioned, + ) { allowed, provisioned -> + allowed && provisioned + } + + private val showIcon = + canShowIcon + .flatMapLatest { canShow -> + if (!canShow) { + flowOf(false) + } else { + combine( + shouldShowIconForOosAfterHysteresis, + interactor.connectionState, + interactor.isWifiActive, + airplaneModeRepository.isAirplaneMode, + ) { showForOos, connectionState, isWifiActive, isAirplaneMode -> + if (isWifiActive || isAirplaneMode) { + false + } else { + showForOos || + connectionState == SatelliteConnectionState.On || + connectionState == SatelliteConnectionState.Connected + } + } + } + } + .distinctUntilChanged() + .logDiffsForTable( + tableLog, + columnPrefix = "vm", columnName = COL_VISIBLE, initialValue = false, ) @@ -132,7 +142,7 @@ constructor( override val icon: StateFlow<Icon?> = combine( - shouldActuallyShowIcon, + showIcon, interactor.connectionState, interactor.signalStrength, ) { shouldShow, state, signalStrength -> @@ -146,7 +156,7 @@ constructor( override val carrierText: StateFlow<String?> = combine( - shouldActuallyShowIcon, + showIcon, interactor.connectionState, ) { shouldShow, connectionState -> logBuffer.log( @@ -156,7 +166,7 @@ constructor( bool1 = shouldShow str1 = connectionState.name }, - { "Updating carrier text. shouldActuallyShow=$bool1 connectionState=$str1" } + { "Updating carrier text. shouldShow=$bool1 connectionState=$str1" } ) if (shouldShow) { when (connectionState) { @@ -165,28 +175,30 @@ constructor( context.getString(R.string.satellite_connected_carrier_text) SatelliteConnectionState.Off, SatelliteConnectionState.Unknown -> { - null + // If we're showing the satellite icon opportunistically, use the + // emergency-only version of the carrier string + context.getString(R.string.satellite_emergency_only_carrier_text) } } } else { null } } - .onEach { - logBuffer.log( - TAG, - LogLevel.INFO, - { str1 = it }, - { "Resulting carrier text = $str1" } - ) - } + .distinctUntilChanged() + .logDiffsForTable( + tableLog, + columnPrefix = "vm", + columnName = COL_CARRIER_TEXT, + initialValue = null, + ) .stateIn(scope, SharingStarted.WhileSubscribed(), null) companion object { private const val TAG = "DeviceBasedSatelliteViewModel" private val DELAY_DURATION = 10.seconds - const val COL_VISIBLE_CONDITION = "visCondition" + const val COL_VISIBLE_FOR_OOS = "visibleForOos" const val COL_VISIBLE = "visible" + const val COL_CARRIER_TEXT = "carrierText" } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt index f16fcb5d4e82..d351da68e1c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt @@ -95,10 +95,6 @@ constructor( return mode.getIcon(context, iconLoader).await().asIcon() } - suspend fun getLockscreenModeIcon(mode: ZenMode): Icon { - return mode.getLockscreenIcon(context, iconLoader).await().asIcon() - } - /** * Given the list of modes (which may include zero or more currently active modes), returns an * icon representing the active mode, if any (or, if multiple modes are active, to the most @@ -106,8 +102,8 @@ constructor( * standard DND icon for implicit modes, instead of the launcher icon of the associated * package). */ - suspend fun getActiveModeIcon(context: Context, modes: List<ZenMode>): Icon? { - return getMainActiveMode(modes)?.let { m -> getLockscreenModeIcon(m) } + suspend fun getActiveModeIcon(modes: List<ZenMode>): Icon? { + return getMainActiveMode(modes)?.let { m -> getModeIcon(m) } } fun activateMode(zenMode: ZenMode) { diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt index 1c8041ff5b31..a3b186744537 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt @@ -16,7 +16,6 @@ package com.android.systemui.touchpad.tutorial.ui.composable -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.airbnb.lottie.compose.rememberLottieDynamicProperties @@ -67,7 +66,6 @@ private fun rememberScreenColors(): TutorialScreenConfig.Colors { val onTertiaryFixed = LocalAndroidColorScheme.current.onTertiaryFixed val onTertiaryFixedVariant = LocalAndroidColorScheme.current.onTertiaryFixedVariant val tertiaryFixedDim = LocalAndroidColorScheme.current.tertiaryFixedDim - val surfaceContainer = MaterialTheme.colorScheme.surfaceContainer val dynamicProperties = rememberLottieDynamicProperties( rememberColorFilterProperty(".tertiaryFixedDim", tertiaryFixedDim), @@ -76,10 +74,9 @@ private fun rememberScreenColors(): TutorialScreenConfig.Colors { rememberColorFilterProperty(".onTertiaryFixedVariant", onTertiaryFixedVariant) ) val screenColors = - remember(onTertiaryFixed, surfaceContainer, tertiaryFixedDim, dynamicProperties) { + remember(dynamicProperties) { TutorialScreenConfig.Colors( background = onTertiaryFixed, - successBackground = surfaceContainer, title = tertiaryFixedDim, animationColors = dynamicProperties, ) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt index 0a6283aa7417..d4eb0cd7327b 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt @@ -16,7 +16,6 @@ package com.android.systemui.touchpad.tutorial.ui.composable -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.airbnb.lottie.compose.rememberLottieDynamicProperties @@ -66,7 +65,6 @@ private fun rememberScreenColors(): TutorialScreenConfig.Colors { val primaryFixedDim = LocalAndroidColorScheme.current.primaryFixedDim val onPrimaryFixed = LocalAndroidColorScheme.current.onPrimaryFixed val onPrimaryFixedVariant = LocalAndroidColorScheme.current.onPrimaryFixedVariant - val surfaceContainer = MaterialTheme.colorScheme.surfaceContainer val dynamicProperties = rememberLottieDynamicProperties( rememberColorFilterProperty(".primaryFixedDim", primaryFixedDim), @@ -74,10 +72,9 @@ private fun rememberScreenColors(): TutorialScreenConfig.Colors { rememberColorFilterProperty(".onPrimaryFixedVariant", onPrimaryFixedVariant) ) val screenColors = - remember(surfaceContainer, dynamicProperties) { + remember(dynamicProperties) { TutorialScreenConfig.Colors( background = onPrimaryFixed, - successBackground = surfaceContainer, title = primaryFixedDim, animationColors = dynamicProperties, ) diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt index ae0061b02ace..727e51fdef11 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt @@ -19,7 +19,7 @@ package com.android.systemui.util.kotlin import android.util.IndentingPrintWriter import com.android.systemui.Dumpable import com.android.systemui.dump.DumpManager -import com.android.systemui.lifecycle.BaseActivatable +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.util.asIndenting import com.android.systemui.util.printCollection @@ -189,7 +189,7 @@ class ActivatableFlowDumperImpl( ) : SimpleFlowDumper(), ActivatableFlowDumper { private val registration = - object : BaseActivatable() { + object : ExclusiveActivatable() { override suspend fun onActivated(): Nothing { try { dumpManager.registerCriticalDumpable( diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt index 28ac2c0e8283..055671cf32ca 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt @@ -28,6 +28,7 @@ import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -62,7 +63,9 @@ constructor( /** * Collect information for the given [flow], calling [consumer] for each emitted event. Defaults to * [LifeCycle.State.CREATED] to better align with legacy ViewController usage of attaching listeners - * during onViewAttached() and removing during onViewRemoved() + * during onViewAttached() and removing during onViewRemoved(). + * + * @return a disposable handle in order to cancel the flow in the future. */ @JvmOverloads fun <T> collectFlow( @@ -71,8 +74,8 @@ fun <T> collectFlow( consumer: Consumer<T>, coroutineContext: CoroutineContext = EmptyCoroutineContext, state: Lifecycle.State = Lifecycle.State.CREATED, -) { - view.repeatWhenAttached(coroutineContext) { +): DisposableHandle { + return view.repeatWhenAttached(coroutineContext) { repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } } } diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 50efc21f50fc..5f6ad9205ec7 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -60,7 +60,6 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.kotlin.JavaAdapter; -import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.onehanded.OneHanded; @@ -70,6 +69,7 @@ import com.android.wm.shell.onehanded.OneHandedUiEventLogger; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.recents.RecentTasks; +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.sysui.ShellInterface; diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserverTest.java new file mode 100644 index 000000000000..ba990efd5162 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserverTest.java @@ -0,0 +1,104 @@ +/* + * 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.accessibility; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.provider.Settings; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.settings.UserTracker; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Test for {@link AccessibilityGestureTargetsObserver}. */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class AccessibilityGestureTargetsObserverTest extends SysuiTestCase { + private static final int MY_USER_ID = ActivityManager.getCurrentUser(); + + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private UserTracker mUserTracker; + @Mock + private AccessibilityGestureTargetsObserver.TargetsChangedListener mListener; + + private AccessibilityGestureTargetsObserver mAccessibilityGestureTargetsObserver; + + private static final String TEST_A11Y_BTN_TARGETS = "Magnification"; + + @Before + public void setUp() { + when(mUserTracker.getUserId()).thenReturn(MY_USER_ID); + mAccessibilityGestureTargetsObserver = new AccessibilityGestureTargetsObserver(mContext, + mUserTracker); + } + + @Test + public void onChange_haveListener_invokeCallback() { + mAccessibilityGestureTargetsObserver.addListener(mListener); + Settings.Secure.putStringForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS, TEST_A11Y_BTN_TARGETS, + MY_USER_ID); + + mAccessibilityGestureTargetsObserver.mContentObserver.onChange(false); + + verify(mListener).onAccessibilityGestureTargetsChanged(TEST_A11Y_BTN_TARGETS); + } + + @Test + public void onChange_listenerRemoved_noInvokeCallback() { + mAccessibilityGestureTargetsObserver.addListener(mListener); + mAccessibilityGestureTargetsObserver.removeListener(mListener); + Settings.Secure.putStringForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS, TEST_A11Y_BTN_TARGETS, + MY_USER_ID); + + mAccessibilityGestureTargetsObserver.mContentObserver.onChange(false); + + verify(mListener, never()).onAccessibilityGestureTargetsChanged(anyString()); + } + + @Test + public void getCurrentAccessibilityGestureTargets_expectedValue() { + Settings.Secure.putStringForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS, TEST_A11Y_BTN_TARGETS, + MY_USER_ID); + + final String actualValue = + mAccessibilityGestureTargetsObserver.getCurrentAccessibilityGestureTargets(); + + assertThat(actualValue).isEqualTo(TEST_A11Y_BTN_TARGETS); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java index 5600b87280ad..a18d272b8fe3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java @@ -711,6 +711,16 @@ public class TouchMonitorTest extends SysuiTestCase { } @Test + public void testDestroy_cleansUpHandler() { + final TouchHandler touchHandler = createTouchHandler(); + + final Environment environment = new Environment(Stream.of(touchHandler) + .collect(Collectors.toCollection(HashSet::new)), mKosmos); + environment.destroyMonitor(); + verify(touchHandler).onDestroy(); + } + + @Test public void testLastSessionPop_createsNewInputSession() { final TouchHandler touchHandler = createTouchHandler(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt index e60848bb6bc3..6e9b24f1433e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt @@ -123,19 +123,19 @@ class CommunalBackupHelperTest : SysuiTestCase() { FakeWidgetMetadata( widgetId = 11, componentName = "com.android.fakePackage1/fakeWidget1", - rank = 3, + rank = 0, userSerialNumber = 0, ), FakeWidgetMetadata( widgetId = 12, componentName = "com.android.fakePackage2/fakeWidget2", - rank = 2, + rank = 1, userSerialNumber = 0, ), FakeWidgetMetadata( widgetId = 13, componentName = "com.android.fakePackage3/fakeWidget3", - rank = 1, + rank = 2, userSerialNumber = 10, ), ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt index eb0ab782ae3f..ad2550255e29 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt @@ -72,6 +72,82 @@ class CommunalDatabaseMigrationsTest : SysuiTestCase() { databaseV2.verifyWidgetsV2(fakeWidgetsV1.map { it.getV2() }) } + @Test + fun migrate2To3_noGapBetweenRanks_ranksReversed() { + // Create a communal database in version 2 + val databaseV2 = migrationTestHelper.createDatabase(DATABASE_NAME, version = 2) + + // Populate some fake data + val fakeRanks = + listOf( + FakeCommunalItemRank(3), + FakeCommunalItemRank(2), + FakeCommunalItemRank(1), + FakeCommunalItemRank(0), + ) + databaseV2.insertRanks(fakeRanks) + + // Verify fake ranks populated + databaseV2.verifyRanksInOrder(fakeRanks) + + // Run migration and get database V3 + val databaseV3 = + migrationTestHelper.runMigrationsAndValidate( + name = DATABASE_NAME, + version = 3, + validateDroppedTables = false, + CommunalDatabase.MIGRATION_2_3, + ) + + // Verify ranks are reversed + databaseV3.verifyRanksInOrder( + listOf( + FakeCommunalItemRank(0), + FakeCommunalItemRank(1), + FakeCommunalItemRank(2), + FakeCommunalItemRank(3), + ) + ) + } + + @Test + fun migrate2To3_withGapBetweenRanks_ranksReversed() { + // Create a communal database in version 2 + val databaseV2 = migrationTestHelper.createDatabase(DATABASE_NAME, version = 2) + + // Populate some fake data with gaps between ranks + val fakeRanks = + listOf( + FakeCommunalItemRank(9), + FakeCommunalItemRank(7), + FakeCommunalItemRank(2), + FakeCommunalItemRank(0), + ) + databaseV2.insertRanks(fakeRanks) + + // Verify fake ranks populated + databaseV2.verifyRanksInOrder(fakeRanks) + + // Run migration and get database V3 + val databaseV3 = + migrationTestHelper.runMigrationsAndValidate( + name = DATABASE_NAME, + version = 3, + validateDroppedTables = false, + CommunalDatabase.MIGRATION_2_3, + ) + + // Verify ranks are reversed + databaseV3.verifyRanksInOrder( + listOf( + FakeCommunalItemRank(0), + FakeCommunalItemRank(2), + FakeCommunalItemRank(7), + FakeCommunalItemRank(9), + ) + ) + } + private fun SupportSQLiteDatabase.insertWidgetsV1(widgets: List<FakeCommunalWidgetItemV1>) { widgets.forEach { widget -> execSQL( @@ -117,6 +193,25 @@ class CommunalDatabaseMigrationsTest : SysuiTestCase() { assertThat(cursor.isAfterLast).isTrue() } + private fun SupportSQLiteDatabase.insertRanks(ranks: List<FakeCommunalItemRank>) { + ranks.forEach { rank -> + execSQL("INSERT INTO communal_item_rank_table(rank) VALUES(${rank.rank})") + } + } + + private fun SupportSQLiteDatabase.verifyRanksInOrder(ranks: List<FakeCommunalItemRank>) { + val cursor = query("SELECT * FROM communal_item_rank_table ORDER BY uid") + assertThat(cursor.moveToFirst()).isTrue() + + ranks.forEach { rank -> + assertThat(cursor.getInt(cursor.getColumnIndex("rank"))).isEqualTo(rank.rank) + cursor.moveToNext() + } + + // Verify there is no more columns + assertThat(cursor.isAfterLast).isTrue() + } + /** * Returns the expected data after migration from V1 to V2, which is simply that the new user * serial number field is now set to [CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED]. @@ -143,6 +238,10 @@ class CommunalDatabaseMigrationsTest : SysuiTestCase() { val userSerialNumber: Int, ) + private data class FakeCommunalItemRank( + val rank: Int, + ) + companion object { private const val DATABASE_NAME = "communal_db" } diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt index 0b7a3ed3a7c2..6aecc0e2b8fc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt @@ -22,6 +22,7 @@ import android.hardware.biometrics.BiometricSourceType import android.os.PowerManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState import com.android.keyguard.keyguardUpdateMonitor import com.android.keyguard.trustManager import com.android.systemui.SysuiTestCase @@ -37,6 +38,7 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeFaceWakeUpTriggersConfig import com.android.systemui.deviceentry.shared.FaceAuthUiEvent import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository @@ -52,12 +54,15 @@ import com.android.systemui.log.logcatLogBuffer import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.eq import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -113,6 +118,7 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { powerInteractor, fakeBiometricSettingsRepository, trustManager, + { kosmos.sceneInteractor }, deviceEntryFaceAuthStatusInteractor, ) } @@ -279,6 +285,22 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { } @Test + @EnableSceneContainer + fun withSceneContainerEnabled_faceAuthIsRequestedWhenPrimaryBouncerIsVisible() = + testScope.runTest { + underTest.start() + + kosmos.sceneInteractor.changeScene(Scenes.Bouncer, "for-test") + kosmos.sceneInteractor.setTransitionState( + MutableStateFlow(ObservableTransitionState.Idle(Scenes.Bouncer)) + ) + + runCurrent() + assertThat(faceAuthRepository.runningAuthRequest.value) + .isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN, false)) + } + + @Test fun faceAuthIsRequestedWhenAlternateBouncerIsVisible() = testScope.runTest { underTest.start() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt index a0fe538bdd2b..3cbbb648af94 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt @@ -23,6 +23,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor +import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.EnableSceneContainer @@ -62,26 +63,19 @@ class KeyguardDismissActionInteractorTest : SysuiTestCase() { private val keyguardRepository = kosmos.fakeKeyguardRepository private val testScope = kosmos.testScope - private lateinit var dismissInteractorWithDependencies: - KeyguardDismissInteractorFactory.WithDependencies + private lateinit var dismissInteractor: KeyguardDismissInteractor private lateinit var underTest: KeyguardDismissActionInteractor @Before fun setUp() { MockitoAnnotations.initMocks(this) - dismissInteractorWithDependencies = - KeyguardDismissInteractorFactory.create( - context = context, - testScope = testScope, - keyguardRepository = keyguardRepository, - ) - + dismissInteractor = kosmos.keyguardDismissInteractor underTest = KeyguardDismissActionInteractor( repository = keyguardRepository, transitionInteractor = kosmos.keyguardTransitionInteractor, - dismissInteractor = dismissInteractorWithDependencies.interactor, + dismissInteractor = dismissInteractor, applicationScope = testScope.backgroundScope, sceneInteractor = kosmos.sceneInteractor, deviceEntryInteractor = kosmos.deviceEntryInteractor, @@ -166,9 +160,7 @@ class KeyguardDismissActionInteractorTest : SysuiTestCase() { willAnimateOnLockscreen = true, ) ) - dismissInteractorWithDependencies.bouncerRepository.setKeyguardAuthenticatedBiometrics( - true - ) + kosmos.fakeKeyguardBouncerRepository.setKeyguardAuthenticatedBiometrics(true) assertThat(executeDismissAction).isEqualTo(onDismissAction) } @@ -307,8 +299,7 @@ class KeyguardDismissActionInteractorTest : SysuiTestCase() { @Test fun setKeyguardDone() = testScope.runTest { - val keyguardDoneTiming by - collectLastValue(dismissInteractorWithDependencies.interactor.keyguardDone) + val keyguardDoneTiming by collectLastValue(dismissInteractor.keyguardDone) runCurrent() underTest.setKeyguardDone(KeyguardDone.LATER) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorTest.kt index ecb46bdd06c4..fabed03bc18c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorTest.kt @@ -23,11 +23,18 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.keyguard.TrustGrantFlags import com.android.systemui.SysuiTestCase +import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeTrustRepository import com.android.systemui.keyguard.shared.model.DismissAction import com.android.systemui.keyguard.shared.model.KeyguardDone import com.android.systemui.keyguard.shared.model.TrustModel +import com.android.systemui.power.data.repository.fakePowerRepository +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.TestScope @@ -38,14 +45,16 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class KeyguardDismissInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private lateinit var dispatcher: TestDispatcher private lateinit var testScope: TestScope - private lateinit var underTestDependencies: KeyguardDismissInteractorFactory.WithDependencies - private lateinit var underTest: KeyguardDismissInteractor + private val underTest = kosmos.keyguardDismissInteractor private val userInfo = UserInfo(0, "", 0) @Before @@ -54,13 +63,7 @@ class KeyguardDismissInteractorTest : SysuiTestCase() { dispatcher = StandardTestDispatcher() testScope = TestScope(dispatcher) - underTestDependencies = - KeyguardDismissInteractorFactory.create( - context = context, - testScope = testScope, - ) - underTest = underTestDependencies.interactor - underTestDependencies.userRepository.setUserInfos(listOf(userInfo)) + kosmos.fakeUserRepository.setUserInfos(listOf(userInfo)) } @Test @@ -69,10 +72,10 @@ class KeyguardDismissInteractorTest : SysuiTestCase() { val dismissKeyguardRequestWithoutImmediateDismissAction by collectLastValue(underTest.dismissKeyguardRequestWithoutImmediateDismissAction) - underTestDependencies.bouncerRepository.setKeyguardAuthenticatedBiometrics(null) + kosmos.fakeKeyguardBouncerRepository.setKeyguardAuthenticatedBiometrics(null) assertThat(dismissKeyguardRequestWithoutImmediateDismissAction).isNull() - underTestDependencies.bouncerRepository.setKeyguardAuthenticatedBiometrics(true) + kosmos.fakeKeyguardBouncerRepository.setKeyguardAuthenticatedBiometrics(true) assertThat(dismissKeyguardRequestWithoutImmediateDismissAction).isEqualTo(Unit) } @@ -81,7 +84,7 @@ class KeyguardDismissInteractorTest : SysuiTestCase() { testScope.runTest { val dismissKeyguardRequestWithoutImmediateDismissAction by collectLastValue(underTest.dismissKeyguardRequestWithoutImmediateDismissAction) - underTestDependencies.trustRepository.setRequestDismissKeyguard( + kosmos.fakeTrustRepository.setRequestDismissKeyguard( TrustModel( true, 0, @@ -90,8 +93,8 @@ class KeyguardDismissInteractorTest : SysuiTestCase() { ) assertThat(dismissKeyguardRequestWithoutImmediateDismissAction).isNull() - underTestDependencies.powerRepository.setInteractive(true) - underTestDependencies.trustRepository.setRequestDismissKeyguard( + kosmos.fakePowerRepository.setInteractive(true) + kosmos.fakeTrustRepository.setRequestDismissKeyguard( TrustModel( true, 0, @@ -106,15 +109,15 @@ class KeyguardDismissInteractorTest : SysuiTestCase() { testScope.runTest { val dismissKeyguardRequestWithoutImmediateDismissAction by collectLastValue(underTest.dismissKeyguardRequestWithoutImmediateDismissAction) - underTestDependencies.userRepository.setSelectedUserInfo(userInfo) + kosmos.fakeUserRepository.setSelectedUserInfo(userInfo) runCurrent() // authenticated different user - underTestDependencies.bouncerRepository.setKeyguardAuthenticatedPrimaryAuth(22) + kosmos.fakeKeyguardBouncerRepository.setKeyguardAuthenticatedPrimaryAuth(22) assertThat(dismissKeyguardRequestWithoutImmediateDismissAction).isNull() // authenticated correct user - underTestDependencies.bouncerRepository.setKeyguardAuthenticatedPrimaryAuth(userInfo.id) + kosmos.fakeKeyguardBouncerRepository.setKeyguardAuthenticatedPrimaryAuth(userInfo.id) assertThat(dismissKeyguardRequestWithoutImmediateDismissAction).isEqualTo(Unit) } @@ -123,17 +126,15 @@ class KeyguardDismissInteractorTest : SysuiTestCase() { testScope.runTest { val dismissKeyguardRequestWithoutImmediateDismissAction by collectLastValue(underTest.dismissKeyguardRequestWithoutImmediateDismissAction) - underTestDependencies.userRepository.setSelectedUserInfo(userInfo) + kosmos.fakeUserRepository.setSelectedUserInfo(userInfo) runCurrent() // requested from different user - underTestDependencies.bouncerRepository.setUserRequestedBouncerWhenAlreadyAuthenticated( - 22 - ) + kosmos.fakeKeyguardBouncerRepository.setUserRequestedBouncerWhenAlreadyAuthenticated(22) assertThat(dismissKeyguardRequestWithoutImmediateDismissAction).isNull() // requested from correct user - underTestDependencies.bouncerRepository.setUserRequestedBouncerWhenAlreadyAuthenticated( + kosmos.fakeKeyguardBouncerRepository.setUserRequestedBouncerWhenAlreadyAuthenticated( userInfo.id ) assertThat(dismissKeyguardRequestWithoutImmediateDismissAction).isEqualTo(Unit) @@ -159,10 +160,10 @@ class KeyguardDismissInteractorTest : SysuiTestCase() { collectLastValue(underTest.dismissKeyguardRequestWithoutImmediateDismissAction) val dismissKeyguardRequestWithImmediateDismissAction by collectLastValue(underTest.dismissKeyguardRequestWithImmediateDismissAction) - underTestDependencies.userRepository.setSelectedUserInfo(userInfo) + kosmos.fakeUserRepository.setSelectedUserInfo(userInfo) runCurrent() - underTestDependencies.keyguardRepository.setDismissAction( + kosmos.fakeKeyguardRepository.setDismissAction( DismissAction.RunImmediately( onDismissAction = { KeyguardDone.IMMEDIATE }, onCancelAction = {}, @@ -170,7 +171,7 @@ class KeyguardDismissInteractorTest : SysuiTestCase() { willAnimateOnLockscreen = true, ) ) - underTestDependencies.bouncerRepository.setUserRequestedBouncerWhenAlreadyAuthenticated( + kosmos.fakeKeyguardBouncerRepository.setUserRequestedBouncerWhenAlreadyAuthenticated( userInfo.id ) assertThat(dismissKeyguardRequestWithoutImmediateDismissAction).isNull() @@ -184,10 +185,10 @@ class KeyguardDismissInteractorTest : SysuiTestCase() { collectLastValue(underTest.dismissKeyguardRequestWithoutImmediateDismissAction) val dismissKeyguardRequestWithImmediateDismissAction by collectLastValue(underTest.dismissKeyguardRequestWithImmediateDismissAction) - underTestDependencies.userRepository.setSelectedUserInfo(userInfo) + kosmos.fakeUserRepository.setSelectedUserInfo(userInfo) runCurrent() - underTestDependencies.keyguardRepository.setDismissAction( + kosmos.fakeKeyguardRepository.setDismissAction( DismissAction.RunAfterKeyguardGone( dismissAction = {}, onCancelAction = {}, @@ -195,7 +196,7 @@ class KeyguardDismissInteractorTest : SysuiTestCase() { willAnimateOnLockscreen = true, ) ) - underTestDependencies.bouncerRepository.setUserRequestedBouncerWhenAlreadyAuthenticated( + kosmos.fakeKeyguardBouncerRepository.setUserRequestedBouncerWhenAlreadyAuthenticated( userInfo.id ) assertThat(dismissKeyguardRequestWithImmediateDismissAction).isNull() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt index 2021400b326a..664a0bdedec4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt @@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.policy.IKeyguardDismissCallback import com.android.systemui.SysuiTestCase -import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues @@ -31,7 +30,6 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope -import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any @@ -64,31 +62,13 @@ class AlternateBouncerViewModelTest : SysuiTestCase() { @Test fun onRemovedFromWindow() = testScope.runTest { - kosmos.primaryBouncerInteractor.setDismissAction( - mock(ActivityStarter.OnDismissAction::class.java), - {}, - ) - assertThat(kosmos.primaryBouncerInteractor.bouncerDismissAction).isNotNull() - - val dismissCallback = mock(IKeyguardDismissCallback::class.java) - kosmos.dismissCallbackRegistry.addCallback(dismissCallback) underTest.onRemovedFromWindow() - - kosmos.fakeExecutor.runAllReady() verify(statusBarKeyguardViewManager).hideAlternateBouncer(any()) - verify(dismissCallback).onDismissCancelled() - assertThat(kosmos.primaryBouncerInteractor.bouncerDismissAction).isNull() } @Test fun onBackRequested() = testScope.runTest { - kosmos.primaryBouncerInteractor.setDismissAction( - mock(ActivityStarter.OnDismissAction::class.java), - {}, - ) - assertThat(kosmos.primaryBouncerInteractor.bouncerDismissAction).isNotNull() - val dismissCallback = mock(IKeyguardDismissCallback::class.java) kosmos.dismissCallbackRegistry.addCallback(dismissCallback) @@ -96,7 +76,6 @@ class AlternateBouncerViewModelTest : SysuiTestCase() { kosmos.fakeExecutor.runAllReady() verify(statusBarKeyguardViewManager).hideAlternateBouncer(any()) verify(dismissCallback).onDismissCancelled() - assertThat(kosmos.primaryBouncerInteractor.bouncerDismissAction).isNull() } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index fc7f69319261..d13419eed281 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -30,6 +30,7 @@ import com.android.systemui.animation.Expandable import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor import com.android.systemui.dock.DockManagerFake import com.android.systemui.doze.util.BurnInHelperWrapper import com.android.systemui.flags.FakeFeatureFlags @@ -152,9 +153,7 @@ class KeyguardBottomAreaViewModelTest(flags: FlagsParameterization) : SysuiTestC dockManager = DockManagerFake() biometricSettingsRepository = FakeBiometricSettingsRepository() val featureFlags = - FakeFeatureFlags().apply { - set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false) - } + FakeFeatureFlags().apply { set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false) } val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags) val keyguardInteractor = withDeps.keyguardInteractor @@ -223,6 +222,7 @@ class KeyguardBottomAreaViewModelTest(flags: FlagsParameterization) : SysuiTestC broadcastDispatcher = broadcastDispatcher, accessibilityManager = accessibilityManager, pulsingGestureListener = kosmos.pulsingGestureListener, + faceAuthInteractor = kosmos.deviceEntryFaceAuthInteractor, ) underTest = KeyguardBottomAreaViewModel( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ActivatableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/ActivatableTest.kt index 67517a25ec87..67517a25ec87 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ActivatableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/ActivatableTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt index 7d5722035a14..c7acd78c5623 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt @@ -17,14 +17,8 @@ package com.android.systemui.lifecycle import android.view.View -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.test.assertTextEquals -import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -32,8 +26,6 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.util.Assert import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -157,51 +149,9 @@ class SysUiViewModelTest : SysuiTestCase() { assertThat(viewModel.isActivated).isTrue() } - - @Test - fun hydratedStateOf() { - val keepAliveMutable = mutableStateOf(true) - val upstreamStateFlow = MutableStateFlow(true) - val upstreamFlow = upstreamStateFlow.map { !it } - composeRule.setContent { - val keepAlive by keepAliveMutable - if (keepAlive) { - val viewModel = rememberViewModel { - FakeSysUiViewModel( - upstreamFlow = upstreamFlow, - upstreamStateFlow = upstreamStateFlow, - ) - } - - Column { - Text( - "upstreamStateFlow=${viewModel.stateBackedByStateFlow}", - Modifier.testTag("upstreamStateFlow") - ) - Text( - "upstreamFlow=${viewModel.stateBackedByFlow}", - Modifier.testTag("upstreamFlow") - ) - } - } - } - - composeRule.waitForIdle() - composeRule - .onNode(hasTestTag("upstreamStateFlow")) - .assertTextEquals("upstreamStateFlow=true") - composeRule.onNode(hasTestTag("upstreamFlow")).assertTextEquals("upstreamFlow=false") - - composeRule.runOnUiThread { upstreamStateFlow.value = false } - composeRule.waitForIdle() - composeRule - .onNode(hasTestTag("upstreamStateFlow")) - .assertTextEquals("upstreamStateFlow=false") - composeRule.onNode(hasTestTag("upstreamFlow")).assertTextEquals("upstreamFlow=true") - } } -private class FakeViewModel : SysUiViewModel() { +private class FakeViewModel : SysUiViewModel, ExclusiveActivatable() { var isActivated = false override suspend fun onActivated(): Nothing { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt index bdee9365d494..fd53b5baece5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt @@ -38,19 +38,28 @@ import android.media.session.MediaSession import android.media.session.PlaybackState import android.net.Uri import android.os.Bundle +import android.platform.test.flag.junit.FlagsParameterization import android.provider.Settings import android.service.notification.StatusBarNotification import android.testing.TestableLooper.RunWithLooper import androidx.media.utils.MediaConstants -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.internal.logging.InstanceId import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.Flags import com.android.systemui.InstanceIdSequenceFake import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.Flags.MEDIA_REMOTE_RESUME +import com.android.systemui.flags.Flags.MEDIA_RESUME_PROGRESS +import com.android.systemui.flags.Flags.MEDIA_RETAIN_RECOMMENDATIONS +import com.android.systemui.flags.Flags.MEDIA_RETAIN_SESSIONS +import com.android.systemui.flags.Flags.MEDIA_SESSION_ACTIONS +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.domain.resume.MediaResumeListener import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE @@ -58,16 +67,21 @@ import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERI import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.SmartspaceMediaData import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider -import com.android.systemui.media.controls.util.MediaControllerFactory -import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.media.controls.util.MediaUiEventLogger -import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.media.controls.util.fakeMediaControllerFactory +import com.android.systemui.media.controls.util.mediaFlags +import com.android.systemui.plugins.activityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.SbnBuilder +import com.android.systemui.testKosmos import com.android.systemui.tuner.TunerService import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before import org.junit.Rule @@ -91,6 +105,8 @@ import org.mockito.kotlin.any import org.mockito.kotlin.capture import org.mockito.kotlin.eq import org.mockito.quality.Strictness +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters private const val KEY = "KEY" private const val KEY_2 = "KEY_2" @@ -111,13 +127,13 @@ private fun <T> anyObject(): T { return Mockito.anyObject<T>() } +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWithLooper(setAsMainLooper = true) -@RunWith(AndroidJUnit4::class) -class LegacyMediaDataManagerImplTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { @JvmField @Rule val mockito = MockitoJUnit.rule() - @Mock lateinit var mediaControllerFactory: MediaControllerFactory @Mock lateinit var controller: MediaController @Mock lateinit var transportControls: MediaController.TransportControls @Mock lateinit var playbackInfo: MediaController.PlaybackInfo @@ -136,7 +152,6 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Mock lateinit var mediaDataFilter: LegacyMediaDataFilterImpl @Mock lateinit var listener: MediaDataManager.Listener @Mock lateinit var pendingIntent: PendingIntent - @Mock lateinit var activityStarter: ActivityStarter @Mock lateinit var smartspaceManager: SmartspaceManager @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider @@ -144,7 +159,6 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Mock private lateinit var mediaRecommendationItem: SmartspaceAction lateinit var validRecommendationList: List<SmartspaceAction> @Mock private lateinit var mediaSmartspaceBaseAction: SmartspaceAction - @Mock private lateinit var mediaFlags: MediaFlags @Mock private lateinit var logger: MediaUiEventLogger lateinit var mediaDataManager: LegacyMediaDataManagerImpl lateinit var mediaNotification: StatusBarNotification @@ -159,6 +173,26 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Mock private lateinit var ugm: IUriGrantsManager @Mock private lateinit var imageSource: ImageDecoder.Source + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.progressionOf( + Flags.FLAG_MEDIA_LOAD_METADATA_VIA_MEDIA_DATA_LOADER + ) + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + + private val kosmos = testKosmos() + private val testDispatcher = kosmos.testDispatcher + private val testScope = kosmos.testScope + private val fakeFeatureFlags = kosmos.fakeFeatureFlagsClassic + private val activityStarter = kosmos.activityStarter + private val mediaControllerFactory = kosmos.fakeMediaControllerFactory private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20) private val originalSmartspaceSetting = @@ -188,12 +222,16 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1 ) + mediaDataManager = LegacyMediaDataManagerImpl( context = context, backgroundExecutor = backgroundExecutor, + backgroundDispatcher = testDispatcher, uiExecutor = uiExecutor, foregroundExecutor = foregroundExecutor, + mainDispatcher = testDispatcher, + applicationScope = testScope, mediaControllerFactory = mediaControllerFactory, broadcastDispatcher = broadcastDispatcher, dumpManager = dumpManager, @@ -209,10 +247,11 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { useQsMediaPlayer = true, systemClock = clock, tunerService = tunerService, - mediaFlags = mediaFlags, + mediaFlags = kosmos.mediaFlags, logger = logger, smartspaceManager = smartspaceManager, keyguardUpdateMonitor = keyguardUpdateMonitor, + mediaDataLoader = { kosmos.mediaDataLoader }, ) verify(tunerService) .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION)) @@ -248,7 +287,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) } verify(smartspaceManager).createSmartspaceSession(capture(smartSpaceConfigBuilderCaptor)) - whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller) + mediaControllerFactory.setControllerForToken(session.sessionToken, controller) whenever(controller.transportControls).thenReturn(transportControls) whenever(controller.playbackInfo).thenReturn(playbackInfo) whenever(controller.metadata).thenReturn(metadataBuilder.build()) @@ -278,10 +317,11 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList) whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(SMARTSPACE_CREATION_TIME) whenever(mediaSmartspaceTarget.expiryTimeMillis).thenReturn(SMARTSPACE_EXPIRY_TIME) - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false) - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(false) - whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false) - whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(false) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, false) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, false) + fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, false) + fakeFeatureFlags.set(MEDIA_REMOTE_RESUME, false) + fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, false) whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId()) whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(false) } @@ -310,49 +350,51 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { } @Test - fun testsetInactive_resume_dismissesMedia() { - // WHEN resume controls are present, and time out - val desc = - MediaDescription.Builder().run { - setTitle(SESSION_TITLE) - build() - } - mediaDataManager.addResumptionControls( - USER_ID, - desc, - Runnable {}, - session.sessionToken, - APP_NAME, - pendingIntent, - PACKAGE_NAME - ) - - backgroundExecutor.runAllReady() - foregroundExecutor.runAllReady() - verify(listener) - .onMediaDataLoaded( - eq(PACKAGE_NAME), - eq(null), - capture(mediaDataCaptor), - eq(true), - eq(0), - eq(false) + fun testsetInactive_resume_dismissesMedia() = + testScope.runTest { + // WHEN resume controls are present, and time out + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + mediaDataManager.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME ) - mediaDataManager.setInactive(PACKAGE_NAME, timedOut = true) - verify(logger) - .logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(mediaDataCaptor.value.instanceId)) + runCurrent() + backgroundExecutor.runAllReady() + foregroundExecutor.runAllReady() + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) - // THEN it is removed and listeners are informed - foregroundExecutor.advanceClockToLast() - foregroundExecutor.runAllReady() - verify(listener).onMediaDataRemoved(PACKAGE_NAME, false) - } + mediaDataManager.setInactive(PACKAGE_NAME, timedOut = true) + verify(logger) + .logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(mediaDataCaptor.value.instanceId)) + + // THEN it is removed and listeners are informed + foregroundExecutor.advanceClockToLast() + foregroundExecutor.runAllReady() + verify(listener).onMediaDataRemoved(PACKAGE_NAME, false) + } @Test fun testLoadsMetadataOnBackground() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) - assertThat(backgroundExecutor.numPending()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 0, background = 1) } @Test @@ -370,8 +412,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { mediaDataManager.addListener(listener) mediaDataManager.onNotificationAdded(KEY, mediaNotification) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -389,8 +430,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { mediaDataManager.addListener(listener) mediaDataManager.onNotificationAdded(KEY, mediaNotification) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -417,11 +457,9 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testOnMetaDataLoaded_conservesActiveFlag() { - whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller) mediaDataManager.addListener(listener) mediaDataManager.onNotificationAdded(KEY, mediaNotification) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -465,8 +503,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { } mediaDataManager.onNotificationAdded(KEY, notif) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -552,8 +589,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) // Then a media control is created with a placeholder title string - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -583,8 +619,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) // Then a media control is created with a placeholder title string - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -625,8 +660,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) // Then the media control is added using the notification's title - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -734,8 +768,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { // GIVEN that the manager has two notifications with resume actions mediaDataManager.onNotificationAdded(KEY, mediaNotification) mediaDataManager.onNotificationAdded(KEY_2, mediaNotification) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(2) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(2) + testScope.assertRunAllReady(foreground = 2, background = 2) verify(listener) .onMediaDataLoaded( @@ -822,7 +855,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testOnNotificationRemoved_withResumption_isRemoteAndRemoteAllowed() { // With the flag enabled to allow remote media to resume - whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_REMOTE_RESUME, true) // GIVEN that the manager has a notification with a resume action, but is not local whenever(controller.metadata).thenReturn(metadataBuilder.build()) @@ -853,7 +886,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testOnNotificationRemoved_withResumption_isRcnAndRemoteAllowed() { // With the flag enabled to allow remote media to resume - whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_REMOTE_RESUME, true) // GIVEN that the manager has a remote cast notification addNotificationAndLoad(remoteCastNotification) @@ -972,7 +1005,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testAddResumptionControls_hasPartialProgress() { - whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) // WHEN resumption controls are added with partial progress val progress = 0.5 @@ -999,7 +1032,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testAddResumptionControls_hasNotPlayedProgress() { - whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) // WHEN resumption controls are added that have not been played val extras = @@ -1024,7 +1057,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testAddResumptionControls_hasFullProgress() { - whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) // WHEN resumption controls are added with progress info val extras = @@ -1050,7 +1083,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testAddResumptionControls_hasNoExtras() { - whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) // WHEN resumption controls are added that do not have any extras val desc = @@ -1068,7 +1101,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testAddResumptionControls_hasEmptyTitle() { - whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) // WHEN resumption controls are added that have empty title val desc = @@ -1087,8 +1120,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { ) // Resumption controls are not added. - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(0) + testScope.assertRunAllReady(foreground = 0, background = 1) verify(listener, never()) .onMediaDataLoaded( eq(PACKAGE_NAME), @@ -1102,7 +1134,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testAddResumptionControls_hasBlankTitle() { - whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) // WHEN resumption controls are added that have a blank title val desc = @@ -1121,8 +1153,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { ) // Resumption controls are not added. - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(0) + testScope.assertRunAllReady(foreground = 0, background = 1) verify(listener, never()) .onMediaDataLoaded( eq(PACKAGE_NAME), @@ -1189,8 +1220,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { mediaDataManager.onNotificationAdded(KEY, notif) // THEN it still loads - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -1307,7 +1337,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testOnSmartspaceMediaDataLoaded_persistentEnabled_headphoneTrigger_isActive() { - whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, true) smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) val instanceId = instanceIdSequence.lastInstanceId @@ -1333,7 +1363,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testOnSmartspaceMediaDataLoaded_persistentEnabled_periodicTrigger_notActive() { - whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, true) val extras = Bundle().apply { putString("package_name", PACKAGE_NAME) @@ -1367,7 +1397,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testOnSmartspaceMediaDataLoaded_persistentEnabled_noTargets_inactive() { - whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, true) smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) val instanceId = instanceIdSequence.lastInstanceId @@ -1399,7 +1429,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testSetRecommendationInactive_notifiesListeners() { - whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, true) smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) val instanceId = instanceIdSequence.lastInstanceId @@ -1479,8 +1509,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { fun testOnMediaDataTimedOut_updatesLastActiveTime() { // GIVEN that the manager has a notification mediaDataManager.onNotificationAdded(KEY, mediaNotification) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) // WHEN the notification times out clock.advanceTime(100) @@ -1588,8 +1617,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { // WHEN the notification is loaded mediaDataManager.onNotificationAdded(KEY, notif) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) // THEN only the first MAX_COMPACT_ACTIONS are actually set verify(listener) @@ -1624,8 +1652,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { // WHEN the notification is loaded mediaDataManager.onNotificationAdded(KEY, notif) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) // THEN only the first MAX_NOTIFICATION_ACTIONS are actually included verify(listener) @@ -1644,7 +1671,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testPlaybackActions_noState_usesNotification() { val desc = "Notification Action" - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) whenever(controller.playbackState).thenReturn(null) val notifWithAction = @@ -1659,8 +1686,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { } mediaDataManager.onNotificationAdded(KEY, notifWithAction) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -1679,7 +1705,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testPlaybackActions_hasPrevNext() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) val stateActions = PlaybackState.ACTION_PLAY or PlaybackState.ACTION_SKIP_TO_PREVIOUS or @@ -1723,7 +1749,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testPlaybackActions_noPrevNext_usesCustom() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5") - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) val stateActions = PlaybackState.ACTION_PLAY val stateBuilder = PlaybackState.Builder().setActions(stateActions) customDesc.forEach { @@ -1755,7 +1781,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testPlaybackActions_connecting() { - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) val stateActions = PlaybackState.ACTION_PLAY val stateBuilder = PlaybackState.Builder() @@ -1776,7 +1802,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testPlaybackActions_reservedSpace() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) val stateActions = PlaybackState.ACTION_PLAY val stateBuilder = PlaybackState.Builder().setActions(stateActions) customDesc.forEach { @@ -1814,7 +1840,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testPlaybackActions_playPause_hasButton() { - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) val stateActions = PlaybackState.ACTION_PLAY_PAUSE val stateBuilder = PlaybackState.Builder().setActions(stateActions) whenever(controller.playbackState).thenReturn(stateBuilder.build()) @@ -1851,8 +1877,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { // update to remote cast mediaDataManager.onNotificationAdded(KEY, remoteCastNotification) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(logger) .logPlaybackLocationChange( anyInt(), @@ -1914,7 +1939,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testPlaybackState_PauseWhenFlagTrue_keyExists_callsListener() { - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) val state = PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 1f).build() whenever(controller.playbackState).thenReturn(state) @@ -1935,46 +1960,48 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { } @Test - fun testPlaybackState_PauseStateAfterAddingResumption_keyExists_callsListener() { - val desc = - MediaDescription.Builder().run { - setTitle(SESSION_TITLE) - build() - } - val state = - PlaybackState.Builder() - .setState(PlaybackState.STATE_PAUSED, 0L, 1f) - .setActions(PlaybackState.ACTION_PLAY_PAUSE) - .build() - - // Add resumption controls in order to have semantic actions. - // To make sure that they are not null after changing state. - mediaDataManager.addResumptionControls( - USER_ID, - desc, - Runnable {}, - session.sessionToken, - APP_NAME, - pendingIntent, - PACKAGE_NAME - ) - backgroundExecutor.runAllReady() - foregroundExecutor.runAllReady() - - stateCallbackCaptor.value.invoke(PACKAGE_NAME, state) + fun testPlaybackState_PauseStateAfterAddingResumption_keyExists_callsListener() = + testScope.runTest { + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + val state = + PlaybackState.Builder() + .setState(PlaybackState.STATE_PAUSED, 0L, 1f) + .setActions(PlaybackState.ACTION_PLAY_PAUSE) + .build() - verify(listener) - .onMediaDataLoaded( - eq(PACKAGE_NAME), - eq(PACKAGE_NAME), - capture(mediaDataCaptor), - eq(true), - eq(0), - eq(false) + // Add resumption controls in order to have semantic actions. + // To make sure that they are not null after changing state. + mediaDataManager.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME ) - assertThat(mediaDataCaptor.value.isPlaying).isFalse() - assertThat(mediaDataCaptor.value.semanticActions).isNotNull() - } + runCurrent() + backgroundExecutor.runAllReady() + foregroundExecutor.runAllReady() + + stateCallbackCaptor.value.invoke(PACKAGE_NAME, state) + + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(PACKAGE_NAME), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.isPlaying).isFalse() + assertThat(mediaDataCaptor.value.semanticActions).isNotNull() + } @Test fun testPlaybackStateNull_Pause_keyExists_callsListener() { @@ -2036,7 +2063,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testRetain_notifPlayer_notifRemoved_setToResume() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) // When a media control based on notification is added, times out, and then removed addNotificationAndLoad() @@ -2066,7 +2093,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testRetain_notifPlayer_sessionDestroyed_doesNotChange() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) // When a media control based on notification is added and times out addNotificationAndLoad() @@ -2084,7 +2111,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testRetain_notifPlayer_removeWhileActive_fullyRemoved() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) // When a media control based on notification is added and then removed, without timing out addNotificationAndLoad() @@ -2101,7 +2128,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testRetain_canResume_removeWhileActive_setToResume() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) // When a media control that supports resumption is added addNotificationAndLoad() @@ -2133,8 +2160,8 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testRetain_sessionPlayer_notifRemoved_doesNotChange() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) addPlaybackStateAction() // When a media control with PlaybackState actions is added, times out, @@ -2153,8 +2180,8 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testRetain_sessionPlayer_sessionDestroyed_setToResume() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) addPlaybackStateAction() // When a media control with PlaybackState actions is added, times out, @@ -2187,8 +2214,8 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testRetain_sessionPlayer_destroyedWhileActive_noResume_fullyRemoved() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) addPlaybackStateAction() // When a media control using session actions is added, and then the session is destroyed @@ -2207,8 +2234,8 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testRetain_sessionPlayer_canResume_destroyedWhileActive_setToResume() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) addPlaybackStateAction() // When a media control using session actions and that does allow resumption is added, @@ -2241,7 +2268,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testSessionPlayer_sessionDestroyed_noResume_fullyRemoved() { - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) addPlaybackStateAction() // When a media control with PlaybackState actions is added, times out, @@ -2268,7 +2295,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testSessionPlayer_destroyedWhileActive_noResume_fullyRemoved() { - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) addPlaybackStateAction() // When a media control using session actions is added, and then the session is destroyed @@ -2287,7 +2314,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testSessionPlayer_canResume_destroyedWhileActive_setToResume() { - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) addPlaybackStateAction() // When a media control using session actions and that does allow resumption is added, @@ -2320,8 +2347,8 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { @Test fun testSessionDestroyed_noNotificationKey_stillRemoved() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) // When a notiifcation is added and then removed before it is fully processed mediaDataManager.onNotificationAdded(KEY, mediaNotification) @@ -2392,6 +2419,23 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { assertThat(mediaDataCaptor.value.artwork).isNull() } + private fun TestScope.assertRunAllReady(foreground: Int = 0, background: Int = 0) { + runCurrent() + if (Flags.mediaLoadMetadataViaMediaDataLoader()) { + // It doesn't make much sense to count tasks when we use coroutines in loader + // so this check is skipped in that scenario. + backgroundExecutor.runAllReady() + foregroundExecutor.runAllReady() + } else { + if (background > 0) { + assertThat(backgroundExecutor.runAllReady()).isEqualTo(background) + } + if (foreground > 0) { + assertThat(foregroundExecutor.runAllReady()).isEqualTo(foreground) + } + } + } + /** Helper function to add a basic media notification and capture the resulting MediaData */ private fun addNotificationAndLoad() { addNotificationAndLoad(mediaNotification) @@ -2400,8 +2444,7 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { /** Helper function to add the given notification and capture the resulting MediaData */ private fun addNotificationAndLoad(sbn: StatusBarNotification) { mediaDataManager.onNotificationAdded(KEY, sbn) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -2435,8 +2478,8 @@ class LegacyMediaDataManagerImplTest : SysuiTestCase() { pendingIntent, packageName ) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt index 3b541cd98f4e..99c5b7cdfdc5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt @@ -38,6 +38,9 @@ import android.media.session.MediaSession import android.media.session.PlaybackState import android.net.Uri import android.os.Bundle +import android.os.UserHandle +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.provider.Settings import android.service.notification.StatusBarNotification import android.testing.TestableLooper @@ -48,10 +51,14 @@ import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.internal.logging.InstanceId import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.Flags import com.android.systemui.InstanceIdSequenceFake import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.data.repository.MediaDataRepository import com.android.systemui.media.controls.data.repository.MediaFilterRepository import com.android.systemui.media.controls.data.repository.mediaFilterRepository @@ -69,6 +76,7 @@ import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.SbnBuilder +import com.android.systemui.statusbar.notificationLockscreenUserManager import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.settings.FakeSettings @@ -79,6 +87,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before import org.junit.Rule @@ -126,6 +135,7 @@ private fun <T> anyObject(): T { @SmallTest @RunWithLooper(setAsMainLooper = true) @RunWith(AndroidJUnit4::class) +@EnableSceneContainer class MediaDataProcessorTest : SysuiTestCase() { val kosmos = testKosmos() @@ -146,7 +156,6 @@ class MediaDataProcessorTest : SysuiTestCase() { @Mock lateinit var mediaSessionBasedFilter: MediaSessionBasedFilter @Mock lateinit var mediaDeviceManager: MediaDeviceManager @Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest - @Mock lateinit var mediaDataFilter: MediaDataFilterImpl @Mock lateinit var listener: MediaDataManager.Listener @Mock lateinit var pendingIntent: PendingIntent @Mock lateinit var activityStarter: ActivityStarter @@ -185,14 +194,14 @@ class MediaDataProcessorTest : SysuiTestCase() { Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1 ) + private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager private val mediaFilterRepository: MediaFilterRepository = kosmos.mediaFilterRepository + private val mediaDataFilter: MediaDataFilterImpl = kosmos.mediaDataFilter private lateinit var staticMockSession: MockitoSession @Before fun setup() { - whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) - staticMockSession = ExtendedMockito.mockitoSession() .mockStatic<UriGrantsManager>(UriGrantsManager::class.java) @@ -258,6 +267,7 @@ class MediaDataProcessorTest : SysuiTestCase() { session = MediaSession(context, "MediaDataProcessorTestSession") mediaNotification = SbnBuilder().run { + setUser(UserHandle(USER_ID)) setPkg(PACKAGE_NAME) modifyNotification(context).also { it.setSmallIcon(android.R.drawable.ic_media_pause) @@ -1798,6 +1808,85 @@ class MediaDataProcessorTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE) + fun postWithPlaybackActions_drawablesReused() = + kosmos.testScope.runTest { + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) + whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) + val stateActions = + PlaybackState.ACTION_PAUSE or + PlaybackState.ACTION_SKIP_TO_PREVIOUS or + PlaybackState.ACTION_SKIP_TO_NEXT + val stateBuilder = + PlaybackState.Builder() + .setState(PlaybackState.STATE_PLAYING, 0, 10f) + .setActions(stateActions) + whenever(controller.playbackState).thenReturn(stateBuilder.build()) + val userEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + + mediaDataProcessor.addInternalListener(mediaDataFilter) + mediaDataFilter.mediaDataProcessor = mediaDataProcessor + addNotificationAndLoad() + + assertThat(userEntries).hasSize(1) + val firstSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!! + + addNotificationAndLoad() + + assertThat(userEntries).hasSize(1) + val secondSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!! + assertThat(secondSemanticActions.playOrPause?.icon) + .isEqualTo(firstSemanticActions.playOrPause?.icon) + assertThat(secondSemanticActions.playOrPause?.background) + .isEqualTo(firstSemanticActions.playOrPause?.background) + assertThat(secondSemanticActions.nextOrCustom?.icon) + .isEqualTo(firstSemanticActions.nextOrCustom?.icon) + assertThat(secondSemanticActions.prevOrCustom?.icon) + .isEqualTo(firstSemanticActions.prevOrCustom?.icon) + } + + @Test + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE) + fun postWithPlaybackActions_drawablesNotReused() = + kosmos.testScope.runTest { + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) + whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) + val stateActions = + PlaybackState.ACTION_PAUSE or + PlaybackState.ACTION_SKIP_TO_PREVIOUS or + PlaybackState.ACTION_SKIP_TO_NEXT + val stateBuilder = + PlaybackState.Builder() + .setState(PlaybackState.STATE_PLAYING, 0, 10f) + .setActions(stateActions) + whenever(controller.playbackState).thenReturn(stateBuilder.build()) + val userEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + + mediaDataProcessor.addInternalListener(mediaDataFilter) + mediaDataFilter.mediaDataProcessor = mediaDataProcessor + addNotificationAndLoad() + + assertThat(userEntries).hasSize(1) + val firstSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!! + + addNotificationAndLoad() + + assertThat(userEntries).hasSize(1) + val secondSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!! + + assertThat(secondSemanticActions.playOrPause?.icon) + .isNotEqualTo(firstSemanticActions.playOrPause?.icon) + assertThat(secondSemanticActions.playOrPause?.background) + .isNotEqualTo(firstSemanticActions.playOrPause?.background) + assertThat(secondSemanticActions.nextOrCustom?.icon) + .isNotEqualTo(firstSemanticActions.nextOrCustom?.icon) + assertThat(secondSemanticActions.prevOrCustom?.icon) + .isNotEqualTo(firstSemanticActions.prevOrCustom?.icon) + } + + @Test fun testPlaybackActions_reservedSpace() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt index 514273042685..6a66c4087615 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt @@ -254,22 +254,17 @@ public class MediaDeviceManagerTest : SysuiTestCase() { // AND that token results in a null route whenever(playbackInfo.playbackType).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null) - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() + val data = loadMediaAndCaptureDeviceData() + // THEN the device should be disabled - val data = captureDeviceData(KEY) assertThat(data.enabled).isFalse() } @Test fun deviceEventOnAddNotification() { // WHEN a notification is added - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() // THEN the update is dispatched to the listener - val data = captureDeviceData(KEY) + val data = loadMediaAndCaptureDeviceData() assertThat(data.enabled).isTrue() assertThat(data.name).isEqualTo(DEVICE_NAME) assertThat(data.icon).isEqualTo(icon) @@ -417,15 +412,40 @@ public class MediaDeviceManagerTest : SysuiTestCase() { whenever(routingSession.name).thenReturn(REMOTE_DEVICE_NAME) whenever(playbackInfo.playbackType).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) // WHEN a notification is added - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() // THEN it uses the route name (instead of device name) - val data = captureDeviceData(KEY) + val data = loadMediaAndCaptureDeviceData() assertThat(data.enabled).isTrue() assertThat(data.name).isEqualTo(REMOTE_DEVICE_NAME) } + @Test + @EnableFlags(com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE) + fun onMediaDataLoaded_withRemotePlaybackType_usesNonNullRoutingSessionName_drawableReused() { + whenever(routingSession.name).thenReturn(REMOTE_DEVICE_NAME) + whenever(routingSession.selectedRoutes).thenReturn(listOf("selectedRoute", "selectedRoute")) + whenever(playbackInfo.playbackType).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) + + val firstData = loadMediaAndCaptureDeviceData() + reset(listener) + val secondData = loadMediaAndCaptureDeviceData() + + assertThat(secondData.icon).isEqualTo(firstData.icon) + } + + @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE) + fun onMediaDataLoaded_withRemotePlaybackType_usesNonNullRoutingSessionName_drawableNotReused() { + whenever(routingSession.name).thenReturn(REMOTE_DEVICE_NAME) + whenever(routingSession.selectedRoutes).thenReturn(listOf("selectedRoute", "selectedRoute")) + whenever(playbackInfo.playbackType).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) + + val firstData = loadMediaAndCaptureDeviceData() + reset(listener) + val secondData = loadMediaAndCaptureDeviceData() + + assertThat(secondData.icon).isNotEqualTo(firstData.icon) + } + @RequiresFlagsDisabled(FLAG_USE_PLAYBACK_INFO_FOR_ROUTING_CONTROLS) @Test fun onMediaDataLoaded_withRemotePlaybackInfo_noMatchingRoutingSession_setsDisabledDevice() { @@ -433,11 +453,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { whenever(playbackInfo.playbackType).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null) // WHEN a notification is added - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() // THEN the device is disabled and name is set to null - val data = captureDeviceData(KEY) + val data = loadMediaAndCaptureDeviceData() assertThat(data.enabled).isFalse() assertThat(data.name).isNull() } @@ -449,23 +466,48 @@ public class MediaDeviceManagerTest : SysuiTestCase() { whenever(playbackInfo.playbackType).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null) // WHEN a notification is added - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() // THEN the device is disabled and name and icon are set to "OTHER DEVICE". - val data = captureDeviceData(KEY) + val data = loadMediaAndCaptureDeviceData() assertThat(data.enabled).isFalse() assertThat(data.name).isEqualTo(context.getString(R.string.media_seamless_other_device)) assertThat(data.icon).isEqualTo(OTHER_DEVICE_ICON_STUB) } + @Test + @RequiresFlagsEnabled(FLAG_USE_PLAYBACK_INFO_FOR_ROUTING_CONTROLS) + @EnableFlags(com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE) + fun onMediaDataLoaded_withRemotePlaybackInfo_noMatchingRoutingSession_drawableReused() { + whenever(playbackInfo.playbackType).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) + whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null) + context.orCreateTestableResources.removeOverride(R.drawable.ic_media_home_devices) + + val firstData = loadMediaAndCaptureDeviceData() + reset(listener) + val secondData = loadMediaAndCaptureDeviceData() + + assertThat(secondData.icon).isEqualTo(firstData.icon) + } + + @Test + @RequiresFlagsEnabled(FLAG_USE_PLAYBACK_INFO_FOR_ROUTING_CONTROLS) + @DisableFlags(com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE) + fun onMediaDataLoaded_withRemotePlaybackInfo_noMatchingRoutingSession_drawableNotReused() { + whenever(playbackInfo.playbackType).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) + whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null) + context.orCreateTestableResources.removeOverride(R.drawable.ic_media_home_devices) + + val firstData = loadMediaAndCaptureDeviceData() + reset(listener) + val secondData = loadMediaAndCaptureDeviceData() + + assertThat(secondData.icon).isNotEqualTo(firstData.icon) + } + @RequiresFlagsDisabled(FLAG_USE_PLAYBACK_INFO_FOR_ROUTING_CONTROLS) @Test fun onSelectedDeviceStateChanged_withRemotePlaybackInfo_noMatchingRoutingSession_setsDisabledDevice() { // GIVEN a notif is added - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() + loadMediaAndCaptureDeviceData() reset(listener) // AND MR2Manager returns null for routing session whenever(playbackInfo.playbackType).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) @@ -480,13 +522,12 @@ public class MediaDeviceManagerTest : SysuiTestCase() { assertThat(data.enabled).isFalse() assertThat(data.name).isNull() } + @RequiresFlagsEnabled(FLAG_USE_PLAYBACK_INFO_FOR_ROUTING_CONTROLS) @Test fun onSelectedDeviceStateChanged_withRemotePlaybackInfo_noMatchingRoutingSession_returnOtherDevice() { // GIVEN a notif is added - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() + loadMediaAndCaptureDeviceData() reset(listener) // AND MR2Manager returns null for routing session whenever(playbackInfo.playbackType).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) @@ -507,9 +548,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Test fun onDeviceListUpdate_withRemotePlaybackInfo_noMatchingRoutingSession_setsDisabledDevice() { // GIVEN a notif is added - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() + loadMediaAndCaptureDeviceData() reset(listener) // GIVEN that MR2Manager returns null for routing session whenever(playbackInfo.playbackType).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) @@ -529,9 +568,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Test fun onDeviceListUpdate_withRemotePlaybackInfo_noMatchingRoutingSession_returnsOtherDevice() { // GIVEN a notif is added - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() + loadMediaAndCaptureDeviceData() reset(listener) // GIVEN that MR2Manager returns null for routing session whenever(playbackInfo.playbackType).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) @@ -563,12 +600,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { whenever(selectedRoute.name).thenReturn(REMOTE_DEVICE_NAME) whenever(selectedRoute.type).thenReturn(MediaRoute2Info.TYPE_BUILTIN_SPEAKER) - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() - // Then the device name is the PhoneMediaDevice string - val data = captureDeviceData(KEY) + val data = loadMediaAndCaptureDeviceData() assertThat(data.name).isEqualTo(PhoneMediaDevice.getMediaTransferThisDeviceName(context)) } @@ -582,12 +615,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { whenever(selectedRoute.name).thenReturn(REMOTE_DEVICE_NAME) whenever(routingSession.isSystemSession).thenReturn(true) - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() - // Then the device name is the selected route name - val data = captureDeviceData(KEY) + val data = loadMediaAndCaptureDeviceData() assertThat(data.name).isEqualTo(REMOTE_DEVICE_NAME) } @@ -597,11 +626,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { whenever(routingSession.name).thenReturn(null) whenever(routingSession.isSystemSession).thenReturn(false) // WHEN a notification is added - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() // THEN the device is enabled and uses the current connected device name - val data = captureDeviceData(KEY) + val data = loadMediaAndCaptureDeviceData() assertThat(data.name).isEqualTo(DEVICE_NAME) assertThat(data.enabled).isTrue() } @@ -611,9 +637,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { whenever(playbackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_LOCAL) whenever(controller.getPlaybackInfo()).thenReturn(playbackInfo) // GIVEN a controller with local playback type - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() + loadMediaAndCaptureDeviceData() reset(mr2) // WHEN onAudioInfoChanged fires with remote playback type whenever(playbackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) @@ -630,9 +654,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { whenever(playbackInfo.getVolumeControlId()).thenReturn(null) whenever(controller.getPlaybackInfo()).thenReturn(playbackInfo) // GIVEN a controller with local playback type - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() + loadMediaAndCaptureDeviceData() reset(mr2) // WHEN onAudioInfoChanged fires with a volume control id change whenever(playbackInfo.getVolumeControlId()).thenReturn("placeholder id") @@ -649,9 +671,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { whenever(playbackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) whenever(controller.getPlaybackInfo()).thenReturn(playbackInfo) // GIVEN a controller with remote playback type - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() + loadMediaAndCaptureDeviceData() reset(mr2) // WHEN onAudioInfoChanged fires with remote playback type val captor = ArgumentCaptor.forClass(MediaController.Callback::class.java) @@ -665,9 +685,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { fun deviceIdChanged_informListener() { // GIVEN a notification is added, with a particular device connected whenever(device.id).thenReturn(DEVICE_ID) - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() + loadMediaAndCaptureDeviceData() // and later the manager gets a new device ID val deviceCallback = captureCallback() @@ -694,9 +712,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { // GIVEN a notification is added, with a particular device connected whenever(device.id).thenReturn(DEVICE_ID) whenever(device.name).thenReturn(DEVICE_NAME) - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() + loadMediaAndCaptureDeviceData() // and later the manager gets a new device name val deviceCallback = captureCallback() @@ -725,12 +741,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { whenever(device.name).thenReturn(DEVICE_NAME) val firstIcon = mock(Drawable::class.java) whenever(device.icon).thenReturn(firstIcon) - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() - val dataCaptor = ArgumentCaptor.forClass(MediaDeviceData::class.java) - verify(listener).onMediaDeviceChanged(eq(KEY), any(), dataCaptor.capture()) + loadMediaAndCaptureDeviceData() // and later the manager gets a callback with only the icon changed val deviceCallback = captureCallback() @@ -772,11 +784,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { setupBroadcastPackage(BROADCAST_APP_NAME) broadcastCallback.onBroadcastStarted(1, 1) - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() - - val data = captureDeviceData(KEY) + val data = loadMediaAndCaptureDeviceData() assertThat(data.showBroadcastButton).isFalse() assertThat(data.enabled).isTrue() assertThat(data.name).isEqualTo(DEVICE_NAME) @@ -791,11 +799,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { setupBroadcastPackage(BROADCAST_APP_NAME) broadcastCallback.onBroadcastStarted(1, 1) - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() - - val data = captureDeviceData(KEY) + val data = loadMediaAndCaptureDeviceData() assertThat(data.showBroadcastButton).isTrue() assertThat(data.enabled).isTrue() assertThat(data.name) @@ -811,11 +815,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { setupBroadcastPackage(NORMAL_APP_NAME) broadcastCallback.onBroadcastStarted(1, 1) - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() - - val data = captureDeviceData(KEY) + val data = loadMediaAndCaptureDeviceData() assertThat(data.showBroadcastButton).isTrue() assertThat(data.enabled).isTrue() assertThat(data.name).isEqualTo(BROADCAST_APP_NAME) @@ -829,11 +829,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { setupLeAudioConfiguration(false) broadcastCallback.onBroadcastStopped(1, 1) - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() - - val data = captureDeviceData(KEY) + val data = loadMediaAndCaptureDeviceData() assertThat(data.showBroadcastButton).isFalse() } @@ -846,11 +842,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { setupBroadcastPackage(BROADCAST_APP_NAME) broadcastCallback.onBroadcastStarted(1, 1) - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() - - val data = captureDeviceData(KEY) + val data = loadMediaAndCaptureDeviceData() assertThat(data.showBroadcastButton).isFalse() assertThat(data.enabled).isFalse() assertThat(data.name).isEqualTo(context.getString(R.string.audio_sharing_description)) @@ -858,18 +850,90 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Test @DisableFlags(Flags.FLAG_LEGACY_LE_AUDIO_SHARING) + @EnableFlags( + Flags.FLAG_ENABLE_LE_AUDIO_SHARING, + com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE + ) + fun onBroadcastStarted_currentMediaDeviceDataIsBroadcasting_drawablesReused() { + val broadcastCallback = setupBroadcastCallback() + setupLeAudioConfiguration(true) + setupBroadcastPackage(BROADCAST_APP_NAME) + broadcastCallback.onBroadcastStarted(1, 1) + + val firstDeviceData = loadMediaAndCaptureDeviceData() + reset(listener) + val secondDeviceData = loadMediaAndCaptureDeviceData() + + assertThat(firstDeviceData.icon).isEqualTo(secondDeviceData.icon) + } + + @Test + @DisableFlags( + Flags.FLAG_LEGACY_LE_AUDIO_SHARING, + com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE + ) @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) - fun onBroadcastStarted_currentMediaDeviceDataIsNotBroadcasting() { + fun onBroadcastStarted_currentMediaDeviceDataIsBroadcasting_drawablesNotReused() { + val broadcastCallback = setupBroadcastCallback() + setupLeAudioConfiguration(true) + setupBroadcastPackage(BROADCAST_APP_NAME) + broadcastCallback.onBroadcastStarted(1, 1) + + val firstDeviceData = loadMediaAndCaptureDeviceData() + reset(listener) + val secondDeviceData = loadMediaAndCaptureDeviceData() + + assertThat(firstDeviceData.icon).isNotEqualTo(secondDeviceData.icon) + } + + @Test + @EnableFlags( + Flags.FLAG_LEGACY_LE_AUDIO_SHARING, + com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE + ) + @DisableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + fun onBroadcastStarted_legacy_currentMediaDeviceDataIsNotBroadcasting_drawableReused() { val broadcastCallback = setupBroadcastCallback() setupLeAudioConfiguration(true) setupBroadcastPackage(NORMAL_APP_NAME) broadcastCallback.onBroadcastStarted(1, 1) - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() + val firstDeviceData = loadMediaAndCaptureDeviceData() + reset(listener) + val secondDeviceData = loadMediaAndCaptureDeviceData() - val data = captureDeviceData(KEY) + assertThat(firstDeviceData.icon).isEqualTo(secondDeviceData.icon) + } + + @Test + @EnableFlags(Flags.FLAG_LEGACY_LE_AUDIO_SHARING) + @DisableFlags( + Flags.FLAG_ENABLE_LE_AUDIO_SHARING, + com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE + ) + fun onBroadcastStarted_legacy_currentMediaDeviceDataIsNotBroadcasting_drawableNotReused() { + val broadcastCallback = setupBroadcastCallback() + setupLeAudioConfiguration(true) + setupBroadcastPackage(NORMAL_APP_NAME) + broadcastCallback.onBroadcastStarted(1, 1) + + val firstDeviceData = loadMediaAndCaptureDeviceData() + reset(listener) + val secondDeviceData = loadMediaAndCaptureDeviceData() + + assertThat(firstDeviceData.icon).isNotEqualTo(secondDeviceData.icon) + } + + @Test + @DisableFlags(Flags.FLAG_LEGACY_LE_AUDIO_SHARING) + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + fun onBroadcastStarted_currentMediaDeviceDataIsNotBroadcasting() { + val broadcastCallback = setupBroadcastCallback() + setupLeAudioConfiguration(true) + setupBroadcastPackage(NORMAL_APP_NAME) + broadcastCallback.onBroadcastStarted(1, 1) + + val data = loadMediaAndCaptureDeviceData() assertThat(data.showBroadcastButton).isFalse() assertThat(data.enabled).isFalse() assertThat(data.name).isEqualTo(context.getString(R.string.audio_sharing_description)) @@ -883,11 +947,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { setupLeAudioConfiguration(false) broadcastCallback.onBroadcastStopped(1, 1) - manager.onMediaDataLoaded(KEY, null, mediaData) - fakeBgExecutor.runAllReady() - fakeFgExecutor.runAllReady() - - val data = captureDeviceData(KEY) + val data = loadMediaAndCaptureDeviceData() assertThat(data.showBroadcastButton).isFalse() assertThat(data.name?.equals(context.getString(R.string.audio_sharing_description))) .isFalse() @@ -903,13 +963,21 @@ public class MediaDeviceManagerTest : SysuiTestCase() { val callback: BluetoothLeBroadcast.Callback = object : BluetoothLeBroadcast.Callback { override fun onBroadcastStarted(reason: Int, broadcastId: Int) {} + override fun onBroadcastStartFailed(reason: Int) {} + override fun onBroadcastStopped(reason: Int, broadcastId: Int) {} + override fun onBroadcastStopFailed(reason: Int) {} + override fun onPlaybackStarted(reason: Int, broadcastId: Int) {} + override fun onPlaybackStopped(reason: Int, broadcastId: Int) {} + override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {} + override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {} + override fun onBroadcastMetadataChanged( broadcastId: Int, metadata: BluetoothLeBroadcastMetadata @@ -941,4 +1009,12 @@ public class MediaDeviceManagerTest : SysuiTestCase() { verify(listener).onMediaDeviceChanged(eq(key), eq(oldKey), captor.capture()) return captor.getValue() } + + private fun loadMediaAndCaptureDeviceData(): MediaDeviceData { + manager.onMediaDataLoaded(KEY, null, mediaData) + fakeBgExecutor.runAllReady() + fakeFgExecutor.runAllReady() + + return captureDeviceData(KEY) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt index f8358c51ed5c..850916be35bf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt @@ -186,7 +186,6 @@ class MediaCarouselControllerTest : SysuiTestCase() { sceneInteractor = kosmos.sceneInteractor, ) verify(configurationController).addCallback(capture(configListener)) - verify(mediaDataManager).addListener(capture(listener)) verify(visualStabilityProvider) .addPersistentReorderingAllowedListener(capture(visualStabilityCallback)) verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCallback)) @@ -405,8 +404,11 @@ class MediaCarouselControllerTest : SysuiTestCase() { assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec) } + @DisableSceneContainer @Test fun testOrderWithSmartspace_prioritized_updatingVisibleMediaPlayers() { + verify(mediaDataManager).addListener(capture(listener)) + testPlayerOrdering() // If smartspace is prioritized @@ -439,8 +441,11 @@ class MediaCarouselControllerTest : SysuiTestCase() { assertTrue(MediaPlayerData.playerKeys().elementAt(idx).isSsMediaRec) } + @DisableSceneContainer @Test fun testPlayingExistingMediaPlayerFromCarousel_visibleMediaPlayersNotUpdated() { + verify(mediaDataManager).addListener(capture(listener)) + testPlayerOrdering() // playing paused player listener.value.onMediaDataLoaded( @@ -547,8 +552,11 @@ class MediaCarouselControllerTest : SysuiTestCase() { verify(logger).logRecommendationRemoved(eq(packageName), eq(instanceId!!)) } + @DisableSceneContainer @Test fun testMediaLoaded_ScrollToActivePlayer() { + verify(mediaDataManager).addListener(capture(listener)) + listener.value.onMediaDataLoaded( PLAYING_LOCAL, null, @@ -604,8 +612,11 @@ class MediaCarouselControllerTest : SysuiTestCase() { ) } + @DisableSceneContainer @Test fun testMediaLoadedFromRecommendationCard_ScrollToActivePlayer() { + verify(mediaDataManager).addListener(capture(listener)) + listener.value.onSmartspaceMediaDataLoaded( SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true), @@ -647,8 +658,11 @@ class MediaCarouselControllerTest : SysuiTestCase() { assertEquals(playerIndex, 0) } + @DisableSceneContainer @Test fun testRecommendationRemovedWhileNotVisible_updateHostVisibility() { + verify(mediaDataManager).addListener(capture(listener)) + var result = false mediaCarouselController.updateHostVisibility = { result = true } @@ -658,8 +672,11 @@ class MediaCarouselControllerTest : SysuiTestCase() { assertEquals(true, result) } + @DisableSceneContainer @Test fun testRecommendationRemovedWhileVisible_thenReorders_updateHostVisibility() { + verify(mediaDataManager).addListener(capture(listener)) + var result = false mediaCarouselController.updateHostVisibility = { result = true } @@ -788,8 +805,11 @@ class MediaCarouselControllerTest : SysuiTestCase() { verify(pageIndicator, times(4)).setNumPages(any()) } + @DisableSceneContainer @Test fun testRecommendation_persistentEnabled_newSmartspaceLoaded_updatesSort() { + verify(mediaDataManager).addListener(capture(listener)) + testRecommendation_persistentEnabled_inactiveSmartspaceDataLoaded_isAdded() // When an update to existing smartspace data is loaded @@ -804,8 +824,11 @@ class MediaCarouselControllerTest : SysuiTestCase() { assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(0).data.active) } + @DisableSceneContainer @Test fun testRecommendation_persistentEnabled_inactiveSmartspaceDataLoaded_isAdded() { + verify(mediaDataManager).addListener(capture(listener)) + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) // When inactive smartspace data is loaded @@ -1023,11 +1046,13 @@ class MediaCarouselControllerTest : SysuiTestCase() { verify(panel).updateAnimatorDurationScale() } + @DisableSceneContainer @Test fun swipeToDismiss_pausedAndResumeOff_userInitiated() { + verify(mediaDataManager).addListener(capture(listener)) + // When resumption is disabled, paused media should be dismissed after being swiped away Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0) - val pausedMedia = DATA.copy(isPlaying = false) listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, pausedMedia) mediaCarouselController.onSwipeToDismiss() @@ -1042,8 +1067,11 @@ class MediaCarouselControllerTest : SysuiTestCase() { verify(mediaDataManager).dismissMediaData(eq(PAUSED_LOCAL), anyLong(), eq(true)) } + @DisableSceneContainer @Test fun swipeToDismiss_pausedAndResumeOff_delayed_userInitiated() { + verify(mediaDataManager).addListener(capture(listener)) + // When resumption is disabled, paused media should be dismissed after being swiped away Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0) mediaCarouselController.updateHostVisibility = {} @@ -1068,6 +1096,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { * @param function called when a certain configuration change occurs. */ private fun testConfigurationChange(function: () -> Unit) { + verify(mediaDataManager).addListener(capture(listener)) mediaCarouselController.pageIndicator = pageIndicator listener.value.onMediaDataLoaded( PLAYING_LOCAL, diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt index 521aa5a7352b..1260a65b9c1c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt @@ -70,6 +70,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.bluetooth.BroadcastDialogController import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.media.controls.MediaTestUtils import com.android.systemui.media.controls.domain.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA import com.android.systemui.media.controls.domain.pipeline.MediaDataManager @@ -84,7 +85,6 @@ import com.android.systemui.media.controls.ui.view.GutsViewHolder import com.android.systemui.media.controls.ui.view.MediaViewHolder import com.android.systemui.media.controls.ui.view.RecommendationViewHolder import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel -import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.media.dialog.MediaOutputDialogManager import com.android.systemui.monet.ColorScheme @@ -141,6 +141,7 @@ private const val APP_NAME = "APP_NAME" @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) +@DisableSceneContainer public class MediaControlPanelTest : SysuiTestCase() { @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() @@ -233,9 +234,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Mock private lateinit var recProgressBar1: SeekBar @Mock private lateinit var recProgressBar2: SeekBar @Mock private lateinit var recProgressBar3: SeekBar - private var shouldShowBroadcastButton: Boolean = false @Mock private lateinit var globalSettings: GlobalSettings - @Mock private lateinit var mediaFlags: MediaFlags @JvmField @Rule val mockito = MockitoJUnit.rule() @@ -254,7 +253,6 @@ public class MediaControlPanelTest : SysuiTestCase() { .thenReturn(applicationInfo) whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE) context.setMockPackageManager(packageManager) - whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(false) player = object : @@ -278,7 +276,6 @@ public class MediaControlPanelTest : SysuiTestCase() { lockscreenUserManager, broadcastDialogController, globalSettings, - mediaFlags, ) { override fun loadAnimator( animId: Int, diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt index 6c350cb4a5b0..2370bca52951 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt @@ -30,6 +30,7 @@ import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.ui.viewmodel.communalTransitionViewModel import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq import com.android.systemui.dreams.DreamOverlayStateController +import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository @@ -40,7 +41,6 @@ import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.media.controls.ui.view.MediaCarouselScrollHandler import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.media.controls.ui.view.MediaHostState -import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.media.dream.MediaDreamComplication import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R @@ -85,6 +85,7 @@ import org.mockito.kotlin.anyOrNull @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) +@DisableSceneContainer class MediaHierarchyManagerTest : SysuiTestCase() { private val kosmos = testKosmos() @@ -105,7 +106,6 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Mock private lateinit var dreamOverlayStateController: DreamOverlayStateController @Mock private lateinit var shadeInteractor: ShadeInteractor @Mock lateinit var logger: MediaViewLogger - @Mock private lateinit var mediaFlags: MediaFlags @Captor private lateinit var wakefullnessObserver: ArgumentCaptor<(WakefulnessLifecycle.Observer)> @Captor @@ -139,7 +139,6 @@ class MediaHierarchyManagerTest : SysuiTestCase() { shadeExpansion = MutableStateFlow(0f) whenever(shadeInteractor.isQsBypassingShade).thenReturn(isQsBypassingShade) whenever(shadeInteractor.shadeExpansion).thenReturn(shadeExpansion) - whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(false) mediaHierarchyManager = MediaHierarchyManager( context, @@ -160,7 +159,6 @@ class MediaHierarchyManagerTest : SysuiTestCase() { testScope.backgroundScope, ResourcesSplitShadeStateController(), logger, - mediaFlags, ) verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture()) verify(statusBarStateController).addCallback(statusBarCallback.capture()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt index 00b9a46f340a..e765b6f77155 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt @@ -38,12 +38,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.widget.CachingIconView import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.media.controls.ui.view.GutsViewHolder import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.media.controls.ui.view.MediaViewHolder import com.android.systemui.media.controls.ui.view.RecommendationViewHolder import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel -import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.res.R import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffectView import com.android.systemui.surfaceeffects.ripple.MultiRippleView @@ -113,7 +113,6 @@ class MediaViewControllerTest : SysuiTestCase() { @Mock private lateinit var mediaTitleWidgetState: WidgetState @Mock private lateinit var mediaSubTitleWidgetState: WidgetState @Mock private lateinit var mediaContainerWidgetState: WidgetState - @Mock private lateinit var mediaFlags: MediaFlags @Mock private lateinit var seekBarViewModel: SeekBarViewModel @Mock private lateinit var seekBarData: LiveData<SeekBarViewModel.Progress> @Mock private lateinit var globalSettings: GlobalSettings @@ -140,7 +139,6 @@ class MediaViewControllerTest : SysuiTestCase() { logger, seekBarViewModel, mainExecutor, - mediaFlags, globalSettings, ) { override fun loadAnimator( @@ -374,10 +372,9 @@ class MediaViewControllerTest : SysuiTestCase() { verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } } + @EnableSceneContainer @Test fun attachPlayer_seekBarDisabled_seekBarVisibilityIsSetToInvisible() { - whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) - mediaViewController.attachPlayer(viewHolder) getEnabledChangeListener().onEnabledChanged(enabled = true) getEnabledChangeListener().onEnabledChanged(enabled = false) @@ -386,10 +383,9 @@ class MediaViewControllerTest : SysuiTestCase() { .isEqualTo(ConstraintSet.INVISIBLE) } + @EnableSceneContainer @Test fun attachPlayer_seekBarEnabled_seekBarVisible() { - whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) - mediaViewController.attachPlayer(viewHolder) getEnabledChangeListener().onEnabledChanged(enabled = true) @@ -397,10 +393,9 @@ class MediaViewControllerTest : SysuiTestCase() { .isEqualTo(ConstraintSet.VISIBLE) } + @EnableSceneContainer @Test fun attachPlayer_seekBarStatusUpdate_seekBarVisibilityChanges() { - whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) - mediaViewController.attachPlayer(viewHolder) getEnabledChangeListener().onEnabledChanged(enabled = true) @@ -413,10 +408,9 @@ class MediaViewControllerTest : SysuiTestCase() { .isEqualTo(ConstraintSet.INVISIBLE) } + @EnableSceneContainer @Test fun attachPlayer_notScrubbing_scrubbingViewsGone() { - whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) - mediaViewController.attachPlayer(viewHolder) mediaViewController.canShowScrubbingTime = true getScrubbingChangeListener().onScrubbingChanged(true) @@ -433,10 +427,9 @@ class MediaViewControllerTest : SysuiTestCase() { .isEqualTo(ConstraintSet.GONE) } + @EnableSceneContainer @Test fun setIsScrubbing_noSemanticActions_scrubbingViewsGone() { - whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) - mediaViewController.attachPlayer(viewHolder) mediaViewController.canShowScrubbingTime = false getScrubbingChangeListener().onScrubbingChanged(true) @@ -452,10 +445,9 @@ class MediaViewControllerTest : SysuiTestCase() { .isEqualTo(ConstraintSet.GONE) } + @EnableSceneContainer @Test fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() { - whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) - mediaViewController.attachPlayer(viewHolder) mediaViewController.setUpNextButtonInfo(true) mediaViewController.setUpPrevButtonInfo(false) @@ -474,10 +466,9 @@ class MediaViewControllerTest : SysuiTestCase() { .isEqualTo(ConstraintSet.GONE) } + @EnableSceneContainer @Test fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() { - whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) - mediaViewController.attachPlayer(viewHolder) mediaViewController.setUpNextButtonInfo(false) mediaViewController.setUpPrevButtonInfo(true) @@ -496,10 +487,9 @@ class MediaViewControllerTest : SysuiTestCase() { .isEqualTo(ConstraintSet.GONE) } + @EnableSceneContainer @Test fun setIsScrubbing_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() { - whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) - mediaViewController.attachPlayer(viewHolder) mediaViewController.setUpNextButtonInfo(true) mediaViewController.setUpPrevButtonInfo(true) @@ -522,10 +512,9 @@ class MediaViewControllerTest : SysuiTestCase() { .isEqualTo(ConstraintSet.VISIBLE) } + @EnableSceneContainer @Test fun setIsScrubbing_trueThenFalse_reservePrevAndNextButtons() { - whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true) - mediaViewController.attachPlayer(viewHolder) mediaViewController.setUpNextButtonInfo(true, ConstraintSet.INVISIBLE) mediaViewController.setUpPrevButtonInfo(true, ConstraintSet.INVISIBLE) diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt index f531a3fdd8f0..3e3aa4f079f7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt @@ -16,9 +16,9 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.wm.shell.recents.RecentTasks +import com.android.wm.shell.shared.GroupedRecentTaskInfo +import com.android.wm.shell.shared.split.SplitBounds import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50 -import com.android.wm.shell.util.GroupedRecentTaskInfo -import com.android.wm.shell.util.SplitBounds import com.google.common.truth.Truth.assertThat import java.util.Optional import java.util.function.Consumer diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt index 8fbd3c8b7ebf..69b7b2bfcf8c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt @@ -29,8 +29,8 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler import com.android.systemui.mediaprojection.appselector.data.RecentTask import com.android.systemui.util.mockito.mock +import com.android.wm.shell.shared.split.SplitBounds import com.android.wm.shell.splitscreen.SplitScreen -import com.android.wm.shell.util.SplitBounds import com.google.common.truth.Expect import com.google.common.truth.Truth.assertThat import java.util.Optional diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java index bfbb7ceee6b5..a770ee199ba6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java @@ -18,7 +18,9 @@ package com.android.systemui.navigationbar; import static android.app.StatusBarManager.WINDOW_NAVIGATION_BAR; import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU; +import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE; import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE; @@ -37,6 +39,9 @@ import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.res.Configuration; import android.os.Handler; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.provider.Flags; import android.view.IWindowManager; import android.view.accessibility.AccessibilityManager; @@ -47,6 +52,7 @@ import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutT import com.android.systemui.SysuiTestCase; import com.android.systemui.accessibility.AccessibilityButtonModeObserver; import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver; +import com.android.systemui.accessibility.AccessibilityGestureTargetsObserver; import com.android.systemui.accessibility.SystemActions; import com.android.systemui.assist.AssistManager; import com.android.systemui.dump.DumpManager; @@ -94,6 +100,8 @@ public class NavBarHelperTest extends SysuiTestCase { @Mock AccessibilityButtonTargetsObserver mAccessibilityButtonTargetObserver; @Mock + AccessibilityGestureTargetsObserver mAccessibilityGestureTargetObserver; + @Mock SystemActions mSystemActions; @Mock OverviewProxyService mOverviewProxyService; @@ -152,6 +160,7 @@ public class NavBarHelperTest extends SysuiTestCase { mAccessibilityManager).addAccessibilityServicesStateChangeListener(any()); mNavBarHelper = new NavBarHelper(mContext, mAccessibilityManager, mAccessibilityButtonModeObserver, mAccessibilityButtonTargetObserver, + mAccessibilityGestureTargetObserver, mSystemActions, mOverviewProxyService, mAssistManagerLazy, () -> Optional.of(mock(CentralSurfaces.class)), mock(KeyguardStateController.class), mNavigationModeController, mEdgeBackGestureHandlerFactory, mWm, mUserTracker, @@ -171,6 +180,7 @@ public class NavBarHelperTest extends SysuiTestCase { mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater); verify(mAccessibilityButtonModeObserver, times(1)).addListener(mNavBarHelper); verify(mAccessibilityButtonTargetObserver, times(1)).addListener(mNavBarHelper); + verify(mAccessibilityGestureTargetObserver, times(1)).addListener(mNavBarHelper); verify(mAccessibilityManager, times(1)).addAccessibilityServicesStateChangeListener( mNavBarHelper); verify(mAssistManager, times(1)).getAssistInfoForUser(anyInt()); @@ -185,6 +195,7 @@ public class NavBarHelperTest extends SysuiTestCase { mNavBarHelper.removeNavTaskStateUpdater(mNavbarTaskbarStateUpdater); verify(mAccessibilityButtonModeObserver, times(1)).removeListener(mNavBarHelper); verify(mAccessibilityButtonTargetObserver, times(1)).removeListener(mNavBarHelper); + verify(mAccessibilityGestureTargetObserver, times(1)).removeListener(mNavBarHelper); verify(mAccessibilityManager, times(1)).removeAccessibilityServicesStateChangeListener( mNavBarHelper); verify(mWm, times(1)).removeRotationWatcher(any()); @@ -353,6 +364,83 @@ public class NavBarHelperTest extends SysuiTestCase { verify(mEdgeBackGestureHandler, times(1)).onConfigurationChanged(any()); } + @Test + public void updateA11yState_navBarMode_softwareTargets_isClickable() { + when(mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode()).thenReturn( + ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR); + when(mAccessibilityManager.getAccessibilityShortcutTargets(UserShortcutType.SOFTWARE)) + .thenReturn(createFakeShortcutTargets()); + + mNavBarHelper.updateA11yState(); + long state = mNavBarHelper.getA11yButtonState(); + assertThat(state & SYSUI_STATE_A11Y_BUTTON_CLICKABLE).isEqualTo( + SYSUI_STATE_A11Y_BUTTON_CLICKABLE); + assertThat(state & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE).isEqualTo( + SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE); + } + + @Test + @DisableFlags(Flags.FLAG_A11Y_STANDALONE_GESTURE_ENABLED) + public void updateA11yState_gestureMode_softwareTargets_isClickable() { + when(mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode()).thenReturn( + ACCESSIBILITY_BUTTON_MODE_GESTURE); + when(mAccessibilityManager.getAccessibilityShortcutTargets(UserShortcutType.SOFTWARE)) + .thenReturn(createFakeShortcutTargets()); + + mNavBarHelper.updateA11yState(); + long state = mNavBarHelper.getA11yButtonState(); + assertThat(state & SYSUI_STATE_A11Y_BUTTON_CLICKABLE).isEqualTo( + SYSUI_STATE_A11Y_BUTTON_CLICKABLE); + assertThat(state & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE).isEqualTo( + SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE); + } + + @Test + @EnableFlags(Flags.FLAG_A11Y_STANDALONE_GESTURE_ENABLED) + public void updateA11yState_gestureNavMode_floatingButtonMode_gestureTargets_isClickable() { + mNavBarHelper.onNavigationModeChanged(NAV_BAR_MODE_GESTURAL); + when(mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode()).thenReturn( + ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU); + when(mAccessibilityManager.getAccessibilityShortcutTargets(UserShortcutType.GESTURE)) + .thenReturn(createFakeShortcutTargets()); + + mNavBarHelper.updateA11yState(); + long state = mNavBarHelper.getA11yButtonState(); + assertThat(state & SYSUI_STATE_A11Y_BUTTON_CLICKABLE).isEqualTo( + SYSUI_STATE_A11Y_BUTTON_CLICKABLE); + assertThat(state & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE).isEqualTo( + SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE); + } + + @Test + @EnableFlags(Flags.FLAG_A11Y_STANDALONE_GESTURE_ENABLED) + public void updateA11yState_navBarMode_gestureTargets_isNotClickable() { + when(mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode()).thenReturn( + ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR); + when(mAccessibilityManager.getAccessibilityShortcutTargets(UserShortcutType.GESTURE)) + .thenReturn(createFakeShortcutTargets()); + + mNavBarHelper.updateA11yState(); + long state = mNavBarHelper.getA11yButtonState(); + assertThat(state & SYSUI_STATE_A11Y_BUTTON_CLICKABLE).isEqualTo(0); + assertThat(state & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE).isEqualTo(0); + } + + @Test + @EnableFlags(Flags.FLAG_A11Y_STANDALONE_GESTURE_ENABLED) + public void updateA11yState_singleTarget_clickableButNotLongClickable() { + when(mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode()).thenReturn( + ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR); + when(mAccessibilityManager.getAccessibilityShortcutTargets(UserShortcutType.SOFTWARE)) + .thenReturn(new ArrayList<>(List.of("a"))); + + mNavBarHelper.updateA11yState(); + long state = mNavBarHelper.getA11yButtonState(); + assertThat(state & SYSUI_STATE_A11Y_BUTTON_CLICKABLE).isEqualTo( + SYSUI_STATE_A11Y_BUTTON_CLICKABLE); + assertThat(state & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE).isEqualTo(0); + } + private List<String> createFakeShortcutTargets() { return new ArrayList<>(List.of("a", "b", "c", "d")); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java index 04d140c458e8..2905a7329d21 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java @@ -85,6 +85,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestableContext; import com.android.systemui.accessibility.AccessibilityButtonModeObserver; import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver; +import com.android.systemui.accessibility.AccessibilityGestureTargetsObserver; import com.android.systemui.accessibility.SystemActions; import com.android.systemui.assist.AssistManager; import com.android.systemui.dump.DumpManager; @@ -287,6 +288,7 @@ public class NavigationBarTest extends SysuiTestCase { mNavBarHelper = spy(new NavBarHelper(mContext, mock(AccessibilityManager.class), mock(AccessibilityButtonModeObserver.class), mock(AccessibilityButtonTargetsObserver.class), + mock(AccessibilityGestureTargetsObserver.class), mSystemActions, mOverviewProxyService, () -> mock(AssistManager.class), () -> Optional.of(mCentralSurfaces), mKeyguardStateController, mock(NavigationModeController.class), diff --git a/packages/SystemUI/multivalentTests/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 d9faa30cb072..d9faa30cb072 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java index ebab04989590..748c7d9d939b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java @@ -497,6 +497,17 @@ public class QSTileImplTest extends SysuiTestCase { assertThat(mTile.mRefreshes).isEqualTo(1); } + @Test + public void testStaleTriggeredOnUserSwitch() { + mTile.clearRefreshes(); + + mTile.userSwitch(10); + mTestableLooper.processAllMessages(); + + assertFalse(mTile.isListening()); + assertThat(mTile.mRefreshes).isEqualTo(1); + } + private void assertEvent(UiEventLogger.UiEventEnum eventType, UiEventLoggerFake.FakeUiEvent fakeEvent) { assertEquals(eventType.getId(), fakeEvent.eventId); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index b67e111af13d..5a5cdcd99054 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -33,7 +33,6 @@ import androidx.lifecycle.LifecycleOwner import androidx.test.filters.SmallTest import com.android.compose.animation.scene.SceneKey import com.android.systemui.Flags -import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_BACK_GESTURE import com.android.systemui.Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX import com.android.systemui.SysuiTestCase import com.android.systemui.ambient.touch.TouchHandler @@ -58,6 +57,7 @@ import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.controller.keyguardMediaController import com.android.systemui.res.R import com.android.systemui.scene.shared.model.sceneDataSourceDelegator @@ -140,7 +140,8 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { kosmos.sceneDataSourceDelegator, kosmos.notificationStackScrollLayoutController, kosmos.keyguardMediaController, - kosmos.lockscreenSmartspaceController + kosmos.lockscreenSmartspaceController, + logcatLogBuffer("GlanceableHubContainerControllerTest") ) } testableLooper = TestableLooper.get(this) @@ -186,7 +187,8 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { kosmos.sceneDataSourceDelegator, kosmos.notificationStackScrollLayoutController, kosmos.keyguardMediaController, - kosmos.lockscreenSmartspaceController + kosmos.lockscreenSmartspaceController, + logcatLogBuffer("GlanceableHubContainerControllerTest") ) // First call succeeds. @@ -214,7 +216,8 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { kosmos.sceneDataSourceDelegator, kosmos.notificationStackScrollLayoutController, kosmos.keyguardMediaController, - kosmos.lockscreenSmartspaceController + kosmos.lockscreenSmartspaceController, + logcatLogBuffer("GlanceableHubContainerControllerTest") ) assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED) @@ -237,7 +240,8 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { kosmos.sceneDataSourceDelegator, kosmos.notificationStackScrollLayoutController, kosmos.keyguardMediaController, - kosmos.lockscreenSmartspaceController + kosmos.lockscreenSmartspaceController, + logcatLogBuffer("GlanceableHubContainerControllerTest") ) // Only initView without attaching a view as we don't want the flows to start collecting @@ -437,7 +441,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_GLANCEABLE_HUB_BACK_GESTURE, FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) + @DisableFlags(FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) fun gestureExclusionZone_setAfterInit() = with(kosmos) { testScope.runTest { @@ -463,7 +467,6 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_GLANCEABLE_HUB_BACK_GESTURE) @EnableFlags(FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) fun gestureExclusionZone_setAfterInit_fullSwipe() = with(kosmos) { @@ -484,7 +487,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_GLANCEABLE_HUB_BACK_GESTURE, FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) + @DisableFlags(FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) fun gestureExclusionZone_setAfterInit_rtl() = with(kosmos) { testScope.runTest { @@ -509,7 +512,6 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } - @DisableFlags(FLAG_GLANCEABLE_HUB_BACK_GESTURE) @EnableFlags(FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) fun gestureExclusionZone_setAfterInit_rtl_fullSwipe() = with(kosmos) { @@ -530,102 +532,6 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_GLANCEABLE_HUB_BACK_GESTURE) - @DisableFlags(FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) - fun gestureExclusionZone_setAfterInit_backGestureEnabled() = - with(kosmos) { - testScope.runTest { - whenever(containerView.layoutDirection).thenReturn(View.LAYOUT_DIRECTION_LTR) - goToScene(CommunalScenes.Communal) - - assertThat(containerView.systemGestureExclusionRects) - .containsExactly( - Rect( - /* left= */ FAKE_INSETS.left, - /* top= */ TOP_SWIPE_REGION_WIDTH, - /* right= */ CONTAINER_WIDTH - FAKE_INSETS.right, - /* bottom= */ CONTAINER_HEIGHT - BOTTOM_SWIPE_REGION_WIDTH - ), - Rect( - /* left= */ 0, - /* top= */ 0, - /* right= */ FAKE_INSETS.right, - /* bottom= */ CONTAINER_HEIGHT - ) - ) - } - } - - @Test - @EnableFlags(FLAG_GLANCEABLE_HUB_BACK_GESTURE, FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) - fun gestureExclusionZone_setAfterInit_backGestureEnabled_fullSwipe() = - with(kosmos) { - testScope.runTest { - whenever(containerView.layoutDirection).thenReturn(View.LAYOUT_DIRECTION_LTR) - goToScene(CommunalScenes.Communal) - - assertThat(containerView.systemGestureExclusionRects) - .containsExactly( - Rect( - /* left= */ 0, - /* top= */ 0, - /* right= */ FAKE_INSETS.right, - /* bottom= */ CONTAINER_HEIGHT - ) - ) - } - } - - @Test - @EnableFlags(FLAG_GLANCEABLE_HUB_BACK_GESTURE) - @DisableFlags(FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) - fun gestureExclusionZone_setAfterInit_backGestureEnabled_rtl() = - with(kosmos) { - testScope.runTest { - whenever(containerView.layoutDirection).thenReturn(View.LAYOUT_DIRECTION_RTL) - goToScene(CommunalScenes.Communal) - - assertThat(containerView.systemGestureExclusionRects) - .containsExactly( - Rect( - /* left= */ FAKE_INSETS.left, - /* top= */ TOP_SWIPE_REGION_WIDTH, - /* right= */ CONTAINER_WIDTH - FAKE_INSETS.right, - /* bottom= */ CONTAINER_HEIGHT - BOTTOM_SWIPE_REGION_WIDTH - ), - Rect( - /* left= */ FAKE_INSETS.left, - /* top= */ 0, - /* right= */ CONTAINER_WIDTH, - /* bottom= */ CONTAINER_HEIGHT - ) - ) - } - } - - @Test - @EnableFlags(FLAG_GLANCEABLE_HUB_BACK_GESTURE, FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) - fun gestureExclusionZone_setAfterInit_backGestureEnabled_rtl_fullSwipe() = - with(kosmos) { - testScope.runTest { - whenever(containerView.layoutDirection).thenReturn(View.LAYOUT_DIRECTION_RTL) - goToScene(CommunalScenes.Communal) - - assertThat(containerView.systemGestureExclusionRects) - .containsExactly( - Rect( - Rect( - /* left= */ FAKE_INSETS.left, - /* top= */ 0, - /* right= */ CONTAINER_WIDTH, - /* bottom= */ CONTAINER_HEIGHT - ) - ) - ) - } - } - - @Test fun gestureExclusionZone_unsetWhenShadeOpen() = with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java index 505f7997ef1c..3f6617b32131 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java @@ -197,6 +197,7 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { () -> sceneInteractor, () -> mKosmos.getFromGoneTransitionInteractor(), () -> mKosmos.getFromLockscreenTransitionInteractor(), + () -> mKosmos.getFromOccludedTransitionInteractor(), () -> mKosmos.getSharedNotificationContainerInteractor(), mTestScope); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt index fadb1d7c91a1..fadb1d7c91a1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java index 50131cb06631..a0d231b8bb6f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java @@ -32,6 +32,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; +import android.app.Flags; import android.app.Notification; import android.content.Context; import android.content.ContextWrapper; @@ -41,9 +42,13 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Icon; +import android.graphics.drawable.ShapeDrawable; import android.os.Bundle; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.service.notification.StatusBarNotification; import android.view.ViewGroup; @@ -191,6 +196,34 @@ public class StatusBarIconViewTest extends SysuiTestCase { } @Test + @EnableFlags({Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS}) + public void setIcon_withPreloaded_usesPreloaded() { + Icon mockIcon = mock(Icon.class); + when(mockIcon.loadDrawableAsUser(any(), anyInt())).thenReturn(new ColorDrawable(1)); + mStatusBarIcon.icon = mockIcon; + mStatusBarIcon.preloadedIcon = new ShapeDrawable(); + + mIconView.set(mStatusBarIcon); + + assertThat(mIconView.getDrawable()).isNotNull(); + assertThat(mIconView.getDrawable()).isInstanceOf(ShapeDrawable.class); + } + + @Test + @DisableFlags({Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS}) + public void setIcon_withPreloadedButFlagDisabled_ignoresPreloaded() { + Icon mockIcon = mock(Icon.class); + when(mockIcon.loadDrawableAsUser(any(), anyInt())).thenReturn(new ColorDrawable(1)); + mStatusBarIcon.icon = mockIcon; + mStatusBarIcon.preloadedIcon = new ShapeDrawable(); + + mIconView.set(mStatusBarIcon); + + assertThat(mIconView.getDrawable()).isNotNull(); + assertThat(mIconView.getDrawable()).isInstanceOf(ColorDrawable.class); + } + + @Test public void testUpdateIconScale_constrainedDrawableSizeLessThanDpIconSize() { int dpIconSize = 60; int dpDrawingSize = 30; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt index ad6aca1dcd4f..3c583f26b0df 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.statusbar.notification.collection.render.NotifStats import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController @@ -45,8 +46,8 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.Mockito.verifyZeroInteractions -import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations.initMocks +import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -66,6 +67,7 @@ class StackCoordinatorTest : SysuiTestCase() { SensitiveNotificationProtectionController @Mock private lateinit var stackController: NotifStackController @Mock private lateinit var section: NotifSection + @Mock private lateinit var row: ExpandableNotificationRow @Before fun setUp() { @@ -74,6 +76,8 @@ class StackCoordinatorTest : SysuiTestCase() { whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(false) entry = NotificationEntryBuilder().setSection(section).build() + entry.row = row + entry.setSensitive(false, false) coordinator = StackCoordinator( groupExpansionManagerImpl, @@ -189,4 +193,17 @@ class StackCoordinatorTest : SysuiTestCase() { .setNotifStats(NotifStats(1, false, false, true, false)) verifyZeroInteractions(stackController) } + + @Test + @EnableFlags( + FooterViewRefactor.FLAG_NAME + ) + fun testSetNotificationStats_footerFlagOn_nonClearableRedacted() { + entry.setSensitive(true, true) + whenever(section.bucket).thenReturn(BUCKET_ALERTING) + afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + verify(activeNotificationsInteractor) + .setNotifStats(NotifStats(1, hasNonClearableAlertingNotifs = true, false, false, false)) + verifyZeroInteractions(stackController) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt index d1b1f466ef7a..ed99705b194e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt @@ -98,16 +98,20 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro // instead of VisualInterruptionDecisionProviderTestBase // because avalanche code is based on the suppression refactor. + private fun getAvalancheSuppressor() : AvalancheSuppressor { + return AvalancheSuppressor( + avalancheProvider, systemClock, settingsInteractor, packageManager, + uiEventLogger, context, notificationManager, logger + ) + } + @Test fun testAvalancheFilter_suppress_hasNotSeenEdu_showEduHun() { setAllowedEmergencyPkg(false) whenever(avalancheProvider.timeoutMs).thenReturn(20) whenever(avalancheProvider.startTime).thenReturn(whenAgo(10)) - val avalancheSuppressor = AvalancheSuppressor( - avalancheProvider, systemClock, settingsInteractor, packageManager, - uiEventLogger, context, notificationManager - ) + val avalancheSuppressor = getAvalancheSuppressor() avalancheSuppressor.hasSeenEdu = false withFilter(avalancheSuppressor) { @@ -128,10 +132,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro whenever(avalancheProvider.timeoutMs).thenReturn(20) whenever(avalancheProvider.startTime).thenReturn(whenAgo(10)) - val avalancheSuppressor = AvalancheSuppressor( - avalancheProvider, systemClock, settingsInteractor, packageManager, - uiEventLogger, context, notificationManager - ) + val avalancheSuppressor = getAvalancheSuppressor() avalancheSuppressor.hasSeenEdu = true withFilter(avalancheSuppressor) { @@ -151,8 +152,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager, - uiEventLogger, context, notificationManager) + getAvalancheSuppressor() ) { ensurePeekState() assertShouldHeadsUp( @@ -171,8 +171,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager, - uiEventLogger, context, notificationManager) + getAvalancheSuppressor() ) { ensurePeekState() assertShouldNotHeadsUp( @@ -191,8 +190,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager, - uiEventLogger, context, notificationManager) + getAvalancheSuppressor() ) { ensurePeekState() assertShouldHeadsUp( @@ -209,8 +207,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager, - uiEventLogger, context, notificationManager) + getAvalancheSuppressor() ) { ensurePeekState() assertShouldHeadsUp( @@ -227,8 +224,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager, - uiEventLogger, context, notificationManager) + getAvalancheSuppressor() ) { ensurePeekState() assertShouldHeadsUp( @@ -245,8 +241,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager, - uiEventLogger, context, notificationManager) + getAvalancheSuppressor() ) { ensurePeekState() assertShouldHeadsUp( @@ -263,8 +258,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager, - uiEventLogger, context, notificationManager) + getAvalancheSuppressor() ) { ensurePeekState() assertShouldHeadsUp( @@ -281,8 +275,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager, - uiEventLogger, context, notificationManager) + getAvalancheSuppressor() ) { ensurePeekState() assertShouldHeadsUp( @@ -300,8 +293,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager, - uiEventLogger, context, notificationManager) + getAvalancheSuppressor() ) { ensurePeekState() assertShouldHeadsUp( @@ -318,8 +310,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager, - uiEventLogger, context, notificationManager) + getAvalancheSuppressor() ) { assertFsiNotSuppressed() } @@ -330,8 +321,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro avalancheProvider.startTime = whenAgo(10) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager, - uiEventLogger, context, notificationManager) + getAvalancheSuppressor() ) { ensurePeekState() assertShouldHeadsUp( @@ -359,8 +349,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro setAllowedEmergencyPkg(true) withFilter( - AvalancheSuppressor(avalancheProvider, systemClock, settingsInteractor, packageManager, - uiEventLogger, context, notificationManager) + getAvalancheSuppressor() ) { ensurePeekState() assertShouldHeadsUp( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt index 9d3d9c156238..284efc71a96d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt @@ -139,6 +139,7 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { protected val settingsInteractor: NotificationSettingsInteractor = mock() protected val packageManager: PackageManager = mock() protected val notificationManager: NotificationManager = mock() + protected val logger: VisualInterruptionDecisionLogger = mock() protected abstract val provider: VisualInterruptionDecisionProvider private val neverSuppresses = object : NotificationInterruptSuppressor {} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt index 491919a16a4e..30a1214d69d0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt @@ -35,6 +35,9 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.res.R import com.android.systemui.statusbar.notification.ConversationNotificationProcessor import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.InflatedContentViewHolder +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.KeepExistingView +import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.NullContentView 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 @@ -74,6 +77,7 @@ import org.mockito.kotlin.never import org.mockito.kotlin.spy import org.mockito.kotlin.times import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyZeroInteractions import org.mockito.kotlin.whenever @SmallTest @@ -125,8 +129,10 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { ): RichOngoingContentModel? = fakeRonContentModel } - private var fakeRonViewHolder: InflatedContentViewHolder? = null - private val fakeRonViewInflater = + private var fakeContractedRonViewHolder: ContentViewInflationResult = NullContentView + private var fakeExpandedRonViewHolder: ContentViewInflationResult = NullContentView + private var fakeHeadsUpRonViewHolder: ContentViewInflationResult = NullContentView + private var fakeRonViewInflater = spy( object : RichOngoingNotificationViewInflater { override fun inflateView( @@ -134,8 +140,20 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { existingView: View?, entry: NotificationEntry, systemUiContext: Context, - parentView: ViewGroup - ): InflatedContentViewHolder? = fakeRonViewHolder + parentView: ViewGroup, + viewType: RichOngoingNotificationViewType + ): ContentViewInflationResult = + when (viewType) { + RichOngoingNotificationViewType.Contracted -> fakeContractedRonViewHolder + RichOngoingNotificationViewType.Expanded -> fakeExpandedRonViewHolder + RichOngoingNotificationViewType.HeadsUp -> fakeHeadsUpRonViewHolder + } + + override fun canKeepView( + contentModel: RichOngoingContentModel, + existingView: View?, + viewType: RichOngoingNotificationViewType + ): Boolean = false } ) @@ -149,6 +167,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { .setContentText("Text") .setStyle(Notification.BigTextStyle().bigText("big text")) testHelper = NotificationTestHelper(mContext, mDependency) + testHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL) row = spy(testHelper.createRow(builder.build())) notificationInflater = NotificationRowContentBinderImpl( @@ -388,15 +407,62 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @Test fun testRonModelRequiredForRonView() { fakeRonContentModel = null + val contractedRonView = View(context) + val expandedRonView = View(context) + val headsUpRonView = View(context) + fakeContractedRonViewHolder = + InflatedContentViewHolder(view = contractedRonView, binder = mock()) + fakeExpandedRonViewHolder = + InflatedContentViewHolder(view = expandedRonView, binder = mock()) + fakeHeadsUpRonViewHolder = InflatedContentViewHolder(view = headsUpRonView, binder = mock()) + + // WHEN inflater inflates + val contentToInflate = + FLAG_CONTENT_VIEW_CONTRACTED or FLAG_CONTENT_VIEW_EXPANDED or FLAG_CONTENT_VIEW_HEADS_UP + inflateAndWait(notificationInflater, contentToInflate, row) + verifyZeroInteractions(fakeRonViewInflater) + } + + @Test + fun testRonModelCleansUpRemoteViews() { val ronView = View(context) - fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mock()) + + val entry = row.entry + + fakeRonContentModel = mock<TimerContentModel>() + fakeContractedRonViewHolder = + InflatedContentViewHolder(view = ronView, binder = mock<DeferredContentViewBinder>()) + // WHEN inflater inflates inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) - verify(fakeRonViewInflater, never()).inflateView(any(), any(), any(), any(), any()) + + // VERIFY + verify(cache).removeCachedView(eq(entry), eq(FLAG_CONTENT_VIEW_CONTRACTED)) + verify(cache).removeCachedView(eq(entry), eq(FLAG_CONTENT_VIEW_EXPANDED)) + verify(cache).removeCachedView(eq(entry), eq(FLAG_CONTENT_VIEW_HEADS_UP)) } @Test - fun testRonModelTriggersInflationOfRonView() { + fun testRonModelCleansUpSmartReplies() { + val ronView = View(context) + + val privateLayout = spy(row.privateLayout) + + row.privateLayout = privateLayout + + fakeRonContentModel = mock<TimerContentModel>() + fakeContractedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mock()) + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) + + // VERIFY + verify(privateLayout).setExpandedInflatedSmartReplies(eq(null)) + verify(privateLayout).setHeadsUpInflatedSmartReplies(eq(null)) + } + + @Test + fun testRonModelTriggersInflationOfContractedRonView() { val mockRonModel = mock<TimerContentModel>() val ronView = View(context) val mockBinder = mock<DeferredContentViewBinder>() @@ -405,18 +471,229 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { val privateLayout = row.privateLayout fakeRonContentModel = mockRonModel - fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder) + fakeContractedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder) + // WHEN inflater inflates inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) + // VERIFY that the inflater is invoked verify(fakeRonViewInflater) - .inflateView(eq(mockRonModel), any(), eq(entry), any(), eq(privateLayout)) + .inflateView( + eq(mockRonModel), + any(), + eq(entry), + any(), + eq(privateLayout), + eq(RichOngoingNotificationViewType.Contracted) + ) assertThat(row.privateLayout.contractedChild).isSameInstanceAs(ronView) verify(mockBinder).setupContentViewBinder() } @Test - fun ronViewAppliesElementsInOrder() { + fun testRonModelTriggersInflationOfExpandedRonView() { + val mockRonModel = mock<TimerContentModel>() + val ronView = View(context) + val mockBinder = mock<DeferredContentViewBinder>() + + val entry = row.entry + val privateLayout = row.privateLayout + + fakeRonContentModel = mockRonModel + fakeExpandedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder) + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_EXPANDED, row) + + // VERIFY that the inflater is invoked + verify(fakeRonViewInflater) + .inflateView( + eq(mockRonModel), + any(), + eq(entry), + any(), + eq(privateLayout), + eq(RichOngoingNotificationViewType.Expanded) + ) + assertThat(row.privateLayout.expandedChild).isSameInstanceAs(ronView) + verify(mockBinder).setupContentViewBinder() + } + + @Test + fun testRonModelTriggersInflationOfHeadsUpRonView() { + val mockRonModel = mock<TimerContentModel>() + val ronView = View(context) + val mockBinder = mock<DeferredContentViewBinder>() + + val entry = row.entry + val privateLayout = row.privateLayout + + fakeRonContentModel = mockRonModel + fakeHeadsUpRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder) + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row) + + // VERIFY that the inflater is invoked + verify(fakeRonViewInflater) + .inflateView( + eq(mockRonModel), + any(), + eq(entry), + any(), + eq(privateLayout), + eq(RichOngoingNotificationViewType.HeadsUp) + ) + assertThat(row.privateLayout.headsUpChild).isSameInstanceAs(ronView) + verify(mockBinder).setupContentViewBinder() + } + + @Test + fun keepExistingViewForContractedRonNotChangingContractedChild() { + val oldHandle = mock<DisposableHandle>() + val mockRonModel = mock<TimerContentModel>() + + row.privateLayout.mContractedBinderHandle = oldHandle + val entry = spy(row.entry) + row.entry = entry + val privateLayout = spy(row.privateLayout) + row.privateLayout = privateLayout + + fakeRonContentModel = mockRonModel + fakeContractedRonViewHolder = KeepExistingView + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) + + // THEN do not dispose old contracted binder handle and change contracted child + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel }) + verifyZeroInteractions(oldHandle) + verify(privateLayout, never()).setContractedChild(any()) + } + + @Test + fun keepExistingViewForExpandedRonNotChangingExpandedChild() { + val oldHandle = mock<DisposableHandle>() + val mockRonModel = mock<TimerContentModel>() + + row.privateLayout.mExpandedBinderHandle = oldHandle + val entry = spy(row.entry) + row.entry = entry + val privateLayout = spy(row.privateLayout) + row.privateLayout = privateLayout + + fakeRonContentModel = mockRonModel + fakeExpandedRonViewHolder = KeepExistingView + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_EXPANDED, row) + + // THEN do not dispose old expanded binder handle and change expanded child + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel }) + verifyZeroInteractions(oldHandle) + verify(privateLayout, never()).setExpandedChild(any()) + } + + @Test + fun keepExistingViewForHeadsUpRonNotChangingHeadsUpChild() { + val oldHandle = mock<DisposableHandle>() + val mockRonModel = mock<TimerContentModel>() + + row.privateLayout.mHeadsUpBinderHandle = oldHandle + val entry = spy(row.entry) + row.entry = entry + val privateLayout = spy(row.privateLayout) + row.privateLayout = privateLayout + + fakeRonContentModel = mockRonModel + fakeHeadsUpRonViewHolder = KeepExistingView + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row) + + // THEN - do not dispose old heads up binder handle and change heads up child + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel }) + verifyZeroInteractions(oldHandle) + verify(privateLayout, never()).setHeadsUpChild(any()) + } + + @Test + fun nullContentViewForContractedRonAppliesElementsInOrder() { + val oldHandle = mock<DisposableHandle>() + val mockRonModel = mock<TimerContentModel>() + + row.privateLayout.mContractedBinderHandle = oldHandle + val entry = spy(row.entry) + row.entry = entry + val privateLayout = spy(row.privateLayout) + row.privateLayout = privateLayout + + fakeRonContentModel = mockRonModel + fakeContractedRonViewHolder = NullContentView + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) + + // Validate that these 4 steps happen in this precise order + inOrder(oldHandle, entry, privateLayout, cache) { + verify(oldHandle).dispose() + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel }) + verify(privateLayout).setContractedChild(eq(null)) + } + } + + @Test + fun nullContentViewForExpandedRonAppliesElementsInOrder() { + val oldHandle = mock<DisposableHandle>() + val mockRonModel = mock<TimerContentModel>() + + row.privateLayout.mExpandedBinderHandle = oldHandle + val entry = spy(row.entry) + row.entry = entry + val privateLayout = spy(row.privateLayout) + row.privateLayout = privateLayout + + fakeRonContentModel = mockRonModel + fakeExpandedRonViewHolder = NullContentView + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_EXPANDED, row) + + // Validate that these 4 steps happen in this precise order + inOrder(oldHandle, entry, privateLayout, cache) { + verify(oldHandle).dispose() + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel }) + verify(privateLayout).setExpandedChild(eq(null)) + } + } + + @Test + fun nullContentViewForHeadsUpRonAppliesElementsInOrder() { + val oldHandle = mock<DisposableHandle>() + val mockRonModel = mock<TimerContentModel>() + + row.privateLayout.mHeadsUpBinderHandle = oldHandle + val entry = spy(row.entry) + row.entry = entry + val privateLayout = spy(row.privateLayout) + row.privateLayout = privateLayout + + fakeRonContentModel = mockRonModel + fakeHeadsUpRonViewHolder = NullContentView + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row) + + // Validate that these 4 steps happen in this precise order + inOrder(oldHandle, entry, privateLayout, cache) { + verify(oldHandle).dispose() + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel }) + verify(privateLayout).setHeadsUpChild(eq(null)) + } + } + + @Test + fun contractedRonViewAppliesElementsInOrder() { val oldHandle = mock<DisposableHandle>() val mockRonModel = mock<TimerContentModel>() val ronView = View(context) @@ -429,7 +706,8 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { row.privateLayout = privateLayout fakeRonContentModel = mockRonModel - fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder) + fakeContractedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder) + // WHEN inflater inflates inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) @@ -443,16 +721,89 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { } @Test - fun testRonNotReinflating() { - val handle0 = mock<DisposableHandle>() - val handle1 = mock<DisposableHandle>() + fun expandedRonViewAppliesElementsInOrder() { + val oldHandle = mock<DisposableHandle>() + val mockRonModel = mock<TimerContentModel>() val ronView = View(context) + val mockBinder = mock<DeferredContentViewBinder>() + + row.privateLayout.mExpandedBinderHandle = oldHandle + val entry = spy(row.entry) + row.entry = entry + val privateLayout = spy(row.privateLayout) + row.privateLayout = privateLayout + + fakeRonContentModel = mockRonModel + fakeExpandedRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder) + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_EXPANDED, row) + + // Validate that these 4 steps happen in this precise order + inOrder(oldHandle, entry, privateLayout, mockBinder) { + verify(oldHandle).dispose() + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel }) + verify(privateLayout).setExpandedChild(eq(ronView)) + verify(mockBinder).setupContentViewBinder() + } + } + + @Test + fun headsUpRonViewAppliesElementsInOrder() { + val oldHandle = mock<DisposableHandle>() + val mockRonModel = mock<TimerContentModel>() + val ronView = View(context) + val mockBinder = mock<DeferredContentViewBinder>() + + row.privateLayout.mHeadsUpBinderHandle = oldHandle + val entry = spy(row.entry) + row.entry = entry + val privateLayout = spy(row.privateLayout) + row.privateLayout = privateLayout + + fakeRonContentModel = mockRonModel + fakeHeadsUpRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder) + + // WHEN inflater inflates + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row) + + // Validate that these 4 steps happen in this precise order + inOrder(oldHandle, entry, privateLayout, mockBinder) { + verify(oldHandle).dispose() + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel }) + verify(privateLayout).setHeadsUpChild(eq(ronView)) + verify(mockBinder).setupContentViewBinder() + } + } + + @Test + fun testRonNotReinflating() { + val oldContractedBinderHandle = mock<DisposableHandle>() + val oldExpandedBinderHandle = mock<DisposableHandle>() + val oldHeadsUpBinderHandle = mock<DisposableHandle>() + + val contractedBinderHandle = mock<DisposableHandle>() + val expandedBinderHandle = mock<DisposableHandle>() + val headsUpBinderHandle = mock<DisposableHandle>() + + val contractedRonView = View(context) + val expandedRonView = View(context) + val headsUpRonView = View(context) + val mockRonModel1 = mock<TimerContentModel>() val mockRonModel2 = mock<TimerContentModel>() - val mockBinder1 = mock<DeferredContentViewBinder>() - doReturn(handle1).whenever(mockBinder1).setupContentViewBinder() - row.privateLayout.mContractedBinderHandle = handle0 + val mockContractedViewBinder = mock<DeferredContentViewBinder>() + val mockExpandedViewBinder = mock<DeferredContentViewBinder>() + val mockHeadsUpViewBinder = mock<DeferredContentViewBinder>() + + doReturn(contractedBinderHandle).whenever(mockContractedViewBinder).setupContentViewBinder() + doReturn(expandedBinderHandle).whenever(mockExpandedViewBinder).setupContentViewBinder() + doReturn(headsUpBinderHandle).whenever(mockHeadsUpViewBinder).setupContentViewBinder() + + row.privateLayout.mContractedBinderHandle = oldContractedBinderHandle + row.privateLayout.mExpandedBinderHandle = oldExpandedBinderHandle + row.privateLayout.mHeadsUpBinderHandle = oldHeadsUpBinderHandle val entry = spy(row.entry) row.entry = entry val privateLayout = spy(row.privateLayout) @@ -460,31 +811,87 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { // WHEN inflater inflates both a model and a view fakeRonContentModel = mockRonModel1 - fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder1) - inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) + fakeContractedRonViewHolder = + InflatedContentViewHolder(view = contractedRonView, binder = mockContractedViewBinder) + fakeExpandedRonViewHolder = + InflatedContentViewHolder(view = expandedRonView, binder = mockExpandedViewBinder) + fakeHeadsUpRonViewHolder = + InflatedContentViewHolder(view = headsUpRonView, binder = mockHeadsUpViewBinder) + + val contentToInflate = + FLAG_CONTENT_VIEW_CONTRACTED or FLAG_CONTENT_VIEW_EXPANDED or FLAG_CONTENT_VIEW_HEADS_UP + inflateAndWait(notificationInflater, contentToInflate, row) // Validate that these 4 steps happen in this precise order - inOrder(handle0, entry, privateLayout, mockBinder1, handle1) { - verify(handle0).dispose() + inOrder( + oldContractedBinderHandle, + oldExpandedBinderHandle, + oldHeadsUpBinderHandle, + entry, + privateLayout, + mockContractedViewBinder, + mockExpandedViewBinder, + mockHeadsUpViewBinder, + contractedBinderHandle, + expandedBinderHandle, + headsUpBinderHandle + ) { + verify(oldContractedBinderHandle).dispose() + verify(oldExpandedBinderHandle).dispose() + verify(oldHeadsUpBinderHandle).dispose() + verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel1 }) - verify(privateLayout).setContractedChild(eq(ronView)) - verify(mockBinder1).setupContentViewBinder() - verify(handle1, never()).dispose() + + verify(privateLayout).setContractedChild(eq(contractedRonView)) + verify(mockContractedViewBinder).setupContentViewBinder() + + verify(privateLayout).setExpandedChild(eq(expandedRonView)) + verify(mockExpandedViewBinder).setupContentViewBinder() + + verify(privateLayout).setHeadsUpChild(eq(headsUpRonView)) + verify(mockHeadsUpViewBinder).setupContentViewBinder() + + verify(contractedBinderHandle, never()).dispose() + verify(expandedBinderHandle, never()).dispose() + verify(headsUpBinderHandle, never()).dispose() } - clearInvocations(handle0, entry, privateLayout, mockBinder1, handle1) + clearInvocations( + oldContractedBinderHandle, + oldExpandedBinderHandle, + oldHeadsUpBinderHandle, + entry, + privateLayout, + mockContractedViewBinder, + mockExpandedViewBinder, + mockHeadsUpViewBinder, + contractedBinderHandle, + expandedBinderHandle, + headsUpBinderHandle + ) // THEN when the inflater inflates just a model fakeRonContentModel = mockRonModel2 - fakeRonViewHolder = null - inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row) + fakeContractedRonViewHolder = KeepExistingView + fakeExpandedRonViewHolder = KeepExistingView + fakeHeadsUpRonViewHolder = KeepExistingView + + inflateAndWait(notificationInflater, contentToInflate, row) // Validate that for reinflation, the only thing we do us update the model - verify(handle1, never()).dispose() + verify(contractedBinderHandle, never()).dispose() + verify(expandedBinderHandle, never()).dispose() + verify(headsUpBinderHandle, never()).dispose() verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel2 }) verify(privateLayout, never()).setContractedChild(any()) - verify(mockBinder1, never()).setupContentViewBinder() - verify(handle1, never()).dispose() + verify(privateLayout, never()).setExpandedChild(any()) + verify(privateLayout, never()).setHeadsUpChild(any()) + verify(mockContractedViewBinder, never()).setupContentViewBinder() + verify(mockExpandedViewBinder, never()).setupContentViewBinder() + verify(mockHeadsUpViewBinder, never()).setupContentViewBinder() + verify(contractedBinderHandle, never()).dispose() + verify(expandedBinderHandle, never()).dispose() + verify(headsUpBinderHandle, never()).dispose() } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt index dfee2ed43dc0..76dc65cbc915 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt @@ -17,17 +17,24 @@ package com.android.systemui.statusbar.phone import android.app.AlarmManager +import android.app.AutomaticZenRule +import android.app.NotificationManager import android.app.admin.DevicePolicyManager import android.app.admin.DevicePolicyResourcesManager import android.content.SharedPreferences +import android.net.Uri import android.os.UserManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.provider.Settings +import android.service.notification.SystemZenRules +import android.service.notification.ZenModeConfig import android.telecom.TelecomManager import android.testing.TestableLooper import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.settingslib.notification.modes.TestModeBuilder import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher @@ -53,12 +60,13 @@ import com.android.systemui.statusbar.policy.RotationLockController import com.android.systemui.statusbar.policy.SensorPrivacyController import com.android.systemui.statusbar.policy.UserInfoController import com.android.systemui.statusbar.policy.ZenModeController +import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository +import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor +import com.android.systemui.testKosmos import com.android.systemui.util.RingerModeTracker import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.kotlin.JavaAdapter -import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture -import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.DateFormatUtil import com.android.systemui.util.time.FakeSystemClock import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -83,7 +91,10 @@ import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.reset @RunWith(AndroidJUnit4::class) @RunWithLooper @@ -91,7 +102,11 @@ import org.mockito.kotlin.argumentCaptor @SmallTest class PhoneStatusBarPolicyTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val zenModeRepository = kosmos.fakeZenModeRepository + companion object { + private const val ZEN_SLOT = "zen" private const val ALARM_SLOT = "alarm" private const val CAST_SLOT = "cast" private const val SCREEN_RECORD_SLOT = "screen_record" @@ -109,7 +124,6 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() { @Mock private lateinit var userInfoController: UserInfoController @Mock private lateinit var rotationLockController: RotationLockController @Mock private lateinit var dataSaverController: DataSaverController - @Mock private lateinit var zenModeController: ZenModeController @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var locationController: LocationController @@ -133,6 +147,7 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() { private val testScope = TestScope(UnconfinedTestDispatcher()) private val fakeConnectedDisplayStateProvider = FakeConnectedDisplayStateProvider() + private val zenModeController = FakeZenModeController() private lateinit var executor: FakeExecutor private lateinit var statusBarPolicy: PhoneStatusBarPolicy @@ -374,6 +389,102 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() { verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any()) } + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_UI_ICONS) + fun zenModeInteractorActiveModeChanged_showsModeIcon() = + testScope.runTest { + statusBarPolicy.init() + reset(iconController) + + zenModeRepository.addModes( + listOf( + TestModeBuilder() + .setId("bedtime") + .setName("Bedtime Mode") + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setActive(true) + .setPackage("some.package") + .setIconResId(123) + .build(), + TestModeBuilder() + .setId("other") + .setName("Other Mode") + .setType(AutomaticZenRule.TYPE_OTHER) + .setActive(true) + .setPackage(SystemZenRules.PACKAGE_ANDROID) + .setIconResId(456) + .build(), + ) + ) + runCurrent() + + verify(iconController).setIconVisibility(eq(ZEN_SLOT), eq(true)) + verify(iconController) + .setResourceIcon( + eq(ZEN_SLOT), + eq("some.package"), + eq(123), + eq(null), + eq("Bedtime Mode") + ) + + zenModeRepository.deactivateMode("bedtime") + runCurrent() + + verify(iconController) + .setResourceIcon(eq(ZEN_SLOT), eq(null), eq(456), eq(null), eq("Other Mode")) + + zenModeRepository.deactivateMode("other") + runCurrent() + + verify(iconController).setIconVisibility(eq(ZEN_SLOT), eq(false)) + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_UI_ICONS) + fun zenModeControllerOnGlobalZenChanged_doesNotUpdateDndIcon() { + statusBarPolicy.init() + reset(iconController) + + zenModeController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, null) + + verify(iconController, never()).setIconVisibility(eq(ZEN_SLOT), any()) + verify(iconController, never()).setIcon(eq(ZEN_SLOT), anyInt(), any()) + verify(iconController, never()).setResourceIcon(eq(ZEN_SLOT), any(), any(), any(), any()) + } + + @Test + @DisableFlags(android.app.Flags.FLAG_MODES_UI_ICONS) + fun zenModeInteractorActiveModeChanged_withFlagDisabled_ignored() = + testScope.runTest { + statusBarPolicy.init() + reset(iconController) + + zenModeRepository.addMode(id = "Bedtime", active = true) + runCurrent() + + verify(iconController, never()).setIconVisibility(eq(ZEN_SLOT), any()) + verify(iconController, never()).setIcon(eq(ZEN_SLOT), anyInt(), any()) + verify(iconController, never()) + .setResourceIcon(eq(ZEN_SLOT), any(), any(), any(), any()) + } + + @Test + @DisableFlags(android.app.Flags.FLAG_MODES_UI_ICONS) + fun zenModeControllerOnGlobalZenChanged_withFlagDisabled_updatesDndIcon() { + statusBarPolicy.init() + reset(iconController) + + zenModeController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, null) + + verify(iconController).setIconVisibility(eq(ZEN_SLOT), eq(true)) + verify(iconController).setIcon(eq(ZEN_SLOT), anyInt(), eq("Priority only")) + + zenModeController.setZen(Settings.Global.ZEN_MODE_OFF, null, null) + + verify(iconController).setIconVisibility(eq(ZEN_SLOT), eq(false)) + } + private fun createAlarmInfo(): AlarmManager.AlarmClockInfo { return AlarmManager.AlarmClockInfo(10L, null) } @@ -412,6 +523,7 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() { privacyItemController, privacyLogger, fakeConnectedDisplayStateProvider, + kosmos.zenModeInteractor, JavaAdapter(testScope.backgroundScope) ) } @@ -433,4 +545,51 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() { override val concurrentDisplaysInProgress: Flow<Boolean> get() = TODO("Not yet implemented") } + + private class FakeZenModeController : ZenModeController { + + private val callbacks = mutableListOf<ZenModeController.Callback>() + private var zen = Settings.Global.ZEN_MODE_OFF + private var consolidatedPolicy = NotificationManager.Policy(0, 0, 0) + + override fun addCallback(listener: ZenModeController.Callback) { + callbacks.add(listener) + } + + override fun removeCallback(listener: ZenModeController.Callback) { + callbacks.remove(listener) + } + + override fun setZen(zen: Int, conditionId: Uri?, reason: String?) { + this.zen = zen + callbacks.forEach { it.onZenChanged(zen) } + } + + override fun getZen(): Int = zen + + override fun getManualRule(): ZenModeConfig.ZenRule = throw NotImplementedError() + + override fun getConfig(): ZenModeConfig = throw NotImplementedError() + + fun setConsolidatedPolicy(policy: NotificationManager.Policy) { + this.consolidatedPolicy = policy + callbacks.forEach { it.onConsolidatedPolicyChanged(consolidatedPolicy) } + } + + override fun getConsolidatedPolicy(): NotificationManager.Policy = consolidatedPolicy + + override fun getNextAlarm() = throw NotImplementedError() + + override fun isZenAvailable() = throw NotImplementedError() + + override fun getEffectsSuppressor() = throw NotImplementedError() + + override fun isCountdownConditionSupported() = throw NotImplementedError() + + override fun getCurrentUser() = throw NotImplementedError() + + override fun isVolumeRestricted() = throw NotImplementedError() + + override fun areNotificationsHiddenInShade() = throw NotImplementedError() + } } 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 9b611057c059..b75ac2bc9bde 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 @@ -108,7 +108,9 @@ import com.android.systemui.statusbar.domain.interactor.StatusBarKeyguardViewMan import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.unfold.SysUIUnfoldComponent; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; +import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.kotlin.JavaAdapter; +import com.android.systemui.util.time.FakeSystemClock; import com.google.common.truth.Truth; @@ -175,6 +177,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mBouncerExpansionCallback; private FakeKeyguardStateController mKeyguardStateController = spy(new FakeKeyguardStateController()); + private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); @Mock private ViewRootImpl mViewRootImpl; @@ -238,6 +241,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mock(JavaAdapter.class), () -> mSceneInteractor, mock(StatusBarKeyguardViewManagerInteractor.class), + mExecutor, () -> mDeviceEntryInteractor) { @Override public ViewRootImpl getViewRootImpl() { @@ -760,6 +764,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mock(JavaAdapter.class), () -> mSceneInteractor, mock(StatusBarKeyguardViewManagerInteractor.class), + mExecutor, () -> mDeviceEntryInteractor) { @Override public ViewRootImpl getViewRootImpl() { @@ -1084,6 +1089,9 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { .thenReturn(KeyguardState.LOCKSCREEN); reset(mCentralSurfaces); + // Advance past reattempts + mStatusBarKeyguardViewManager.setAttemptsToShowBouncer(10); + mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false); verify(mPrimaryBouncerInteractor).show(true); verify(mCentralSurfaces).showKeyguard(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImplTest.kt index 19abbd58ad3a..26a57e4c1ca9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImplTest.kt @@ -16,7 +16,9 @@ package com.android.systemui.statusbar.phone.ui +import android.graphics.drawable.ColorDrawable import android.os.UserHandle +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.statusbar.StatusBarIcon @@ -406,6 +408,33 @@ class StatusBarIconControllerImplTest : SysuiTestCase() { .isInstanceOf(StatusBarIconHolder.BindableIconHolder::class.java) } + @Test + fun setIcon_setsIconInHolder() { + underTest.setIcon("slot", 123, "description") + + val iconHolder = iconList.getIconHolder("slot", 0) + assertThat(iconHolder).isNotNull() + assertThat(iconHolder?.icon?.pkg).isEqualTo(mContext.packageName) + assertThat(iconHolder?.icon?.icon?.resId).isEqualTo(123) + assertThat(iconHolder?.icon?.icon?.resPackage).isEqualTo(mContext.packageName) + assertThat(iconHolder?.icon?.contentDescription).isEqualTo("description") + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_UI, android.app.Flags.FLAG_MODES_UI_ICONS) + fun setResourceIcon_setsIconAndPreloadedIconInHolder() { + val drawable = ColorDrawable(1) + underTest.setResourceIcon("slot", "some.package", 123, drawable, "description") + + val iconHolder = iconList.getIconHolder("slot", 0) + assertThat(iconHolder).isNotNull() + assertThat(iconHolder?.icon?.pkg).isEqualTo("some.package") + assertThat(iconHolder?.icon?.icon?.resId).isEqualTo(123) + assertThat(iconHolder?.icon?.icon?.resPackage).isEqualTo("some.package") + assertThat(iconHolder?.icon?.contentDescription).isEqualTo("description") + assertThat(iconHolder?.icon?.preloadedIcon).isEqualTo(drawable) + } + private fun createExternalIcon(): StatusBarIcon { return StatusBarIcon( "external.package", diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt index c3cc33ffb81a..bf31f1e6d569 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt @@ -45,6 +45,7 @@ import org.junit.runner.RunWith import org.mockito.MockitoAnnotations import org.mockito.kotlin.mock +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { @@ -88,7 +89,7 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { } @Test - fun icon_nullWhenShouldNotShow_satelliteNotAllowed() = + fun icon_null_satelliteNotAllowed() = testScope.runTest { val latest by collectLastValue(underTest.icon) @@ -108,7 +109,30 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { } @Test - fun icon_nullWhenShouldNotShow_notAllOos() = + fun icon_null_connectedAndNotAllowed() = + testScope.runTest { + val latest by collectLastValue(underTest.icon) + + // GIVEN satellite is not allowed + repo.isSatelliteAllowedForCurrentLocation.value = false + + // GIVEN all icons are OOS + val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) + i1.isInService.value = false + i1.isEmergencyOnly.value = false + + // GIVEN satellite state is Connected. (this should not ever occur, but still) + repo.connectionState.value = SatelliteConnectionState.Connected + + // GIVEN apm is disabled + airplaneModeRepository.setIsAirplaneMode(false) + + // THEN icon is null despite the connected state + assertThat(latest).isNull() + } + + @Test + fun icon_null_notAllOos() = testScope.runTest { val latest by collectLastValue(underTest.icon) @@ -127,9 +151,28 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun icon_nullWhenShouldNotShow_isEmergencyOnly() = + fun icon_null_allOosAndNotAllowed() = + testScope.runTest { + val latest by collectLastValue(underTest.icon) + + // GIVEN satellite is allowed + repo.isSatelliteAllowedForCurrentLocation.value = false + + // GIVEN all icons are OOS + val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) + i1.isInService.value = false + i1.isEmergencyOnly.value = false + + // GIVEN apm is disabled + airplaneModeRepository.setIsAirplaneMode(false) + + // THEN icon is null because it is not allowed + assertThat(latest).isNull() + } + + @Test + fun icon_null_isEmergencyOnly() = testScope.runTest { val latest by collectLastValue(underTest.icon) @@ -158,7 +201,7 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { } @Test - fun icon_nullWhenShouldNotShow_apmIsEnabled() = + fun icon_null_apmIsEnabled() = testScope.runTest { val latest by collectLastValue(underTest.icon) @@ -177,9 +220,8 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun icon_satelliteIsOn() = + fun icon_notNull_satelliteAllowedAndAllOos() = testScope.runTest { val latest by collectLastValue(underTest.icon) @@ -201,7 +243,6 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(Icon::class.java) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun icon_hysteresisWhenEnablingIcon() = testScope.runTest { @@ -234,9 +275,56 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun icon_deviceIsProvisioned() = + fun icon_ignoresHysteresis_whenConnected() = + testScope.runTest { + val latest by collectLastValue(underTest.icon) + + // GIVEN satellite is allowed + repo.isSatelliteAllowedForCurrentLocation.value = true + + // GIVEN all icons are OOS + val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) + i1.isInService.value = false + i1.isEmergencyOnly.value = false + + // GIVEN apm is disabled + airplaneModeRepository.setIsAirplaneMode(false) + + // GIVEN satellite reports that it is Connected + repo.connectionState.value = SatelliteConnectionState.Connected + + // THEN icon is non null because we are connected, despite the normal OOS icon waiting + // 10 seconds for hysteresis + assertThat(latest).isInstanceOf(Icon::class.java) + } + + @Test + fun icon_ignoresHysteresis_whenOn() = + testScope.runTest { + val latest by collectLastValue(underTest.icon) + + // GIVEN satellite is allowed + repo.isSatelliteAllowedForCurrentLocation.value = true + + // GIVEN all icons are OOS + val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) + i1.isInService.value = false + i1.isEmergencyOnly.value = false + + // GIVEN apm is disabled + airplaneModeRepository.setIsAirplaneMode(false) + + // GIVEN satellite reports that it is Connected + repo.connectionState.value = SatelliteConnectionState.On + + // THEN icon is non null because the connection state is On, despite the normal OOS icon + // waiting 10 seconds for hysteresis + assertThat(latest).isInstanceOf(Icon::class.java) + } + + @Test + fun icon_satelliteIsProvisioned() = testScope.runTest { val latest by collectLastValue(underTest.icon) @@ -267,7 +355,6 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(Icon::class.java) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun icon_wifiIsActive() = testScope.runTest { @@ -324,13 +411,13 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { } @Test - fun carrierText_nullWhenShouldNotShow_notAllOos() = + fun carrierText_null_notAllOos() = testScope.runTest { val latest by collectLastValue(underTest.carrierText) - // GIVEN satellite is allowed + connected + // GIVEN satellite is allowed + off repo.isSatelliteAllowedForCurrentLocation.value = true - repo.connectionState.value = SatelliteConnectionState.Connected + repo.connectionState.value = SatelliteConnectionState.Off // GIVEN all icons are not OOS val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) @@ -344,9 +431,8 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun carrierText_nullWhenShouldNotShow_isEmergencyOnly() = + fun carrierText_notNull_notAllOos_butConnected() = testScope.runTest { val latest by collectLastValue(underTest.carrierText) @@ -354,25 +440,17 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { repo.isSatelliteAllowedForCurrentLocation.value = true repo.connectionState.value = SatelliteConnectionState.Connected - // GIVEN all icons are OOS + // GIVEN all icons are not OOS val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) - i1.isInService.value = false + i1.isInService.value = true i1.isEmergencyOnly.value = false // GIVEN apm is disabled airplaneModeRepository.setIsAirplaneMode(false) - // Wait for delay to be completed - advanceTimeBy(10.seconds) - - // THEN carrier text is set because we don't have service + // THEN carrier text is not null, because it is connected + // This case should never happen, but let's test it anyway assertThat(latest).isNotNull() - - // GIVEN the connection is emergency only - i1.isEmergencyOnly.value = true - - // THEN carrier text is null because we have emergency connection - assertThat(latest).isNull() } @Test @@ -396,7 +474,6 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun carrierText_satelliteIsOn() = testScope.runTest { @@ -421,9 +498,8 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isNotNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun carrierText_hysteresisWhenEnablingText() = + fun carrierText_noHysteresisWhenEnablingText_connected() = testScope.runTest { val latest by collectLastValue(underTest.carrierText) @@ -439,23 +515,10 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { // GIVEN apm is disabled airplaneModeRepository.setIsAirplaneMode(false) - // THEN carrier text is null because of the hysteresis - assertThat(latest).isNull() - - // Wait for delay to be completed - advanceTimeBy(10.seconds) - - // THEN carrier text is set after the delay + // THEN carrier text is not null because we skip hysteresis when connected assertThat(latest).isNotNull() - - // GIVEN apm is enabled - airplaneModeRepository.setIsAirplaneMode(true) - - // THEN carrier text is null immediately - assertThat(latest).isNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun carrierText_deviceIsProvisioned() = testScope.runTest { @@ -489,7 +552,6 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isNotNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun carrierText_wifiIsActive() = testScope.runTest { @@ -526,9 +588,8 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { assertThat(latest).isNotNull() } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun carrierText_connectionStateUnknown_null() = + fun carrierText_connectionStateUnknown_usesEmergencyOnlyText() = testScope.runTest { val latest by collectLastValue(underTest.carrierText) @@ -544,12 +605,12 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { // Wait for delay to be completed advanceTimeBy(10.seconds) - assertThat(latest).isNull() + assertThat(latest) + .isEqualTo(context.getString(R.string.satellite_emergency_only_carrier_text)) } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun carrierText_connectionStateOff_null() = + fun carrierText_connectionStateOff_usesEmergencyOnlyText() = testScope.runTest { val latest by collectLastValue(underTest.carrierText) @@ -565,10 +626,10 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { // Wait for delay to be completed advanceTimeBy(10.seconds) - assertThat(latest).isNull() + assertThat(latest) + .isEqualTo(context.getString(R.string.satellite_emergency_only_carrier_text)) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun carrierText_connectionStateOn_notConnectedString() = testScope.runTest { @@ -590,7 +651,6 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { .isEqualTo(context.getString(R.string.satellite_connected_carrier_text)) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun carrierText_connectionStateConnected_connectedString() = testScope.runTest { diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt index 84cd79d9c8dc..25ceea951d3c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt @@ -26,8 +26,8 @@ class CreateUserActivityTest : SysuiTestCase() { val dialog: Dialog = mock() whenever( createDialog( - /* activity = */ nullable(), - /* activityStarter = */ nullable(), + /* activity = */ any(), + /* activityStarter = */ any(), /* isMultipleAdminsEnabled = */ any(), /* successCallback = */ nullable(), /* cancelCallback = */ nullable() diff --git a/packages/SystemUI/tests/utils/src/android/app/StatusBarManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/app/StatusBarManagerKosmos.kt new file mode 100644 index 000000000000..6251ae909fc8 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/android/app/StatusBarManagerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +val Kosmos.statusBarManager by Kosmos.Fixture { mock<StatusBarManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt index c00454f3bc48..5d7e7c726c6c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt @@ -30,7 +30,7 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : override fun addWidget( provider: ComponentName, user: UserHandle, - priority: Int, + rank: Int?, configurator: WidgetConfigurator? ) { coroutineScope.launch { @@ -38,7 +38,7 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : val providerInfo = AppWidgetProviderInfo().apply { this.provider = provider } val configured = configurator?.configureWidget(id) ?: true if (configured) { - onConfigured(id, providerInfo, priority) + onConfigured(id, providerInfo, rank ?: -1) } } } @@ -46,14 +46,14 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : fun addWidget( appWidgetId: Int, componentName: String = "pkg/cls", - priority: Int = 0, + rank: Int = 0, category: Int = AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD, userId: Int = 0, ) { fakeDatabase[appWidgetId] = CommunalWidgetContentModel.Available( appWidgetId = appWidgetId, - priority = priority, + rank = rank, providerInfo = AppWidgetProviderInfo().apply { provider = ComponentName.unflattenFromString(componentName)!! @@ -73,14 +73,14 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : fun addPendingWidget( appWidgetId: Int, componentName: String = "pkg/cls", - priority: Int = 0, + rank: Int = 0, icon: Bitmap? = null, userId: Int = 0, ) { fakeDatabase[appWidgetId] = CommunalWidgetContentModel.Pending( appWidgetId = appWidgetId, - priority = priority, + rank = rank, componentName = ComponentName.unflattenFromString(componentName)!!, icon = icon, user = UserHandle(userId), @@ -97,8 +97,8 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : override fun abortRestoreWidgets() {} - private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) { + private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, rank: Int) { _communalWidgets.value += - listOf(CommunalWidgetContentModel.Available(id, providerInfo, priority)) + listOf(CommunalWidgetContentModel.Available(id, providerInfo, rank)) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt index b9be04dc0a32..3dfe0eea500f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt @@ -33,6 +33,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.log.FaceAuthenticationLogger import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.user.data.repository.userRepository import com.android.systemui.util.mockito.mock import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -57,6 +58,7 @@ val Kosmos.deviceEntryFaceAuthInteractor by powerInteractor = powerInteractor, biometricSettingsRepository = biometricSettingsRepository, trustManager = trustManager, + sceneInteractor = { sceneInteractor }, deviceEntryFaceAuthStatusInteractor = deviceEntryFaceAuthStatusInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt index cdfb297c27b9..fb4e2fb5fd14 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.education.data.repository import com.android.systemui.contextualeducation.GestureType +import com.android.systemui.education.data.model.EduDeviceConnectionTime import com.android.systemui.education.data.model.GestureEduModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -27,22 +28,34 @@ class FakeContextualEducationRepository : ContextualEducationRepository { private val userGestureMap = mutableMapOf<Int, GestureEduModel>() private val _gestureEduModels = MutableStateFlow(GestureEduModel(userId = 0)) private val gestureEduModelsFlow = _gestureEduModels.asStateFlow() + + private val userEduDeviceConnectionTimeMap = mutableMapOf<Int, EduDeviceConnectionTime>() + private val _eduDeviceConnectionTime = MutableStateFlow(EduDeviceConnectionTime()) + private val eduDeviceConnectionTime = _eduDeviceConnectionTime.asStateFlow() + private var currentUser: Int = 0 override fun setUser(userId: Int) { if (!userGestureMap.contains(userId)) { userGestureMap[userId] = GestureEduModel(userId = userId) + userEduDeviceConnectionTimeMap[userId] = EduDeviceConnectionTime() } // save data of current user to the map userGestureMap[currentUser] = _gestureEduModels.value + userEduDeviceConnectionTimeMap[currentUser] = _eduDeviceConnectionTime.value // switch to data of new user _gestureEduModels.value = userGestureMap[userId]!! + _eduDeviceConnectionTime.value = userEduDeviceConnectionTimeMap[userId]!! } override fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel> { return gestureEduModelsFlow } + override fun readEduDeviceConnectionTime(): Flow<EduDeviceConnectionTime> { + return eduDeviceConnectionTime + } + override suspend fun updateGestureEduModel( gestureType: GestureType, transform: (GestureEduModel) -> GestureEduModel @@ -50,4 +63,11 @@ class FakeContextualEducationRepository : ContextualEducationRepository { val currentModel = _gestureEduModels.value _gestureEduModels.value = transform(currentModel) } + + override suspend fun updateEduDeviceConnectionTime( + transform: (EduDeviceConnectionTime) -> EduDeviceConnectionTime + ) { + val currentModel = _eduDeviceConnectionTime.value + _eduDeviceConnectionTime.value = transform(currentModel) + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt index 5088677161d8..88ab170d72b3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt @@ -17,14 +17,26 @@ package com.android.systemui.education.domain.interactor import com.android.systemui.education.data.repository.fakeEduClock +import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository +import com.android.systemui.keyboard.data.repository.keyboardRepository import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.touchpad.data.repository.touchpadRepository +import com.android.systemui.user.data.repository.userRepository var Kosmos.keyboardTouchpadEduInteractor by Kosmos.Fixture { KeyboardTouchpadEduInteractor( backgroundScope = testScope.backgroundScope, contextualEducationInteractor = contextualEducationInteractor, + userInputDeviceRepository = + UserInputDeviceRepository( + testDispatcher, + keyboardRepository, + touchpadRepository, + userRepository + ), clock = fakeEduClock ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/FakeMSDLPlayer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/FakeMSDLPlayer.kt new file mode 100644 index 000000000000..5ad973a54252 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/FakeMSDLPlayer.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.haptics.msdl + +import com.google.android.msdl.data.model.FeedbackLevel +import com.google.android.msdl.data.model.MSDLToken +import com.google.android.msdl.domain.InteractionProperties +import com.google.android.msdl.domain.MSDLPlayer + +class FakeMSDLPlayer : MSDLPlayer { + var currentFeedbackLevel = FeedbackLevel.DEFAULT + var latestTokenPlayed: MSDLToken? = null + private set + + var latestPropertiesPlayed: InteractionProperties? = null + private set + + override fun getSystemFeedbackLevel(): FeedbackLevel = currentFeedbackLevel + + override fun playToken(token: MSDLToken, properties: InteractionProperties?) { + latestTokenPlayed = token + latestPropertiesPlayed = properties + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/MSDLPlayerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/MSDLPlayerKosmos.kt new file mode 100644 index 000000000000..f5a05b44d2cf --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/MSDLPlayerKosmos.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.haptics.msdl + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.msdlPlayer by Kosmos.Fixture { FakeMSDLPlayer() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt deleted file mode 100644 index 9b7bca6d2d34..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.systemui.keyguard.domain.interactor - -import android.content.Context -import android.os.Handler -import com.android.keyguard.KeyguardSecurityModel -import com.android.keyguard.KeyguardUpdateMonitor -import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository -import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository -import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor -import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor -import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor -import com.android.systemui.bouncer.ui.BouncerView -import com.android.systemui.classifier.FalsingCollector -import com.android.systemui.deviceentry.domain.interactor.DeviceEntryBiometricsAllowedInteractor -import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor -import com.android.systemui.keyguard.DismissCallbackRegistry -import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.data.repository.FakeTrustRepository -import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.power.data.repository.FakePowerRepository -import com.android.systemui.power.domain.interactor.PowerInteractorFactory -import com.android.systemui.scene.domain.interactor.SceneInteractor -import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.user.domain.interactor.SelectedUserInteractor -import com.android.systemui.util.time.FakeSystemClock -import kotlinx.coroutines.test.TestScope -import org.mockito.Mockito.mock - -/** - * Helper to create a new KeyguardDismissInteractor in a way that doesn't require modifying many - * tests whenever we add a constructor param. - */ -object KeyguardDismissInteractorFactory { - @JvmOverloads - @JvmStatic - fun create( - context: Context, - testScope: TestScope, - trustRepository: FakeTrustRepository = FakeTrustRepository(), - keyguardRepository: FakeKeyguardRepository = FakeKeyguardRepository(), - bouncerRepository: FakeKeyguardBouncerRepository = FakeKeyguardBouncerRepository(), - keyguardUpdateMonitor: KeyguardUpdateMonitor = mock(KeyguardUpdateMonitor::class.java), - powerRepository: FakePowerRepository = FakePowerRepository(), - userRepository: FakeUserRepository = FakeUserRepository(), - ): WithDependencies { - val primaryBouncerInteractor = - PrimaryBouncerInteractor( - bouncerRepository, - mock(BouncerView::class.java), - mock(Handler::class.java), - mock(KeyguardStateController::class.java), - mock(KeyguardSecurityModel::class.java), - mock(PrimaryBouncerCallbackInteractor::class.java), - mock(FalsingCollector::class.java), - mock(DismissCallbackRegistry::class.java), - context, - keyguardUpdateMonitor, - trustRepository, - testScope.backgroundScope, - mock(SelectedUserInteractor::class.java), - mock(DeviceEntryFaceAuthInteractor::class.java), - ) - val alternateBouncerInteractor = - AlternateBouncerInteractor( - mock(StatusBarStateController::class.java), - mock(KeyguardStateController::class.java), - bouncerRepository, - FakeFingerprintPropertyRepository(), - FakeBiometricSettingsRepository(), - FakeSystemClock(), - keyguardUpdateMonitor, - { mock(DeviceEntryBiometricsAllowedInteractor::class.java) }, - { mock(KeyguardInteractor::class.java) }, - { mock(KeyguardTransitionInteractor::class.java) }, - { mock(SceneInteractor::class.java) }, - testScope.backgroundScope, - ) - val powerInteractorWithDeps = - PowerInteractorFactory.create( - repository = powerRepository, - ) - val selectedUserInteractor = SelectedUserInteractor(repository = userRepository) - return WithDependencies( - trustRepository = trustRepository, - keyguardRepository = keyguardRepository, - bouncerRepository = bouncerRepository, - keyguardUpdateMonitor = keyguardUpdateMonitor, - powerRepository = powerRepository, - userRepository = userRepository, - interactor = - KeyguardDismissInteractor( - trustRepository, - keyguardRepository, - primaryBouncerInteractor, - alternateBouncerInteractor, - powerInteractorWithDeps.powerInteractor, - selectedUserInteractor, - ), - ) - } - - data class WithDependencies( - val trustRepository: FakeTrustRepository, - val keyguardRepository: FakeKeyguardRepository, - val bouncerRepository: FakeKeyguardBouncerRepository, - val keyguardUpdateMonitor: KeyguardUpdateMonitor, - val powerRepository: FakePowerRepository, - val userRepository: FakeUserRepository, - val interactor: KeyguardDismissInteractor, - ) -} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorKosmos.kt index f33ca95e488d..ace11573c7c6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorKosmos.kt @@ -20,7 +20,10 @@ import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.keyguard.data.repository.trustRepository +import com.android.systemui.keyguard.dismissCallbackRegistry import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.user.domain.interactor.selectedUserInteractor import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -29,11 +32,14 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.keyguardDismissInteractor by Kosmos.Fixture { KeyguardDismissInteractor( - trustRepository = trustRepository, + mainDispatcher = testDispatcher, + scope = applicationCoroutineScope, keyguardRepository = keyguardRepository, primaryBouncerInteractor = primaryBouncerInteractor, + selectedUserInteractor = selectedUserInteractor, + dismissCallbackRegistry = dismissCallbackRegistry, + trustRepository = trustRepository, alternateBouncerInteractor = alternateBouncerInteractor, powerInteractor = powerInteractor, - selectedUserInteractor = selectedUserInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt index a95609efc16f..f5232ce8a200 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt @@ -54,6 +54,7 @@ object KeyguardInteractorFactory { sceneInteractor: SceneInteractor = mock(), fromGoneTransitionInteractor: FromGoneTransitionInteractor = mock(), fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor = mock(), + fromOccludedTransitionInteractor: FromOccludedTransitionInteractor = mock(), sharedNotificationContainerInteractor: SharedNotificationContainerInteractor? = null, powerInteractor: PowerInteractor = PowerInteractorFactory.create().powerInteractor, testScope: CoroutineScope = TestScope(), @@ -100,6 +101,7 @@ object KeyguardInteractorFactory { sceneInteractorProvider = { sceneInteractor }, fromGoneTransitionInteractor = { fromGoneTransitionInteractor }, fromLockscreenTransitionInteractor = { fromLockscreenTransitionInteractor }, + fromOccludedTransitionInteractor = { fromOccludedTransitionInteractor }, sharedNotificationContainerInteractor = { sncInteractor }, applicationScope = testScope, ), diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt index 5ab56e931175..e85114d04825 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt @@ -38,6 +38,7 @@ val Kosmos.keyguardInteractor: KeyguardInteractor by sceneInteractorProvider = { sceneInteractor }, fromGoneTransitionInteractor = { fromGoneTransitionInteractor }, fromLockscreenTransitionInteractor = { fromLockscreenTransitionInteractor }, + fromOccludedTransitionInteractor = { fromOccludedTransitionInteractor }, sharedNotificationContainerInteractor = { sharedNotificationContainerInteractor }, applicationScope = testScope.backgroundScope, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt index 73799b63a6fc..769612c988ba 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt @@ -20,6 +20,7 @@ import android.content.applicationContext import android.view.accessibility.accessibilityManagerWrapper import com.android.internal.logging.uiEventLogger import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor import com.android.systemui.flags.featureFlagsClassic import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.Kosmos @@ -38,5 +39,6 @@ val Kosmos.keyguardTouchHandlingInteractor by broadcastDispatcher = broadcastDispatcher, accessibilityManager = accessibilityManagerWrapper, pulsingGestureListener = pulsingGestureListener, + faceAuthInteractor = deviceEntryFaceAuthInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt index f1d87fe3abb7..29583153ccc6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt @@ -19,7 +19,6 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor -import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor import com.android.systemui.keyguard.dismissCallbackRegistry import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos @@ -33,6 +32,5 @@ val Kosmos.alternateBouncerViewModel by Fixture { keyguardTransitionInteractor = keyguardTransitionInteractor, dismissCallbackRegistry = dismissCallbackRegistry, alternateBouncerInteractor = { alternateBouncerInteractor }, - primaryBouncerInteractor = primaryBouncerInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt index 19b32bce77e7..f47b2df607c1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.biometrics.authController +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor import com.android.systemui.kosmos.Kosmos @@ -34,5 +35,6 @@ val Kosmos.lockscreenContentViewModel by shadeInteractor = shadeInteractor, unfoldTransitionInteractor = unfoldTransitionInteractor, occlusionInteractor = sceneContainerOcclusionInteractor, + deviceEntryInteractor = deviceEntryInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index 9fe66eb77feb..953363d010fe 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -42,6 +42,7 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.fromGoneTransitionInteractor import com.android.systemui.keyguard.domain.interactor.fromLockscreenTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.fromOccludedTransitionInteractor import com.android.systemui.keyguard.domain.interactor.fromPrimaryBouncerTransitionInteractor import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor @@ -128,6 +129,7 @@ class KosmosJavaAdapter() { val deviceProvisioningInteractor by lazy { kosmos.deviceProvisioningInteractor } val fakeDeviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository } val fromLockscreenTransitionInteractor by lazy { kosmos.fromLockscreenTransitionInteractor } + val fromOccludedTransitionInteractor by lazy { kosmos.fromOccludedTransitionInteractor } val fromPrimaryBouncerTransitionInteractor by lazy { kosmos.fromPrimaryBouncerTransitionInteractor } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeActivatable.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeActivatable.kt index 4c05939041bd..e66a2be66934 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeActivatable.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeActivatable.kt @@ -21,7 +21,7 @@ import kotlinx.coroutines.awaitCancellation class FakeActivatable( private val onActivation: () -> Unit = {}, private val onDeactivation: () -> Unit = {}, -) : BaseActivatable() { +) : ExclusiveActivatable() { var activationCount = 0 var cancellationCount = 0 diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt index 90cd8c766da8..165246284b5f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt @@ -17,7 +17,6 @@ package com.android.systemui.lifecycle import androidx.compose.runtime.getValue -import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -29,19 +28,21 @@ class FakeSysUiViewModel( private val onDeactivation: () -> Unit = {}, private val upstreamFlow: Flow<Boolean> = flowOf(true), private val upstreamStateFlow: StateFlow<Boolean> = MutableStateFlow(true).asStateFlow(), -) : SysUiViewModel() { +) : SysUiViewModel, ExclusiveActivatable() { var activationCount = 0 var cancellationCount = 0 - val stateBackedByFlow: Boolean by hydratedStateOf(initialValue = true, source = upstreamFlow) - val stateBackedByStateFlow: Boolean by hydratedStateOf(source = upstreamStateFlow) + private val hydrator = Hydrator() + val stateBackedByFlow: Boolean by + hydrator.hydratedStateOf(initialValue = true, source = upstreamFlow) + val stateBackedByStateFlow: Boolean by hydrator.hydratedStateOf(source = upstreamStateFlow) override suspend fun onActivated(): Nothing { activationCount++ onActivation() try { - awaitCancellation() + hydrator.activate() } finally { cancellationCount++ onDeactivation() diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/InstantTaskExecutorRule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/InstantTaskExecutorRule.kt index 373af5cdf4b7..373af5cdf4b7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/InstantTaskExecutorRule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/InstantTaskExecutorRule.kt diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt index 147318473998..61d5f1e3af53 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt @@ -26,7 +26,7 @@ import com.android.systemui.media.controls.util.mediaFlags import com.android.systemui.media.controls.util.mediaUiEventLogger import com.android.systemui.settings.userTracker import com.android.systemui.statusbar.notificationLockscreenUserManager -import com.android.systemui.util.time.systemClock +import com.android.systemui.util.time.fakeSystemClock import com.android.systemui.util.wakelock.WakeLockFake val Kosmos.mediaDataFilter by @@ -42,7 +42,7 @@ val Kosmos.mediaDataFilter by ), lockscreenUserManager = notificationLockscreenUserManager, executor = fakeExecutor, - systemClock = systemClock, + systemClock = fakeSystemClock, logger = mediaUiEventLogger, mediaFlags = mediaFlags, mediaFilterRepository = mediaFilterRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderKosmos.kt new file mode 100644 index 000000000000..a5690a0fa560 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderKosmos.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.domain.pipeline + +import android.app.statusBarManager +import android.content.testableContext +import com.android.systemui.graphics.imageLoader +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.media.controls.util.fakeMediaControllerFactory +import com.android.systemui.media.controls.util.mediaFlags +import com.android.systemui.plugins.activityStarter + +val Kosmos.mediaDataLoader by + Kosmos.Fixture { + MediaDataLoader( + testableContext, + testDispatcher, + testScope, + activityStarter, + fakeMediaControllerFactory, + mediaFlags, + imageLoader, + statusBarManager + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt index cc1ad1fda6dd..2127a88e5a45 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt @@ -28,7 +28,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.media.controls.data.repository.mediaDataRepository import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider -import com.android.systemui.media.controls.util.mediaControllerFactory +import com.android.systemui.media.controls.util.fakeMediaControllerFactory import com.android.systemui.media.controls.util.mediaFlags import com.android.systemui.media.controls.util.mediaUiEventLogger import com.android.systemui.plugins.activityStarter @@ -46,7 +46,7 @@ val Kosmos.mediaDataProcessor by uiExecutor = fakeExecutor, foregroundExecutor = fakeExecutor, handler = fakeExecutorHandler, - mediaControllerFactory = mediaControllerFactory, + mediaControllerFactory = fakeMediaControllerFactory, broadcastDispatcher = broadcastDispatcher, dumpManager = dumpManager, activityStarter = activityStarter, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt index b98f557c0c34..c479ce676761 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt @@ -22,8 +22,8 @@ import android.os.fakeExecutorHandler import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.media.controls.util.fakeMediaControllerFactory import com.android.systemui.media.controls.util.localMediaManagerFactory -import com.android.systemui.media.controls.util.mediaControllerFactory import com.android.systemui.media.muteawait.mediaMuteAwaitConnectionManagerFactory import com.android.systemui.statusbar.policy.configurationController @@ -31,7 +31,7 @@ val Kosmos.mediaDeviceManager by Kosmos.Fixture { MediaDeviceManager( context = applicationContext, - controllerFactory = mediaControllerFactory, + controllerFactory = fakeMediaControllerFactory, localMediaManagerFactory = localMediaManagerFactory, mr2manager = { MediaRouter2Manager.getInstance(applicationContext) }, muteAwaitConnectionManagerFactory = mediaMuteAwaitConnectionManagerFactory, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt index 6ec6378e3bc2..b7660e05ee91 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt @@ -19,7 +19,7 @@ package com.android.systemui.media.controls.domain.pipeline import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.kosmos.Kosmos import com.android.systemui.log.logcatLogBuffer -import com.android.systemui.media.controls.util.mediaControllerFactory +import com.android.systemui.media.controls.util.fakeMediaControllerFactory import com.android.systemui.media.controls.util.mediaFlags import com.android.systemui.plugins.statusbar.statusBarStateController import com.android.systemui.util.time.systemClock @@ -27,7 +27,7 @@ import com.android.systemui.util.time.systemClock val Kosmos.mediaTimeoutListener by Kosmos.Fixture { MediaTimeoutListener( - mediaControllerFactory = mediaControllerFactory, + mediaControllerFactory = fakeMediaControllerFactory, mainExecutor = fakeExecutor, logger = MediaTimeoutLogger(logcatLogBuffer("MediaTimeoutLogBuffer")), statusBarStateController = statusBarStateController, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/FakeMediaControllerFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/FakeMediaControllerFactory.kt new file mode 100644 index 000000000000..7f8348e2ca6f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/FakeMediaControllerFactory.kt @@ -0,0 +1,38 @@ +/* + * 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.util + +import android.content.Context +import android.media.session.MediaController +import android.media.session.MediaSession +import android.media.session.MediaSession.Token + +class FakeMediaControllerFactory(context: Context) : MediaControllerFactory(context) { + + private val mediaControllersForToken = mutableMapOf<Token, MediaController>() + + override fun create(token: MediaSession.Token): android.media.session.MediaController { + if (token !in mediaControllersForToken) { + super.create(token) + } + return mediaControllersForToken[token]!! + } + + fun setControllerForToken(token: Token, mediaController: MediaController) { + mediaControllersForToken[token] = mediaController + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaControllerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaControllerFactoryKosmos.kt index 1ce6e82f71d8..7ee58fa8a295 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaControllerFactoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaControllerFactoryKosmos.kt @@ -19,4 +19,5 @@ package com.android.systemui.media.controls.util import android.content.applicationContext import com.android.systemui.kosmos.Kosmos -val Kosmos.mediaControllerFactory by Kosmos.Fixture { MediaControllerFactory(applicationContext) } +val Kosmos.fakeMediaControllerFactory by + Kosmos.Fixture { FakeMediaControllerFactory(applicationContext) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt index ae8b411a4b95..f84c3bdfdaf1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt @@ -24,7 +24,7 @@ import com.android.systemui.scene.data.repository.sceneContainerRepository import com.android.systemui.scene.domain.resolver.sceneFamilyResolvers import com.android.systemui.scene.shared.logger.sceneLogger -val Kosmos.sceneInteractor by +val Kosmos.sceneInteractor: SceneInteractor by Kosmos.Fixture { SceneInteractor( applicationScope = applicationCoroutineScope, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeScene.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeScene.kt index 64e3526603f9..78358f5a9187 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeScene.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeScene.kt @@ -19,6 +19,8 @@ package com.android.systemui.scene.shared.model import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.lifecycle.ExclusiveActivatable +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onStart @@ -26,7 +28,7 @@ import kotlinx.coroutines.flow.receiveAsFlow class FakeScene( override val key: SceneKey, -) : Scene { +) : ExclusiveActivatable(), Scene { var isDestinationScenesBeingCollected = false private val destinationScenesChannel = Channel<Map<UserAction, UserActionResult>>() @@ -37,6 +39,10 @@ class FakeScene( .onStart { isDestinationScenesBeingCollected = true } .onCompletion { isDestinationScenesBeingCollected = false } + override suspend fun onActivated(): Nothing { + awaitCancellation() + } + suspend fun setDestinationScenes(value: Map<UserAction, UserActionResult>) { destinationScenesChannel.send(value) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelKosmos.kt new file mode 100644 index 000000000000..7e511355372b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelKosmos.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row.ui.viewmodel + +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository +import com.android.systemui.statusbar.notification.row.domain.interactor.getNotificationRowInteractor + +fun Kosmos.getEnRouteViewModel(repository: NotificationRowRepository) = + EnRouteViewModel( + dumpManager = dumpManager, + rowInteractor = getNotificationRowInteractor(repository), + ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/ReferenceTestUtils.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/leak/ReferenceTestUtils.java index b433e7a191f9..b433e7a191f9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/leak/ReferenceTestUtils.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/leak/ReferenceTestUtils.java diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java index a8328e4ab991..2dbac670b298 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java @@ -14,8 +14,11 @@ package com.android.systemui.utils.leaks; +import android.graphics.drawable.Drawable; import android.testing.LeakCheck; +import androidx.annotation.Nullable; + import com.android.internal.statusbar.StatusBarIcon; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState; import com.android.systemui.statusbar.phone.ui.IconManager; @@ -60,6 +63,11 @@ public class FakeStatusBarIconController extends BaseLeakChecker<IconManager> } @Override + public void setResourceIcon(String slot, @Nullable String resPackage, int iconResId, + @Nullable Drawable preloadedIcon, CharSequence contentDescription) { + } + + @Override public void setNewWifiIcon() { } diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java index 09068d5e2b56..26b0f617d971 100644 --- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java +++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java @@ -2605,11 +2605,7 @@ public class CameraExtensionsProxyService extends Service { ret.size.height = imageReaderOutputConfig.getSize().getHeight(); ret.imageFormat = imageReaderOutputConfig.getImageFormat(); ret.capacity = imageReaderOutputConfig.getMaxImages(); - if (EFV_SUPPORTED) { - ret.usage = imageReaderOutputConfig.getUsage(); - } else { - ret.usage = 0; - } + ret.usage = imageReaderOutputConfig.getUsage(); } else if (output instanceof MultiResolutionImageReaderOutputConfigImpl) { MultiResolutionImageReaderOutputConfigImpl multiResReaderConfig = (MultiResolutionImageReaderOutputConfigImpl) output; diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index 58cd2e4cee6c..be4cd761a4ec 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -211,6 +211,7 @@ java_library { libs: [ "junit", "flag-junit", + "framework-annotations-lib", ], visibility: ["//visibility:public"], } @@ -331,6 +332,7 @@ android_ravenwood_libgroup { name: "ravenwood-runtime", data: [ "framework-res", + "ravenwood-empty-res", ], libs: [ "100-framework-minus-apex.ravenwood", diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING index fbf27fa5bf4b..7e2ee3e2a052 100644 --- a/ravenwood/TEST_MAPPING +++ b/ravenwood/TEST_MAPPING @@ -12,9 +12,6 @@ { "name": "RavenwoodBivalentTest_device" }, - { - "name": "RavenwoodResApkTest" - }, // The sysui tests should match vendor/unbundled_google/packages/SystemUIGoogle/TEST_MAPPING { "name": "SystemUIGoogleTests", @@ -55,7 +52,7 @@ "host": true }, { - "name": "RavenwoodCoreTest", + "name": "RavenwoodResApkTest", "host": true }, { diff --git a/ravenwood/bivalenttest/Android.bp b/ravenwood/bivalenttest/Android.bp index 06cf08e6c3df..e897735493a3 100644 --- a/ravenwood/bivalenttest/Android.bp +++ b/ravenwood/bivalenttest/Android.bp @@ -39,6 +39,9 @@ android_ravenwood_test { "androidx.test.ext.junit", "androidx.test.rules", + "junit-params", + "platform-parametric-runner-lib", + // To make sure it won't cause VerifyError (b/324063814) "platformprotosnano", ], @@ -65,6 +68,9 @@ android_test { "androidx.test.ext.junit", "androidx.test.rules", + "junit-params", + "platform-parametric-runner-lib", + "ravenwood-junit", ], jni_libs: [ diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/CallTracker.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/CallTracker.java new file mode 100644 index 000000000000..8dadd398ad55 --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/CallTracker.java @@ -0,0 +1,110 @@ +/* + * 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.ravenwoodtest.bivalenttest.ravenizer; + +import static org.junit.Assert.fail; + +import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE; + +import android.util.Log; + +import java.lang.StackWalker.StackFrame; +import java.util.HashMap; + +/** + * Used to keep track of and count the number of calls. + */ +public class CallTracker { + public static final String TAG = "CallTracker"; + + private final HashMap<String, Integer> mNumCalled = new HashMap<>(); + + /** + * Call it when a method is called. It increments the count for the calling method. + */ + public void incrementMethodCallCount() { + var methodName = getCallingMethodName(1); + + Log.i(TAG, "Method called: " + methodName); + + mNumCalled.put(methodName, getNumCalled(methodName) + 1); + } + + /** + * Return the number of calls of a method. + */ + public int getNumCalled(String methodName) { + return mNumCalled.getOrDefault(methodName, 0); + } + + /** + * Return the current method name. (with the class name.) + */ + private static String getCallingMethodName(int frameOffset) { + var walker = StackWalker.getInstance(RETAIN_CLASS_REFERENCE); + var caller = walker.walk(frames -> + frames.skip(1 + frameOffset).findFirst().map(StackFrame::getMethodName) + ); + return caller.get(); + } + + /** + * Check the number of calls stored in {@link #mNumCalled}. + */ + protected void assertCalls(Object... methodNameAndCountPairs) { + // Create a local copy + HashMap<String, Integer> counts = new HashMap<>(mNumCalled); + for (int i = 0; i < methodNameAndCountPairs.length - 1; i += 2) { + String methodName = (String) methodNameAndCountPairs[i]; + int expectedCount = (Integer) methodNameAndCountPairs[i + 1]; + + if (getNumCalled(methodName) != expectedCount) { + fail(String.format("Method %s: expected call count=%d, actual=%d", + methodName, expectedCount, getNumCalled(methodName))); + } + counts.remove(methodName); + } + // All other entries are expected to be 0. + var sb = new StringBuilder(); + for (var e : counts.entrySet()) { + if (e.getValue() == 0) { + continue; + } + sb.append(String.format("Method %s: expected call count=0, actual=%d", + e.getKey(), e.getValue())); + } + if (sb.length() > 0) { + fail(sb.toString()); + } + } + + /** + * Same as {@link #assertCalls(Object...)} but it kills the process if it fails. + * Only use in @AfterClass. + */ + protected void assertCallsOrDie(Object... methodNameAndCountPairs) { + try { + assertCalls(methodNameAndCountPairs); + } catch (Throwable th) { + // TODO: I don't think it's by spec, but the exception here would be ignored both on + // ravenwood and on the device side. Look into it. + Log.e(TAG, "*** Failure detected in @AfterClass! ***", th); + Log.e(TAG, "JUnit seems to ignore exceptions from @AfterClass, so killing self."); + System.exit(7); + } + } + +} diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodAwareTestRunnerTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodAwareTestRunnerTest.java new file mode 100644 index 000000000000..d7c2c6cd73a8 --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodAwareTestRunnerTest.java @@ -0,0 +1,93 @@ +/* + * 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.ravenwoodtest.bivalenttest.ravenizer; + +import static org.junit.Assert.assertFalse; + +import android.platform.test.annotations.DisabledOnRavenwood; +import android.platform.test.ravenwood.RavenwoodAwareTestRunner.RavenwoodTestRunnerInitializing; +import android.platform.test.ravenwood.RavenwoodRule; +import android.util.Log; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Make sure RavenwoodAwareTestRunnerTest properly delegates to the original runner, + * and also run the special annotated methods. + */ +@RunWith(JUnitParamsRunner.class) +public class RavenwoodAwareTestRunnerTest { + public static final String TAG = "RavenwoodAwareTestRunnerTest"; + + private static final CallTracker sCallTracker = new CallTracker(); + + private static int getExpectedRavenwoodRunnerInitializingNumCalls() { + return RavenwoodRule.isOnRavenwood() ? 1 : 0; + } + + @RavenwoodTestRunnerInitializing + public static void ravenwoodRunnerInitializing() { + // No other calls should have been made. + sCallTracker.assertCalls(); + + sCallTracker.incrementMethodCallCount(); + } + + @BeforeClass + public static void beforeClass() { + sCallTracker.assertCalls( + "ravenwoodRunnerInitializing", + getExpectedRavenwoodRunnerInitializingNumCalls() + ); + sCallTracker.incrementMethodCallCount(); + } + + @Test + public void test1() { + sCallTracker.incrementMethodCallCount(); + } + + @Test + @Parameters({"foo", "bar"}) + public void testWithParams(String arg) { + sCallTracker.incrementMethodCallCount(); + } + + @Test + @DisabledOnRavenwood + public void testDeviceOnly() { + assertFalse(RavenwoodRule.isOnRavenwood()); + } + + @AfterClass + public static void afterClass() { + Log.i(TAG, "afterClass called"); + + sCallTracker.assertCallsOrDie( + "ravenwoodRunnerInitializing", + getExpectedRavenwoodRunnerInitializingNumCalls(), + "beforeClass", 1, + "test1", 1, + "testWithParams", 2 + ); + } +} diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitClassRuleDeviceOnlyTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitClassRuleDeviceOnlyTest.java new file mode 100644 index 000000000000..0f8be0eeebeb --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitClassRuleDeviceOnlyTest.java @@ -0,0 +1,52 @@ +/* + * 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.ravenwoodtest.bivalenttest.ravenizer; + +import android.platform.test.annotations.DisabledOnRavenwood; +import android.platform.test.ravenwood.RavenwoodRule; +import android.util.Log; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@DisabledOnRavenwood +public class RavenwoodImplicitClassRuleDeviceOnlyTest { + public static final String TAG = "RavenwoodImplicitClassRuleDeviceOnlyTest"; + + @BeforeClass + public static void beforeClass() { + Assert.assertFalse(RavenwoodRule.isOnRavenwood()); + } + + @Test + public void testDeviceOnly() { + Assert.assertFalse(RavenwoodRule.isOnRavenwood()); + } + + @AfterClass + public static void afterClass() { + if (RavenwoodRule.isOnRavenwood()) { + Log.e(TAG, "Even @AfterClass shouldn't be executed!"); + System.exit(1); + } + } +} diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleOrderRewriteTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleOrderRewriteTest.java new file mode 100644 index 000000000000..7ef40dc49e1a --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleOrderRewriteTest.java @@ -0,0 +1,136 @@ +/* + * 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.ravenwoodtest.bivalenttest.ravenizer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Assume; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +import java.util.HashMap; + +/** + * Make sure ravenizer will inject implicit rules and rewrite the existing rules' orders. + */ +@RunWith(AndroidJUnit4.class) +public class RavenwoodImplicitRuleOrderRewriteTest { + + private static final TestRule sEmptyRule = (statement, description) -> statement; + + // We have two sets of 9 rules below, for class rules and instance rules. + // - Ravenizer will inject 2 more rules of each kind. + // - Ravenizer will adjust their order, so even though we'll add two sets of class and instance + // rules with a MIN / MAX order, there will still be no duplicate in the order. + + private static final int EXPECTED_RULE_COUNT = 9 + 2; + + @ClassRule(order = Integer.MIN_VALUE) + public static final TestRule sRule01 = sEmptyRule; + + @ClassRule(order = Integer.MIN_VALUE + 1) + public static final TestRule sRule02 = sEmptyRule; + + @ClassRule(order = -10) + public static final TestRule sRule03 = sEmptyRule; + + @ClassRule(order = -1) + public static final TestRule sRule04 = sEmptyRule; + + @ClassRule(order = 0) + public static final TestRule sRule05 = sEmptyRule; + + @ClassRule(order = 1) + public static final TestRule sRule06 = sEmptyRule; + + @ClassRule(order = 10) + public static final TestRule sRule07 = sEmptyRule; + + @ClassRule(order = Integer.MAX_VALUE - 1) + public static final TestRule sRule08 = sEmptyRule; + + @ClassRule(order = Integer.MAX_VALUE) + public static final TestRule sRule09 = sEmptyRule; + + @Rule(order = Integer.MIN_VALUE) + public final TestRule mRule01 = sEmptyRule; + + @Rule(order = Integer.MIN_VALUE + 1) + public final TestRule mRule02 = sEmptyRule; + + @Rule(order = -10) + public final TestRule mRule03 = sEmptyRule; + + @Rule(order = -1) + public final TestRule mRule04 = sEmptyRule; + + @Rule(order = 0) + public final TestRule mRule05 = sEmptyRule; + + @Rule(order = 1) + public final TestRule mRule06 = sEmptyRule; + + @Rule(order = 10) + public final TestRule mRule07 = sEmptyRule; + + @Rule(order = Integer.MAX_VALUE - 1) + public final TestRule mRule08 = sEmptyRule; + + @Rule(order = Integer.MAX_VALUE) + public final TestRule mRule09 = sEmptyRule; + + private void checkRules(boolean classRule) { + final var anotClass = classRule ? ClassRule.class : Rule.class; + + final HashMap<Integer, Integer> ordersUsed = new HashMap<>(); + + for (var field : this.getClass().getDeclaredFields()) { + if (!field.isAnnotationPresent(anotClass)) { + continue; + } + final var anot = field.getAnnotation(anotClass); + final int order = classRule ? ((ClassRule) anot).order() : ((Rule) anot).order(); + + if (ordersUsed.containsKey(order)) { + fail("Detected duplicate order=" + order); + } + ordersUsed.put(order, 1); + } + assertEquals(EXPECTED_RULE_COUNT, ordersUsed.size()); + } + + @Test + public void testClassRules() { + Assume.assumeTrue(RavenwoodRule.isOnRavenwood()); + + checkRules(true); + } + + @Test + public void testInstanceRules() { + Assume.assumeTrue(RavenwoodRule.isOnRavenwood()); + + checkRules(false); + } +} diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTest.java index d952d07b3817..ae596b10848b 100644 --- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTest.java @@ -13,44 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.ravenwoodtest.coretest.methodvalidation; +package com.android.ravenwoodtest.bivalenttest.ravenizer; -import android.platform.test.ravenwood.RavenwoodRule; +import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.runner.AndroidJUnit4; - -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; /** - * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations. - * This class contains tests for this validator. + * Test to make sure when a test class inherits another test class, the base class's + * implicit rules are shadowed and won't be executed. + * + * ... But for now, we don't have a way to programmatically check it, so for now we need to + * check the log file manually. + * + * TODO: Implement the test. */ @RunWith(AndroidJUnit4.class) -public class RavenwoodTestMethodValidation_OkTest { - @Rule - public final RavenwoodRule mRavenwood = new RavenwoodRule(); - - @Before - public void setUp() { - } - - @Before - public void testSetUp() { - } - - @After - public void tearDown() { - } - - @After - public void testTearDown() { - } - +public class RavenwoodImplicitRuleShadowingTest extends RavenwoodImplicitRuleShadowingTestBase { @Test - public void testEmpty() { + public void testOkInSubClass() { } } diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTestBase.java index 5a3589dae43a..1ca97af632dd 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTestBase.java @@ -13,19 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.ravenwoodtest.bivalenttest.ravenizer; -package com.android.platform.test.ravenwood.nativesubstitution; +import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.android.ravenwood.common.JvmWorkaround; +import org.junit.Test; +import org.junit.runner.RunWith; -import java.io.FileDescriptor; - -public class ParcelFileDescriptor_host { - public static void setFdInt(FileDescriptor fd, int fdInt) { - JvmWorkaround.getInstance().setFdInt(fd, fdInt); - } - - public static int getFdInt(FileDescriptor fd) { - return JvmWorkaround.getInstance().getFdInt(fd); +/** + * A test class that's just inherited by RavenwoodImplicitRuleShadowingTest. + */ +@RunWith(AndroidJUnit4.class) +public abstract class RavenwoodImplicitRuleShadowingTestBase { + @Test + public void testOkInBaseClass() { } } diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithAndroidXRunnerTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithAndroidXRunnerTest.java new file mode 100644 index 000000000000..c042eb010558 --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithAndroidXRunnerTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ravenwoodtest.bivalenttest.ravenizer; + +import android.util.Log; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Make sure ravenwood's test runner works with {@link AndroidJUnit4}. + */ +@RunWith(AndroidJUnit4.class) +public class RavenwoodRunnerWithAndroidXRunnerTest { + public static final String TAG = "RavenwoodRunnerWithAndroidXRunnerTest"; + + private static final CallTracker sCallTracker = new CallTracker(); + + @BeforeClass + public static void beforeClass() { + sCallTracker.incrementMethodCallCount(); + } + + @Before + public void beforeTest() { + sCallTracker.incrementMethodCallCount(); + } + + @After + public void afterTest() { + sCallTracker.incrementMethodCallCount(); + } + + @Test + public void test1() { + sCallTracker.incrementMethodCallCount(); + } + + @Test + public void test2() { + sCallTracker.incrementMethodCallCount(); + } + + @AfterClass + public static void afterClass() { + Log.i(TAG, "afterClass called"); + + sCallTracker.assertCallsOrDie( + "beforeClass", 1, + "beforeTest", 2, + "afterTest", 2, + "test1", 1, + "test2", 1 + ); + } +} diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithJUnitParamsRunnerTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithJUnitParamsRunnerTest.java new file mode 100644 index 000000000000..2feb5ba9aa01 --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithJUnitParamsRunnerTest.java @@ -0,0 +1,79 @@ +/* + * 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.ravenwoodtest.bivalenttest.ravenizer; + +import android.util.Log; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Make sure ravenwood's test runner works with {@link AndroidJUnit4}. + */ +@RunWith(JUnitParamsRunner.class) +public class RavenwoodRunnerWithJUnitParamsRunnerTest { + public static final String TAG = "RavenwoodRunnerTest"; + + private static final CallTracker sCallTracker = new CallTracker(); + + @BeforeClass + public static void beforeClass() { + sCallTracker.incrementMethodCallCount(); + } + + @Before + public void beforeTest() { + sCallTracker.incrementMethodCallCount(); + } + + @After + public void afterTest() { + sCallTracker.incrementMethodCallCount(); + } + + @Test + public void testWithNoParams() { + sCallTracker.incrementMethodCallCount(); + } + + @Test + @Parameters({"foo", "bar"}) + public void testWithParams(String arg) { + sCallTracker.incrementMethodCallCount(); + } + + @AfterClass + public static void afterClass() { + Log.i(TAG, "afterClass called"); + + sCallTracker.assertCallsOrDie( + "beforeClass", 1, + "beforeTest", 3, + "afterTest", 3, + "testWithNoParams", 1, + "testWithParams", 2 + ); + } +} diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithParameterizedAndroidJunit4Test.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithParameterizedAndroidJunit4Test.java new file mode 100644 index 000000000000..7e3bc0fccd7f --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithParameterizedAndroidJunit4Test.java @@ -0,0 +1,93 @@ +/* + * 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.ravenwoodtest.bivalenttest.ravenizer; + +import android.util.Log; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + +import java.util.ArrayList; +import java.util.List; + +/** + * Make sure ravenwood's test runner works with {@link ParameterizedAndroidJunit4}. + */ +@RunWith(ParameterizedAndroidJunit4.class) +public class RavenwoodRunnerWithParameterizedAndroidJunit4Test { + public static final String TAG = "RavenwoodRunnerTest"; + + private static final CallTracker sCallTracker = new CallTracker(); + + private final String mParam; + + private static int sNumInsantiation = 0; + + public RavenwoodRunnerWithParameterizedAndroidJunit4Test(String param) { + mParam = param; + sNumInsantiation++; + } + + @BeforeClass + public static void beforeClass() { + // It seems like ParameterizedAndroidJunit4 calls the @BeforeTest / @AfterTest methods + // one time too many. + // With two parameters, this method should be called only twice, but it's actually + // called three times. + // So let's not check the number fo beforeClass calls. + } + + @Before + public void beforeTest() { + sCallTracker.incrementMethodCallCount(); + } + + @After + public void afterTest() { + sCallTracker.incrementMethodCallCount(); + } + + @Parameters + public static List<String> getParams() { + var params = new ArrayList<String>(); + params.add("foo"); + params.add("bar"); + return params; + } + + @Test + public void testWithParams() { + sCallTracker.incrementMethodCallCount(); + } + + @AfterClass + public static void afterClass() { + Log.i(TAG, "afterClass called"); + + sCallTracker.assertCallsOrDie( + "beforeTest", sNumInsantiation, + "afterTest", sNumInsantiation, + "testWithParams", sNumInsantiation + ); + } +} diff --git a/ravenwood/coretest/README.md b/ravenwood/coretest/README.md deleted file mode 100644 index b60bfbfcb6f4..000000000000 --- a/ravenwood/coretest/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Ravenwood core test - -This test contains (non-bivalent) tests for Ravenwood itself -- e.g. tests for the ravenwood rules.
\ No newline at end of file diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodTestRunnerValidationTest.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodTestRunnerValidationTest.java deleted file mode 100644 index f1e33cb686f1..000000000000 --- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodTestRunnerValidationTest.java +++ /dev/null @@ -1,53 +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.ravenwoodtest.coretest; - -import android.platform.test.ravenwood.RavenwoodRule; - -import androidx.test.runner.AndroidJUnit4; // Intentionally use the deprecated one. - -import org.junit.Assume; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.rules.RuleChain; -import org.junit.runner.RunWith; - -/** - * Test for the test runner validator in RavenwoodRule. - */ -@RunWith(AndroidJUnit4.class) -public class RavenwoodTestRunnerValidationTest { - // Note the following rules don't have a @Rule, because they need to be applied in a specific - // order. So we use a RuleChain instead. - private ExpectedException mThrown = ExpectedException.none(); - private final RavenwoodRule mRavenwood = new RavenwoodRule(); - - @Rule - public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood); - - public RavenwoodTestRunnerValidationTest() { - Assume.assumeTrue(RavenwoodRule._$RavenwoodPrivate.isOptionalValidationEnabled()); - // Because RavenwoodRule will throw this error before executing the test method, - // we can't do it in the test method itself. - // So instead, we initialize it here. - mThrown.expectMessage("Switch to androidx.test.ext.junit.runners.AndroidJUnit4"); - } - - @Test - public void testValidateTestRunner() { - } -} diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java deleted file mode 100644 index db95fad2a3ad..000000000000 --- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java +++ /dev/null @@ -1,51 +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.ravenwoodtest.coretest.methodvalidation; - -import android.platform.test.ravenwood.RavenwoodRule; - -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.rules.RuleChain; -import org.junit.runner.RunWith; - -/** - * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations. - * This class contains tests for this validator. - */ -@RunWith(AndroidJUnit4.class) -public class RavenwoodTestMethodValidation_Fail01_Test { - private ExpectedException mThrown = ExpectedException.none(); - private final RavenwoodRule mRavenwood = new RavenwoodRule(); - - @Rule - public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood); - - public RavenwoodTestMethodValidation_Fail01_Test() { - mThrown.expectMessage("Method setUp() doesn't have @Before"); - } - - @SuppressWarnings("JUnit4SetUpNotRun") - public void setUp() { - } - - @Test - public void testEmpty() { - } -} diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java deleted file mode 100644 index ddc66c73a7c0..000000000000 --- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java +++ /dev/null @@ -1,51 +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.ravenwoodtest.coretest.methodvalidation; - -import android.platform.test.ravenwood.RavenwoodRule; - -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.rules.RuleChain; -import org.junit.runner.RunWith; - -/** - * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations. - * This class contains tests for this validator. - */ -@RunWith(AndroidJUnit4.class) -public class RavenwoodTestMethodValidation_Fail02_Test { - private ExpectedException mThrown = ExpectedException.none(); - private final RavenwoodRule mRavenwood = new RavenwoodRule(); - - @Rule - public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood); - - public RavenwoodTestMethodValidation_Fail02_Test() { - mThrown.expectMessage("Method tearDown() doesn't have @After"); - } - - @SuppressWarnings("JUnit4TearDownNotRun") - public void tearDown() { - } - - @Test - public void testEmpty() { - } -} diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java deleted file mode 100644 index ec8e907dcdb3..000000000000 --- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java +++ /dev/null @@ -1,51 +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.ravenwoodtest.coretest.methodvalidation; - -import android.platform.test.ravenwood.RavenwoodRule; - -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.rules.RuleChain; -import org.junit.runner.RunWith; - -/** - * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations. - * This class contains tests for this validator. - */ -@RunWith(AndroidJUnit4.class) -public class RavenwoodTestMethodValidation_Fail03_Test { - private ExpectedException mThrown = ExpectedException.none(); - private final RavenwoodRule mRavenwood = new RavenwoodRule(); - - @Rule - public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood); - - public RavenwoodTestMethodValidation_Fail03_Test() { - mThrown.expectMessage("Method testFoo() doesn't have @Test"); - } - - @SuppressWarnings("JUnit4TestNotRun") - public void testFoo() { - } - - @Test - public void testEmpty() { - } -} diff --git a/ravenwood/empty-res/Android.bp b/ravenwood/empty-res/Android.bp new file mode 100644 index 000000000000..3af769067d35 --- /dev/null +++ b/ravenwood/empty-res/Android.bp @@ -0,0 +1,4 @@ +android_app { + name: "ravenwood-empty-res", + sdk_version: "current", +} diff --git a/ravenwood/empty-res/AndroidManifest.xml b/ravenwood/empty-res/AndroidManifest.xml new file mode 100644 index 000000000000..f73460b42213 --- /dev/null +++ b/ravenwood/empty-res/AndroidManifest.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.ravenwood.emptyres"> +</manifest> diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java new file mode 100644 index 000000000000..03600ad5511f --- /dev/null +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.platform.test.ravenwood; + +import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERSION_JAVA_SYSPROP; + +import android.os.Bundle; +import android.platform.test.ravenwood.RavenwoodAwareTestRunner.Order; +import android.platform.test.ravenwood.RavenwoodAwareTestRunner.Scope; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.ravenwood.common.RavenwoodCommonUtils; + +import org.junit.runner.Description; +import org.junit.runner.Runner; +import org.junit.runners.model.TestClass; + +/** + * Provide hook points created by {@link RavenwoodAwareTestRunner}. + */ +public class RavenwoodAwareTestRunnerHook { + private static final String TAG = "RavenwoodAwareTestRunnerHook"; + + private RavenwoodAwareTestRunnerHook() { + } + + private static void log(String message) { + RavenwoodCommonUtils.log(TAG, message); + } + + public static void onRunnerInitializing(Runner runner, TestClass testClass) { + log("onRunnerStart: testClass=" + testClass + " runner=" + runner); + + // TODO: Move the initialization code to a better place. + + // This will let AndroidJUnit4 use the original runner. + System.setProperty("android.junit.runner", + "androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner"); + System.setProperty(RAVENWOOD_VERSION_JAVA_SYSPROP, "1"); + + // This is needed to make AndroidJUnit4ClassRunner happy. + InstrumentationRegistry.registerInstance(null, Bundle.EMPTY); + } + + public static boolean onBefore(RavenwoodAwareTestRunner runner, Description description, + Scope scope, Order order) { + log("onBefore: description=" + description + ", " + scope + ", " + order); + + // Class-level annotations are checked by the runner already, so we only check + // method-level annotations here. + if (scope == Scope.Instance && order == Order.First) { + if (!RavenwoodRule.shouldEnableOnRavenwood(description)) { + return false; + } + } + return true; + } + + public static void onAfter(RavenwoodAwareTestRunner runner, Description description, + Scope scope, Order order, Throwable th) { + log("onAfter: description=" + description + ", " + scope + ", " + order + ", " + th); + } +} diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java index 3ea4cb7fb69f..7b4c17390942 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -16,9 +16,9 @@ package android.platform.test.ravenwood; +import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_EMPTY_RESOURCES_APK; import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_RESOURCE_APK; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -39,24 +39,12 @@ import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.os.RuntimeInit; import com.android.server.LocalServices; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; import org.junit.runner.Description; -import org.junit.runner.RunWith; import org.junit.runners.model.Statement; import java.io.File; import java.io.IOException; import java.io.PrintStream; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.Executors; @@ -109,10 +97,6 @@ public class RavenwoodRuleImpl { android.os.Process.init$ravenwood(rule.mUid, rule.mPid); android.os.Binder.init$ravenwood(); -// android.os.SystemProperties.init$ravenwood( -// rule.mSystemProperties.getValues(), -// rule.mSystemProperties.getKeyReadablePredicate(), -// rule.mSystemProperties.getKeyWritablePredicate()); setSystemProperties(rule.mSystemProperties); ServiceManager.init$ravenwood(); @@ -131,11 +115,12 @@ public class RavenwoodRuleImpl { // TODO This should be integrated into LoadedApk final Supplier<Resources> resourcesSupplier = () -> { - final var resApkFile = new File(RAVENWOOD_RESOURCE_APK).getAbsoluteFile(); + var resApkFile = new File(RAVENWOOD_RESOURCE_APK); + if (!resApkFile.isFile()) { + resApkFile = new File(RAVENWOOD_EMPTY_RESOURCES_APK); + } assertTrue(resApkFile.isFile()); - - final var res = resApkFile.getAbsolutePath(); - + final String res = resApkFile.getAbsolutePath(); final var emptyPaths = new String[0]; ResourcesManager.getInstance().initializeApplicationPaths(res, emptyPaths); @@ -243,102 +228,7 @@ public class RavenwoodRuleImpl { public static void validate(Statement base, Description description, boolean enableOptionalValidation) { - validateTestRunner(base, description, enableOptionalValidation); - validateTestAnnotations(base, description, enableOptionalValidation); - } - - private static void validateTestRunner(Statement base, Description description, - boolean shouldFail) { - final var testClass = description.getTestClass(); - final var runWith = testClass.getAnnotation(RunWith.class); - if (runWith == null) { - return; - } - - // Due to build dependencies, we can't directly refer to androidx classes here, - // so just check the class name instead. - if (runWith.value().getCanonicalName().equals("androidx.test.runner.AndroidJUnit4")) { - var message = "Test " + testClass.getCanonicalName() + " uses deprecated" - + " test runner androidx.test.runner.AndroidJUnit4." - + " Switch to androidx.test.ext.junit.runners.AndroidJUnit4."; - if (shouldFail) { - Assert.fail(message); - } else { - System.err.println("Warning: " + message); - } - } - } - - /** - * @return if a method has any of annotations. - */ - private static boolean hasAnyAnnotations(Method m, Class<? extends Annotation>... annotations) { - for (var anno : annotations) { - if (m.getAnnotation(anno) != null) { - return true; - } - } - return false; - } - - private static void validateTestAnnotations(Statement base, Description description, - boolean enableOptionalValidation) { - final var testClass = description.getTestClass(); - - final var message = new StringBuilder(); - - boolean hasErrors = false; - for (Method m : collectMethods(testClass)) { - if (Modifier.isPublic(m.getModifiers()) && m.getName().startsWith("test")) { - if (!hasAnyAnnotations(m, Test.class, Before.class, After.class, - BeforeClass.class, AfterClass.class)) { - message.append("\nMethod " + m.getName() + "() doesn't have @Test"); - hasErrors = true; - } - } - if ("setUp".equals(m.getName())) { - if (!hasAnyAnnotations(m, Before.class)) { - message.append("\nMethod " + m.getName() + "() doesn't have @Before"); - hasErrors = true; - } - if (!Modifier.isPublic(m.getModifiers())) { - message.append("\nMethod " + m.getName() + "() must be public"); - hasErrors = true; - } - } - if ("tearDown".equals(m.getName())) { - if (!hasAnyAnnotations(m, After.class)) { - message.append("\nMethod " + m.getName() + "() doesn't have @After"); - hasErrors = true; - } - if (!Modifier.isPublic(m.getModifiers())) { - message.append("\nMethod " + m.getName() + "() must be public"); - hasErrors = true; - } - } - } - assertFalse("Problem(s) detected in class " + testClass.getCanonicalName() + ":" - + message, hasErrors); - } - - /** - * Collect all (public or private or any) methods in a class, including inherited methods. - */ - private static List<Method> collectMethods(Class<?> clazz) { - var ret = new ArrayList<Method>(); - collectMethods(clazz, ret); - return ret; - } - - private static void collectMethods(Class<?> clazz, List<Method> result) { - // Class.getMethods() only return public methods, so we need to use getDeclaredMethods() - // instead, and recurse. - for (var m : clazz.getDeclaredMethods()) { - result.add(m); - } - if (clazz.getSuperclass() != null) { - collectMethods(clazz.getSuperclass(), result); - } + // Nothing to check, for now. } /** diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java new file mode 100644 index 000000000000..a4fa41af26e5 --- /dev/null +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java @@ -0,0 +1,355 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.platform.test.ravenwood; + +import static android.platform.test.ravenwood.RavenwoodRule.shouldRunCassOnRavenwood; + +import static com.android.ravenwood.common.RavenwoodCommonUtils.ensureIsPublicVoidMethod; +import static com.android.ravenwood.common.RavenwoodCommonUtils.isOnRavenwood; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; + +import com.android.ravenwood.common.RavenwoodCommonUtils; +import com.android.ravenwood.common.SneakyThrow; + +import org.junit.Assume; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runner.Runner; +import org.junit.runner.manipulation.Filter; +import org.junit.runner.manipulation.Filterable; +import org.junit.runner.manipulation.InvalidOrderingException; +import org.junit.runner.manipulation.NoTestsRemainException; +import org.junit.runner.manipulation.Orderable; +import org.junit.runner.manipulation.Orderer; +import org.junit.runner.manipulation.Sortable; +import org.junit.runner.manipulation.Sorter; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.model.Statement; +import org.junit.runners.model.TestClass; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.InvocationTargetException; + +/** + * A test runner used for Ravenwood. + * + * TODO: Handle ENABLE_PROBE_IGNORED + * + * It will delegate to another runner specified with {@link InnerRunner} + * (default = {@link BlockJUnit4ClassRunner}) with the following features. + * - Add a {@link RavenwoodAwareTestRunnerHook#onRunnerInitializing} hook, which is called before + * the inner runner gets a chance to run. This can be used to initialize stuff used by the + * inner runner. + * - Add hook points, which are handed by RavenwoodAwareTestRunnerHook, with help from + * the four test rules such as {@link #sImplicitClassMinRule}, which are also injected by + * the ravenizer tool. + * + * We use this runner to: + * - Initialize the bare minimum environmnet just to be enough to make the actual test runners + * happy. + * - Handle {@link android.platform.test.annotations.DisabledOnRavenwood}. + * + * This class is built such that it can also be used on a real device, but in that case + * it will basically just delegate to the inner wrapper, and won't do anything special. + * (no hooks, etc.) + */ +public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orderable { + private static final String TAG = "RavenwoodAwareTestRunner"; + + @Inherited + @Target({TYPE}) + @Retention(RetentionPolicy.RUNTIME) + public @interface InnerRunner { + Class<? extends Runner> value(); + } + + /** + * An annotation similar to JUnit's BeforeClass, but this gets executed before + * the inner runner is instantiated, and only on Ravenwood. + * It can be used to initialize what's needed by the inner runner. + */ + @Target({METHOD}) + @Retention(RetentionPolicy.RUNTIME) + public @interface RavenwoodTestRunnerInitializing { + } + + /** Scope of a hook. */ + public enum Scope { + Runner, + Class, + Instance, + } + + /** Order of a hook. */ + public enum Order { + First, + Last, + } + + // The following four rule instances will be injected to tests by the Ravenizer tool. + + public static final TestRule sImplicitClassMinRule = (base, description) -> + getCurrentRunner().updateStatement(base, description, Scope.Class, Order.First); + + public static final TestRule sImplicitClassMaxRule = (base, description) -> + getCurrentRunner().updateStatement(base, description, Scope.Class, Order.Last); + + public static final TestRule sImplicitInstMinRule = (base, description) -> + getCurrentRunner().updateStatement(base, description, Scope.Instance, Order.First); + + public static final TestRule sImplicitInstMaxRule = (base, description) -> + getCurrentRunner().updateStatement(base, description, Scope.Instance, Order.Last); + + public static final String IMPLICIT_CLASS_MIN_RULE_NAME = "sImplicitClassMinRule"; + public static final String IMPLICIT_CLASS_MAX_RULE_NAME = "sImplicitClassMaxRule"; + public static final String IMPLICIT_INST_MIN_RULE_NAME = "sImplicitInstMinRule"; + public static final String IMPLICIT_INST_MAX_RULE_NAME = "sImplicitInstMaxRule"; + + /** Keeps track of the runner on the current thread. */ + private static final ThreadLocal<RavenwoodAwareTestRunner> sCurrentRunner = new ThreadLocal<>(); + + private static RavenwoodAwareTestRunner getCurrentRunner() { + var runner = sCurrentRunner.get(); + if (runner == null) { + throw new RuntimeException("Current test runner not set!"); + } + return runner; + } + + private final TestClass mTestClsas; + private final Runner mRealRunner; + + /** Simple logging method. */ + private void log(String message) { + RavenwoodCommonUtils.log(TAG, "[" + getTestClass() + " @" + this + "] " + message); + } + + private Error logAndFail(String message, Throwable innerException) { + log(message); + log(" Exception=" + innerException); + throw new AssertionError(message, innerException); + } + + public TestClass getTestClass() { + return mTestClsas; + } + + /** + * Constructor. + */ + public RavenwoodAwareTestRunner(Class<?> testClass) { + mTestClsas = new TestClass(testClass); + + /* + * If the class has @DisabledOnRavenwood, then we'll delegate to ClassSkippingTestRunner, + * which simply skips it. + */ + if (isOnRavenwood() && !shouldRunCassOnRavenwood(mTestClsas.getJavaClass())) { + mRealRunner = new ClassSkippingTestRunner(mTestClsas); + return; + } + + // Find the real runner. + final Class<? extends Runner> realRunner; + final InnerRunner innerRunnerAnnotation = mTestClsas.getAnnotation(InnerRunner.class); + if (innerRunnerAnnotation != null) { + realRunner = innerRunnerAnnotation.value(); + } else { + // Default runner. + realRunner = BlockJUnit4ClassRunner.class; + } + + onRunnerInitializing(); + + try { + log("Initializing the inner runner: " + realRunner); + + mRealRunner = realRunner.getConstructor(Class.class).newInstance(testClass); + + } catch (InstantiationException | IllegalAccessException + | InvocationTargetException | NoSuchMethodException e) { + throw logAndFail("Failed to instantiate " + realRunner, e); + } + } + + /** + * Run the bare minimum setup to initialize the wrapped runner. + */ + // This method is called by the ctor, so never make it virtual. + private void onRunnerInitializing() { + if (!isOnRavenwood()) { + return; + } + + log("onRunnerInitializing"); + + RavenwoodAwareTestRunnerHook.onRunnerInitializing(this, mTestClsas); + + // Hook point to allow more customization. + runAnnotatedMethodsOnRavenwood(RavenwoodTestRunnerInitializing.class, null); + } + + private void runAnnotatedMethodsOnRavenwood(Class<? extends Annotation> annotationClass, + Object instance) { + if (!isOnRavenwood()) { + return; + } + log("runAnnotatedMethodsOnRavenwood() " + annotationClass.getName()); + + for (var method : getTestClass().getAnnotatedMethods(annotationClass)) { + ensureIsPublicVoidMethod(method.getMethod(), /* isStatic=*/ instance == null); + + var methodDesc = method.getDeclaringClass().getName() + "." + + method.getMethod().toString(); + try { + method.getMethod().invoke(instance); + } catch (IllegalAccessException | InvocationTargetException e) { + throw logAndFail("Caught exception while running method " + methodDesc, e); + } + } + } + + @Override + public Description getDescription() { + return mRealRunner.getDescription(); + } + + @Override + public void run(RunNotifier notifier) { + if (mRealRunner instanceof ClassSkippingTestRunner) { + mRealRunner.run(notifier); + return; + } + + sCurrentRunner.set(this); + try { + runWithHooks(getDescription(), Scope.Runner, Order.First, + () -> mRealRunner.run(notifier)); + } finally { + sCurrentRunner.remove(); + } + } + + @Override + public void filter(Filter filter) throws NoTestsRemainException { + if (mRealRunner instanceof Filterable r) { + r.filter(filter); + } + } + + @Override + public void order(Orderer orderer) throws InvalidOrderingException { + if (mRealRunner instanceof Orderable r) { + r.order(orderer); + } + } + + @Override + public void sort(Sorter sorter) { + if (mRealRunner instanceof Sortable r) { + r.sort(sorter); + } + } + + private Statement updateStatement(Statement base, Description description, Scope scope, + Order order) { + if (!isOnRavenwood()) { + return base; + } + return new Statement() { + @Override + public void evaluate() throws Throwable { + runWithHooks(description, scope, order, base); + } + }; + } + + private void runWithHooks(Description description, Scope scope, Order order, Runnable r) { + runWithHooks(description, scope, order, new Statement() { + @Override + public void evaluate() throws Throwable { + r.run(); + } + }); + } + + private void runWithHooks(Description description, Scope scope, Order order, Statement s) { + Throwable th = null; + if (isOnRavenwood()) { + Assume.assumeTrue( + RavenwoodAwareTestRunnerHook.onBefore(this, description, scope, order)); + } + try { + s.evaluate(); + } catch (Throwable t) { + th = t; + SneakyThrow.sneakyThrow(t); + } finally { + if (isOnRavenwood()) { + RavenwoodAwareTestRunnerHook.onAfter(this, description, scope, order, th); + } + } + } + + /** + * A runner that simply skips a class. It still has to support {@link Filterable} + * because otherwise the result still says "SKIPPED" even when it's not included in the + * filter. + */ + private static class ClassSkippingTestRunner extends Runner implements Filterable { + private final TestClass mTestClass; + private final Description mDescription; + private boolean mFilteredOut; + + ClassSkippingTestRunner(TestClass testClass) { + mTestClass = testClass; + mDescription = Description.createTestDescription( + testClass.getJavaClass(), testClass.getJavaClass().getSimpleName()); + mFilteredOut = false; + } + + @Override + public Description getDescription() { + return mDescription; + } + + @Override + public void run(RunNotifier notifier) { + if (mFilteredOut) { + return; + } + notifier.fireTestSuiteStarted(mDescription); + notifier.fireTestIgnored(mDescription); + notifier.fireTestSuiteFinished(mDescription); + } + + @Override + public void filter(Filter filter) throws NoTestsRemainException { + if (filter.shouldRun(mDescription)) { + mFilteredOut = false; + } else { + throw new NoTestsRemainException(); + } + } + } +} diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java index 74de444904ea..75faafb7fe58 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java @@ -60,10 +60,12 @@ public class RavenwoodRule implements TestRule { /** * When probing is enabled, all tests will be unconditionally run on Ravenwood to detect - * cases where a test is able to pass despite being marked as {@code IgnoreUnderRavenwood}. + * cases where a test is able to pass despite being marked as {@link DisabledOnRavenwood}. * * This is typically helpful for internal maintainers discovering tests that had previously * been ignored, but now have enough Ravenwood-supported functionality to be enabled. + * + * TODO: Rename it to a more descriptive name. */ static final boolean ENABLE_PROBE_IGNORED = "1".equals( System.getenv("RAVENWOOD_RUN_DISABLED_TESTS")); @@ -281,7 +283,7 @@ public class RavenwoodRule implements TestRule { * annotation, and an {@link EnabledOnRavenwood} annotation always takes precedence over * an {@link DisabledOnRavenwood} annotation. */ - static boolean shouldEnableOnRavenwood(Description description) { + public static boolean shouldEnableOnRavenwood(Description description) { // First, consult any method-level annotations if (description.isTest()) { // Stopgap for http://g/ravenwood/EPAD-N5ntxM @@ -300,20 +302,21 @@ public class RavenwoodRule implements TestRule { } // Otherwise, consult any class-level annotations - final var clazz = description.getTestClass(); + return shouldRunCassOnRavenwood(description.getTestClass()); + } + + public static boolean shouldRunCassOnRavenwood(Class<?> clazz) { if (clazz != null) { - if (description.getTestClass().getAnnotation(EnabledOnRavenwood.class) != null) { + if (clazz.getAnnotation(EnabledOnRavenwood.class) != null) { return true; } - if (description.getTestClass().getAnnotation(DisabledOnRavenwood.class) != null) { + if (clazz.getAnnotation(DisabledOnRavenwood.class) != null) { return false; } - if (description.getTestClass().getAnnotation(IgnoreUnderRavenwood.class) != null) { + if (clazz.getAnnotation(IgnoreUnderRavenwood.class) != null) { return false; } } - - // When no annotations have been requested, assume test should be included return true; } @@ -364,6 +367,7 @@ public class RavenwoodRule implements TestRule { commonPrologue(base, description); try { base.evaluate(); + RavenwoodRuleImpl.logTestRunner("finished", description); } catch (Throwable t) { RavenwoodRuleImpl.logTestRunner("failed", description); diff --git a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java new file mode 100644 index 000000000000..6b80e0cbf91e --- /dev/null +++ b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.platform.test.ravenwood; + +import android.platform.test.ravenwood.RavenwoodAwareTestRunner.Order; +import android.platform.test.ravenwood.RavenwoodAwareTestRunner.Scope; + +import org.junit.runner.Description; +import org.junit.runner.Runner; +import org.junit.runners.model.TestClass; + +/** + * Provide hook points created by {@link RavenwoodAwareTestRunner}. + */ +public class RavenwoodAwareTestRunnerHook { + private RavenwoodAwareTestRunnerHook() { + } + + /** + * Called when a runner starts, befre the inner runner gets a chance to run. + */ + public static void onRunnerInitializing(Runner runner, TestClass testClass) { + // No-op on a real device. + } + + public static boolean onBefore(RavenwoodAwareTestRunner runner, Description description, + Scope scope, Order order) { + // No-op on a real device. + return true; + } + + public static void onAfter(RavenwoodAwareTestRunner runner, Description description, + Scope scope, Order order, Throwable th) { + // No-op on a real device. + } +} diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java index 129802378cd4..7b5bc5aeb7b6 100644 --- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java +++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java @@ -21,6 +21,8 @@ import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.PrintStream; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Arrays; public class RavenwoodCommonUtils { @@ -42,12 +44,17 @@ public class RavenwoodCommonUtils { private static final boolean IS_ON_RAVENWOOD = RavenwoodDivergence.isOnRavenwood(); - private static final String RAVEWOOD_RUNTIME_PATH = getRavenwoodRuntimePathInternal(); + private static final String RAVENWOOD_RUNTIME_PATH = getRavenwoodRuntimePathInternal(); public static final String RAVENWOOD_SYSPROP = "ro.is_on_ravenwood"; public static final String RAVENWOOD_RESOURCE_APK = "ravenwood-res-apks/ravenwood-res.apk"; + public static final String RAVENWOOD_EMPTY_RESOURCES_APK = + RAVENWOOD_RUNTIME_PATH + "ravenwood-data/ravenwood-empty-res.apk"; + + public static final String RAVENWOOD_VERSION_JAVA_SYSPROP = "android.ravenwood.version"; + // @GuardedBy("sLock") private static boolean sIntegrityChecked = false; @@ -74,6 +81,18 @@ public class RavenwoodCommonUtils { return sEnableExtraRuntimeCheck; } + /** Simple logging method. */ + public static void log(String tag, String message) { + // Avoid using Android's Log class, which could be broken for various reasons. + // (e.g. the JNI file doesn't exist for whatever reason) + System.out.print(tag + ": " + message + "\n"); + } + + /** Simple logging method. */ + private void log(String tag, String format, Object... args) { + log(tag, String.format(format, args)); + } + /** * Load the main runtime JNI library. */ @@ -178,7 +197,7 @@ public class RavenwoodCommonUtils { */ public static String getRavenwoodRuntimePath() { ensureOnRavenwood(); - return RAVEWOOD_RUNTIME_PATH; + return RAVENWOOD_RUNTIME_PATH; } private static String getRavenwoodRuntimePathInternal() { @@ -233,4 +252,17 @@ public class RavenwoodCommonUtils { var is = new FileInputStream(fd); RavenwoodCommonUtils.closeQuietly(is); } + + public static void ensureIsPublicVoidMethod(Method method, boolean isStatic) { + var ok = Modifier.isPublic(method.getModifiers()) + && (Modifier.isStatic(method.getModifiers()) == isStatic) + && (method.getReturnType() == void.class); + if (ok) { + return; // okay + } + throw new AssertionError(String.format( + "Method %s.%s() expected to be public %svoid", + method.getDeclaringClass().getName(), method.getName(), + (isStatic ? "static " : ""))); + } } diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java index 706a055c9faf..f894b0e69a90 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java +++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java @@ -37,6 +37,9 @@ public class RavenwoodEnvironment_host { * Called from {@link RavenwoodEnvironment#ensureRavenwoodInitialized()}. */ public static void ensureRavenwoodInitialized() { + + // TODO Unify it with the initialization code in RavenwoodAwareTestRunnerHook. + synchronized (sInitializeLock) { if (sInitialized) { return; diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java index 7371d0a029b9..a5c0b54a8637 100644 --- a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java +++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java @@ -15,8 +15,8 @@ */ package android.system; +import com.android.ravenwood.RavenwoodRuntimeNative; import com.android.ravenwood.common.JvmWorkaround; -import com.android.ravenwood.common.RavenwoodRuntimeNative; import java.io.FileDescriptor; import java.io.FileInputStream; diff --git a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodJdkPatch.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodJdkPatch.java new file mode 100644 index 000000000000..96aed4b3401d --- /dev/null +++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodJdkPatch.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ravenwood; + +import com.android.ravenwood.common.JvmWorkaround; + +import java.io.FileDescriptor; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Class to host APIs that exist in libcore, but not in standard JRE. + */ +public class RavenwoodJdkPatch { + /** + * Implements FileDescriptor.getInt$() + */ + public static int getInt$(FileDescriptor fd) { + return JvmWorkaround.getInstance().getFdInt(fd); + } + + /** + * Implements FileDescriptor.setInt$(int) + */ + public static void setInt$(FileDescriptor fd, int rawFd) { + JvmWorkaround.getInstance().setFdInt(fd, rawFd); + } + + /** + * Implements LinkedHashMap.eldest() + */ + public static <K, V> Map.Entry<K, V> eldest(LinkedHashMap<K, V> map) { + final var it = map.entrySet().iterator(); + return it.hasNext() ? it.next() : null; + } +} diff --git a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java index beba83391652..0d8408c12033 100644 --- a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java +++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java @@ -13,11 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.ravenwood.common; +package com.android.ravenwood; import android.system.ErrnoException; import android.system.StructStat; +import com.android.ravenwood.common.JvmWorkaround; +import com.android.ravenwood.common.RavenwoodCommonUtils; + import java.io.FileDescriptor; /** diff --git a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java index ed5a587cdccc..ba89f71dde8a 100644 --- a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java +++ b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java @@ -34,11 +34,11 @@ public class VMRuntime { } public boolean is64Bit() { - return true; + return "amd64".equals(System.getProperty("os.arch")); } public static boolean is64BitAbi(String abi) { - return true; + return abi.contains("64"); } public Object newUnpaddedArray(Class<?> componentType, int minLength) { diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/io/IoUtils.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/io/IoUtils.java index 65c285e06bf8..2bd1ae89c824 100644 --- a/ravenwood/runtime-helper-src/libcore-fake/libcore/io/IoUtils.java +++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/io/IoUtils.java @@ -16,7 +16,13 @@ package libcore.io; +import android.system.ErrnoException; +import android.system.Os; + +import com.android.ravenwood.common.JvmWorkaround; + import java.io.File; +import java.io.FileDescriptor; import java.io.IOException; import java.net.Socket; @@ -47,6 +53,13 @@ public final class IoUtils { } } + public static void closeQuietly(FileDescriptor fd) { + try { + Os.close(fd); + } catch (ErrnoException ignored) { + } + } + public static void deleteContents(File dir) throws IOException { File[] files = dir.listFiles(); if (files != null) { @@ -58,4 +71,17 @@ public final class IoUtils { } } } + + /** + * FD owners currently unsupported under Ravenwood; ignored + */ + public static void setFdOwner(FileDescriptor fd, Object owner) { + } + + /** + * FD owners currently unsupported under Ravenwood; return FD directly + */ + public static int acquireRawFd(FileDescriptor fd) { + return JvmWorkaround.getInstance().getFdInt(fd); + } } diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java index 14b5a4f0c1e0..4e7dc5d6264f 100644 --- a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java +++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java @@ -15,7 +15,7 @@ */ package libcore.util; -import com.android.ravenwood.common.RavenwoodRuntimeNative; +import com.android.ravenwood.RavenwoodRuntimeNative; import java.lang.ref.Cleaner; import java.lang.ref.Reference; diff --git a/ravenwood/runtime-jni/ravenwood_runtime.cpp b/ravenwood/runtime-jni/ravenwood_runtime.cpp index c8049281bc53..f5cb019f4e7e 100644 --- a/ravenwood/runtime-jni/ravenwood_runtime.cpp +++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp @@ -245,7 +245,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) g_StructStat = findClass(env, "android/system/StructStat"); g_StructTimespecClass = findClass(env, "android/system/StructTimespec"); - jint res = jniRegisterNativeMethods(env, "com/android/ravenwood/common/RavenwoodRuntimeNative", + jint res = jniRegisterNativeMethods(env, "com/android/ravenwood/RavenwoodRuntimeNative", sMethods, NELEM(sMethods)); if (res < 0) { return res; diff --git a/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java b/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java index 044239f06297..b3d3963270ee 100644 --- a/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java +++ b/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java @@ -16,6 +16,7 @@ package com.android.ravenwoodtest.servicestest; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -62,7 +63,9 @@ public class RavenwoodServicesTest { final SerialManager service = (SerialManager) mRavenwood.getContext().getSystemService(Context.SERIAL_SERVICE); final String[] ports = service.getSerialPorts(); - assertEquals(0, ports.length); + final String[] refPorts = mRavenwood.getContext().getResources().getStringArray( + com.android.internal.R.array.config_serialPorts); + assertArrayEquals(refPorts, ports); } @Test diff --git a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt index 5cffdeccbacf..d8366c58c50d 100644 --- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt @@ -99,6 +99,7 @@ android.util.SparseDoubleArray android.util.SparseIntArray android.util.SparseLongArray android.util.SparseSetArray +android.util.StateSet android.util.StringBuilderPrinter android.util.TeeWriter android.util.TimeUtils @@ -222,9 +223,11 @@ android.content.res.ApkAssets android.content.res.AssetFileDescriptor android.content.res.AssetManager android.content.res.AssetManager$Builder +android.content.res.ColorStateList android.content.res.ConfigurationBoundResourceCache android.content.res.Configuration android.content.res.CompatibilityInfo +android.content.res.ComplexColor android.content.res.ConstantState android.content.res.DrawableCache android.content.res.Element diff --git a/ravenwood/texts/ravenwood-framework-policies.txt b/ravenwood/texts/ravenwood-framework-policies.txt index 2d49128ae292..d962c8232bf7 100644 --- a/ravenwood/texts/ravenwood-framework-policies.txt +++ b/ravenwood/texts/ravenwood-framework-policies.txt @@ -17,6 +17,13 @@ class :r keepclass rename com/.*/nano/ devicenano/ rename android/.*/nano/ devicenano/ +# Support APIs not available in standard JRE +class java.io.FileDescriptor keep + method getInt$ ()I @com.android.ravenwood.RavenwoodJdkPatch.getInt$ + method setInt$ (I)V @com.android.ravenwood.RavenwoodJdkPatch.setInt$ +class java.util.LinkedHashMap keep + method eldest ()Ljava/util/Map$Entry; @com.android.ravenwood.RavenwoodJdkPatch.eldest + # Exported to Mainline modules; cannot use annotations class com.android.internal.util.FastXmlSerializer keepclass class com.android.internal.util.FileRotator keepclass diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Exceptions.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Exceptions.kt new file mode 100644 index 000000000000..3a7fab39e4ac --- /dev/null +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Exceptions.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:Suppress("ktlint:standard:filename") + +package com.android.platform.test.ravenwood.ravenizer + +/** + * Use it for internal exception that really shouldn't happen. + */ +class RavenizerInternalException(message: String) : Exception(message) diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt index da9c7d97dac5..e92ef7216e25 100644 --- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt @@ -20,7 +20,7 @@ import com.android.hoststubgen.asm.ClassNodes import com.android.hoststubgen.asm.zipEntryNameToClassName import com.android.hoststubgen.executableName import com.android.hoststubgen.log -import com.android.platform.test.ravenwood.ravenizer.adapter.TestRunnerRewritingAdapter +import com.android.platform.test.ravenwood.ravenizer.adapter.RunnerRewritingAdapter import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassVisitor import org.objectweb.asm.ClassWriter @@ -177,7 +177,8 @@ class Ravenizer(val options: RavenizerOptions) { * Whether a class needs to be processed. This must be kept in sync with [processSingleClass]. */ private fun shouldProcessClass(classes: ClassNodes, classInternalName: String): Boolean { - return TestRunnerRewritingAdapter.shouldProcess(classes, classInternalName) + return !classInternalName.shouldByBypassed() + && RunnerRewritingAdapter.shouldProcess(classes, classInternalName) } private fun processSingleClass( @@ -191,6 +192,9 @@ class Ravenizer(val options: RavenizerOptions) { lateinit var data: ByteArray stats.totalConversionTime += log.vTime("Modify ${entry.name}") { + + val classInternalName = zipEntryNameToClassName(entry.name) + ?: throw RavenizerInternalException("Unexpected zip entry name: ${entry.name}") val flags = ClassWriter.COMPUTE_MAXS val cw = ClassWriter(flags) var outVisitor: ClassVisitor = cw @@ -201,7 +205,8 @@ class Ravenizer(val options: RavenizerOptions) { } // This must be kept in sync with shouldProcessClass. - outVisitor = TestRunnerRewritingAdapter(allClasses, outVisitor) + outVisitor = RunnerRewritingAdapter.maybeApply( + classInternalName, allClasses, outVisitor) cr.accept(outVisitor, ClassReader.EXPAND_FRAMES) diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt index 0018648998dc..e026e7ab3679 100644 --- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt @@ -15,18 +15,31 @@ */ package com.android.platform.test.ravenwood.ravenizer +import android.platform.test.ravenwood.RavenwoodAwareTestRunner import com.android.hoststubgen.asm.ClassNodes import com.android.hoststubgen.asm.findAnyAnnotation +import com.android.hoststubgen.asm.startsWithAny +import org.junit.rules.TestRule +import org.junit.runner.RunWith import org.objectweb.asm.Type -val junitTestMethodType = Type.getType(org.junit.Test::class.java) -val junitRunWithType = Type.getType(org.junit.runner.RunWith::class.java) +data class TypeHolder( + val clazz: Class<*>, +) { + val type = Type.getType(clazz) + val desc = type.descriptor + val descAsSet = setOf<String>(desc) + val internlName = type.internalName +} -val junitTestMethodDescriptor = junitTestMethodType.descriptor -val junitRunWithDescriptor = junitRunWithType.descriptor +val testAnotType = TypeHolder(org.junit.Test::class.java) +val ruleAnotType = TypeHolder(org.junit.Rule::class.java) +val classRuleAnotType = TypeHolder(org.junit.ClassRule::class.java) +val runWithAnotType = TypeHolder(RunWith::class.java) +val innerRunnerAnotType = TypeHolder(RavenwoodAwareTestRunner.InnerRunner::class.java) -val junitTestMethodDescriptors = setOf<String>(junitTestMethodDescriptor) -val junitRunWithDescriptors = setOf<String>(junitRunWithDescriptor) +val testRuleType = TypeHolder(TestRule::class.java) +val ravenwoodTestRunnerType = TypeHolder(RavenwoodAwareTestRunner::class.java) /** * Returns true, if a test looks like it's a test class which needs to be processed. @@ -39,16 +52,44 @@ fun isTestLookingClass(classes: ClassNodes, className: String): Boolean { val cn = classes.findClass(className) ?: return false - if (cn.findAnyAnnotation(junitRunWithDescriptors) != null) { + if (cn.findAnyAnnotation(runWithAnotType.descAsSet) != null) { return true } cn.methods?.forEach { method -> - if (method.findAnyAnnotation(junitTestMethodDescriptors) != null) { + if (method.findAnyAnnotation(testAnotType.descAsSet) != null) { return true } } + + // Check the super class. if (cn.superName == null) { return false } return isTestLookingClass(classes, cn.superName) } + +fun String.isRavenwoodClass(): Boolean { + return this.startsWithAny( + "com/android/hoststubgen/", + "android/platform/test/ravenwood", + "com/android/ravenwood/", + "com/android/platform/test/ravenwood/", + ) +} + +/** + * Classes that should never be modified. + */ +fun String.shouldByBypassed(): Boolean { + if (this.isRavenwoodClass()) { + return true + } + return this.startsWithAny( + "java/", // just in case... + "javax/", + "org/junit/", + "org/mockito/", + "kotlin/", + // TODO -- anything else? + ) +} diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt new file mode 100644 index 000000000000..25cad0213b72 --- /dev/null +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt @@ -0,0 +1,453 @@ +/* + * 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.platform.test.ravenwood.ravenizer.adapter + +import android.platform.test.ravenwood.RavenwoodAwareTestRunner +import com.android.hoststubgen.ClassParseException +import com.android.hoststubgen.asm.CLASS_INITIALIZER_DESC +import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME +import com.android.hoststubgen.asm.CTOR_NAME +import com.android.hoststubgen.asm.ClassNodes +import com.android.hoststubgen.asm.findAnnotationValueAsType +import com.android.hoststubgen.asm.findAnyAnnotation +import com.android.hoststubgen.asm.toHumanReadableClassName +import com.android.hoststubgen.log +import com.android.hoststubgen.visitors.OPCODE_VERSION +import com.android.platform.test.ravenwood.ravenizer.RavenizerInternalException +import com.android.platform.test.ravenwood.ravenizer.classRuleAnotType +import com.android.platform.test.ravenwood.ravenizer.isTestLookingClass +import com.android.platform.test.ravenwood.ravenizer.innerRunnerAnotType +import com.android.platform.test.ravenwood.ravenizer.ravenwoodTestRunnerType +import com.android.platform.test.ravenwood.ravenizer.ruleAnotType +import com.android.platform.test.ravenwood.ravenizer.runWithAnotType +import com.android.platform.test.ravenwood.ravenizer.testRuleType +import org.objectweb.asm.AnnotationVisitor +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.FieldVisitor +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Opcodes.ACC_FINAL +import org.objectweb.asm.Opcodes.ACC_PUBLIC +import org.objectweb.asm.Opcodes.ACC_STATIC +import org.objectweb.asm.commons.AdviceAdapter +import org.objectweb.asm.tree.ClassNode + +/** + * Class visitor to update the RunWith and inject some necessary rules. + * + * - Change the @RunWith(RavenwoodAwareTestRunner.class). + * - If the original class has a @RunWith(...), then change it to an @OrigRunWith(...). + * - Add RavenwoodAwareTestRunner's member rules as junit rules. + * - Update the order of the existing JUnit rules to make sure they don't use the MIN or MAX. + */ +class RunnerRewritingAdapter private constructor( + protected val classes: ClassNodes, + nextVisitor: ClassVisitor, +) : ClassVisitor(OPCODE_VERSION, nextVisitor) { + /** Arbitrary cut-off point when deciding whether to change the order or an existing rule.*/ + val RULE_ORDER_TWEAK_CUTOFF = 1973020500 + + /** Current class's internal name */ + lateinit var classInternalName: String + + /** [ClassNode] for the current class */ + lateinit var classNode: ClassNode + + /** True if this visitor is generating code. */ + var isGeneratingCode = false + + /** Run a [block] with [isGeneratingCode] set to true. */ + private inline fun <T> generateCode(block: () -> T): T { + isGeneratingCode = true + try { + return block() + } finally { + isGeneratingCode = false + } + } + + override fun visit( + version: Int, + access: Int, + name: String?, + signature: String?, + superName: String?, + interfaces: Array<out String>?, + ) { + classInternalName = name!! + classNode = classes.getClass(name) + if (!isTestLookingClass(classes, name)) { + throw RavenizerInternalException("This adapter shouldn't be used for non-test class") + } + super.visit(version, access, name, signature, superName, interfaces) + + generateCode { + injectRunWithAnnotation() + if (!classes.hasClassInitializer(classInternalName)) { + injectStaticInitializer() + } + injectRules() + } + } + + /** + * Remove the original @RunWith annotation. + */ + override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? { + if (!isGeneratingCode && runWithAnotType.desc == descriptor) { + return null + } + return super.visitAnnotation(descriptor, visible) + } + + override fun visitField( + access: Int, + name: String, + descriptor: String, + signature: String?, + value: Any? + ): FieldVisitor { + val fallback = super.visitField(access, name, descriptor, signature, value) + if (isGeneratingCode) { + return fallback + } + return FieldRuleOrderRewriter(name, fallback) + } + + /** Inject an empty <clinit>. The body will be injected by [visitMethod]. */ + private fun injectStaticInitializer() { + visitMethod( + Opcodes.ACC_PRIVATE or Opcodes.ACC_STATIC, + CLASS_INITIALIZER_NAME, + CLASS_INITIALIZER_DESC, + null, + null + )!!.let { mv -> + mv.visitCode() + mv.visitInsn(Opcodes.RETURN) + mv.visitMaxs(0, 0) + mv.visitEnd() + } + } + + /** + * Inject `@RunWith(RavenwoodAwareTestRunner.class)`. If the class already has + * a `@RunWith`, then change it to add a `@OrigRunWith`. + */ + private fun injectRunWithAnnotation() { + // Extract the original RunWith annotation and its value. + val runWith = classNode.findAnyAnnotation(runWithAnotType.descAsSet) + val runWithClass = runWith?.let { an -> + findAnnotationValueAsType(an, "value") + } + + if (runWith != null) { + if (runWithClass == ravenwoodTestRunnerType.type) { + // It already uses RavenwoodTestRunner. We'll just keep it, but we need to + // inject it again because the original one is removed by visitAnnotation(). + log.d("Class ${classInternalName.toHumanReadableClassName()}" + + " already uses RavenwoodTestRunner.") + visitAnnotation(runWithAnotType.desc, true)!!.let { av -> + av.visit("value", ravenwoodTestRunnerType) + av.visitEnd() + } + return + } + if (runWithClass == null) { + throw ClassParseException("@RunWith annotation doesn't have a property \"value\"" + + " in class ${classInternalName.toHumanReadableClassName()}") + } + + // Inject an @OrigRunWith. + visitAnnotation(innerRunnerAnotType.desc, true)!!.let { av -> + av.visit("value", runWithClass) + av.visitEnd() + } + } + + // Inject a @RunWith(RavenwoodAwareTestRunner.class). + visitAnnotation(runWithAnotType.desc, true)!!.let { av -> + av.visit("value", ravenwoodTestRunnerType.type) + av.visitEnd() + } + log.d("Processed ${classInternalName.toHumanReadableClassName()}") + } + + /* + Generate the fields and the ctor, which should looks like this: + + public static final org.junit.rules.TestRule sRavenwoodImplicitClassMinRule; + descriptor: Lorg/junit/rules/TestRule; + flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL + RuntimeVisibleAnnotations: + 0: #49(#50=I#51) + org.junit.ClassRule( + order=-2147483648 + ) + + public static final org.junit.rules.TestRule sRavenwoodImplicitClassMaxRule; + descriptor: Lorg/junit/rules/TestRule; + flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL + RuntimeVisibleAnnotations: + 0: #49(#50=I#52) + org.junit.ClassRule( + order=2147483647 + ) + + public final org.junit.rules.TestRule sRavenwoodImplicitInstanceMinRule; + descriptor: Lorg/junit/rules/TestRule; + flags: (0x0011) ACC_PUBLIC, ACC_FINAL + RuntimeVisibleAnnotations: + 0: #53(#50=I#51) + org.junit.Rule( + order=-2147483648 + ) + + public final org.junit.rules.TestRule sRavenwoodImplicitInstanceMaxRule; + descriptor: Lorg/junit/rules/TestRule; + flags: (0x0011) ACC_PUBLIC, ACC_FINAL + RuntimeVisibleAnnotations: + 0: #53(#50=I#52) + org.junit.Rule( + order=2147483647 + ) + */ + + val sRavenwood_ClassRuleMin = "sRavenwood_ClassRuleMin" + val sRavenwood_ClassRuleMax = "sRavenwood_ClassRuleMax" + val mRavenwood_InstRuleMin = "mRavenwood_InstRuleMin" + val mRavenwood_InstRuleMax = "mRavenwood_InstRuleMax" + + private fun injectRules() { + injectRule(sRavenwood_ClassRuleMin, true, Integer.MIN_VALUE) + injectRule(sRavenwood_ClassRuleMax, true, Integer.MAX_VALUE) + injectRule(mRavenwood_InstRuleMin, false, Integer.MIN_VALUE) + injectRule(mRavenwood_InstRuleMax, false, Integer.MAX_VALUE) + } + + private fun injectRule(fieldName: String, isStatic: Boolean, order: Int) { + visitField( + ACC_PUBLIC or ACC_FINAL or (if (isStatic) ACC_STATIC else 0), + fieldName, + testRuleType.desc, + null, + null, + ).let { fv -> + val anot = if (isStatic) { classRuleAnotType } else { ruleAnotType } + fv.visitAnnotation(anot.desc, true).let { + it.visit("order", order) + it.visitEnd() + } + fv.visitEnd() + } + } + + override fun visitMethod( + access: Int, + name: String, + descriptor: String, + signature: String?, + exceptions: Array<String>?, + ): MethodVisitor { + val next = super.visitMethod(access, name, descriptor, signature, exceptions) + if (name == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC) { + return ClassInitializerVisitor( + access, name, descriptor, signature, exceptions, next) + } + if (name == CTOR_NAME) { + return ConstructorVisitor( + access, name, descriptor, signature, exceptions, next) + } + return next + } + + /* + + static {}; + descriptor: ()V + flags: (0x0008) ACC_STATIC + Code: + stack=1, locals=0, args_size=0 + 0: getstatic #36 // Field android/platform/test/ravenwood/RavenwoodAwareTestRunner.RavenwoodImplicitClassMinRule:Lorg/junit/rules/TestRule; + 3: putstatic #39 // Field sRavenwoodImplicitClassMinRule:Lorg/junit/rules/TestRule; + 6: getstatic #42 // Field android/platform/test/ravenwood/RavenwoodAwareTestRunner.RavenwoodImplicitClassMaxRule:Lorg/junit/rules/TestRule; + 9: putstatic #45 // Field sRavenwoodImplicitClassMaxRule:Lorg/junit/rules/TestRule; + 12: return + LineNumberTable: + line 33: 0 + line 36: 6 + */ + private inner class ClassInitializerVisitor( + access: Int, + val name: String, + val descriptor: String, + signature: String?, + exceptions: Array<String>?, + next: MethodVisitor?, + ) : MethodVisitor(OPCODE_VERSION, next) { + override fun visitCode() { + visitFieldInsn(Opcodes.GETSTATIC, + ravenwoodTestRunnerType.internlName, + RavenwoodAwareTestRunner.IMPLICIT_CLASS_MIN_RULE_NAME, + testRuleType.desc + ) + visitFieldInsn(Opcodes.PUTSTATIC, + classInternalName, + sRavenwood_ClassRuleMin, + testRuleType.desc + ) + + visitFieldInsn(Opcodes.GETSTATIC, + ravenwoodTestRunnerType.internlName, + RavenwoodAwareTestRunner.IMPLICIT_CLASS_MAX_RULE_NAME, + testRuleType.desc + ) + visitFieldInsn(Opcodes.PUTSTATIC, + classInternalName, + sRavenwood_ClassRuleMax, + testRuleType.desc + ) + + super.visitCode() + } + } + + /* + public com.android.ravenwoodtest.bivalenttest.runnertest.RavenwoodRunnerTest(); + descriptor: ()V + flags: (0x0001) ACC_PUBLIC + Code: + stack=2, locals=1, args_size=1 + 0: aload_0 + 1: invokespecial #1 // Method java/lang/Object."<init>":()V + 4: aload_0 + 5: getstatic #7 // Field android/platform/test/ravenwood/RavenwoodAwareTestRunner.RavenwoodImplicitInstanceMinRule:Lorg/junit/rules/TestRule; + 8: putfield #13 // Field sRavenwoodImplicitInstanceMinRule:Lorg/junit/rules/TestRule; + 11: aload_0 + 12: getstatic #18 // Field android/platform/test/ravenwood/RavenwoodAwareTestRunner.RavenwoodImplicitInstanceMaxRule:Lorg/junit/rules/TestRule; + 15: putfield #21 // Field sRavenwoodImplicitInstanceMaxRule:Lorg/junit/rules/TestRule; + 18: return + LineNumberTable: + line 31: 0 + line 38: 4 + line 41: 11 + LocalVariableTable: + Start Length Slot Name Signature + 0 19 0 this Lcom/android/ravenwoodtest/bivalenttest/runnertest/RavenwoodRunnerTest; + */ + private inner class ConstructorVisitor( + access: Int, + name: String, + descriptor: String, + signature: String?, + exceptions: Array<String>?, + next: MethodVisitor?, + ) : AdviceAdapter(OPCODE_VERSION, next, ACC_ENUM, name, descriptor) { + override fun onMethodEnter() { + visitVarInsn(ALOAD, 0) + visitFieldInsn(Opcodes.GETSTATIC, + ravenwoodTestRunnerType.internlName, + RavenwoodAwareTestRunner.IMPLICIT_INST_MIN_RULE_NAME, + testRuleType.desc + ) + visitFieldInsn(Opcodes.PUTFIELD, + classInternalName, + mRavenwood_InstRuleMin, + testRuleType.desc + ) + + visitVarInsn(ALOAD, 0) + visitFieldInsn(Opcodes.GETSTATIC, + ravenwoodTestRunnerType.internlName, + RavenwoodAwareTestRunner.IMPLICIT_INST_MAX_RULE_NAME, + testRuleType.desc + ) + visitFieldInsn(Opcodes.PUTFIELD, + classInternalName, + mRavenwood_InstRuleMax, + testRuleType.desc + ) + } + } + + /** + * Rewrite "order" of the existing junit rules to make sure no rules use a MAX or MIN order. + * + * Currently, we do it a hacky way -- use an arbitrary cut-off point, and if the order + * is larger than that, decrement by 1, and if it's smaller than the negative cut-off point, + * increment it by 1. + * + * (or the arbitrary number is already used.... then we're unlucky, let's change the cut-off + * point.) + */ + private inner class FieldRuleOrderRewriter( + val fieldName: String, + next: FieldVisitor, + ) : FieldVisitor(OPCODE_VERSION, next) { + override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor { + val fallback = super.visitAnnotation(descriptor, visible) + if (descriptor != ruleAnotType.desc && descriptor != classRuleAnotType.desc) { + return fallback + } + return RuleOrderRewriter(fallback) + } + + private inner class RuleOrderRewriter( + next: AnnotationVisitor, + ) : AnnotationVisitor(OPCODE_VERSION, next) { + override fun visit(name: String?, origValue: Any?) { + if (name != "order") { + return super.visit(name, origValue) + } + var order = origValue as Int + if (order == RULE_ORDER_TWEAK_CUTOFF || order == -RULE_ORDER_TWEAK_CUTOFF) { + // Oops. If this happens, we'll need to change RULE_ORDER_TWEAK_CUTOFF. + // Or, we could scan all the rules in the target jar and find an unused number. + // Because rules propagate to subclasses, we'll at least check all the + // super classes of the current class. + throw RavenizerInternalException( + "OOPS: Field $classInternalName.$fieldName uses $order." + + " We can't update it.") + } + if (order > RULE_ORDER_TWEAK_CUTOFF) { + order -= 1 + } + if (order < -RULE_ORDER_TWEAK_CUTOFF) { + order += 1 + } + super.visit(name, order) + } + } + } + + companion object { + fun shouldProcess(classes: ClassNodes, className: String): Boolean { + return isTestLookingClass(classes, className) + } + + fun maybeApply( + className: String, + classes: ClassNodes, + nextVisitor: ClassVisitor, + ): ClassVisitor { + if (!shouldProcess(classes, className)) { + return nextVisitor + } else { + return RunnerRewritingAdapter(classes, nextVisitor) + } + } + } +}
\ No newline at end of file diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/TestRunnerRewritingAdapter.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/TestRunnerRewritingAdapter.kt deleted file mode 100644 index c5399084fb33..000000000000 --- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/TestRunnerRewritingAdapter.kt +++ /dev/null @@ -1,40 +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.platform.test.ravenwood.ravenizer.adapter - -import com.android.hoststubgen.asm.ClassNodes -import com.android.hoststubgen.visitors.OPCODE_VERSION -import com.android.platform.test.ravenwood.ravenizer.isTestLookingClass -import org.objectweb.asm.ClassVisitor - -/** - * Class visitor to rewrite the test runner for Ravenwood - * - * TODO: Implement it. - */ -class TestRunnerRewritingAdapter( - protected val classes: ClassNodes, - nextVisitor: ClassVisitor, -) : ClassVisitor(OPCODE_VERSION, nextVisitor) { - companion object { - /** - * Returns true if a target class is interesting to this adapter. - */ - fun shouldProcess(classes: ClassNodes, className: String): Boolean { - return isTestLookingClass(classes, className) - } - } -} diff --git a/services/Android.bp b/services/Android.bp index 0006455f41b0..653cd3c3b680 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -136,6 +136,7 @@ filegroup { ":services.searchui-sources", ":services.smartspace-sources", ":services.soundtrigger-sources", + ":services.supervision-sources", ":services.systemcaptions-sources", ":services.translation-sources", ":services.texttospeech-sources", @@ -237,6 +238,7 @@ system_java_library { "services.searchui", "services.smartspace", "services.soundtrigger", + "services.supervision", "services.systemcaptions", "services.translation", "services.texttospeech", diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index edb6390ea874..3224b27d5803 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -34,6 +34,7 @@ import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_ACCE import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK; +import static com.android.server.pm.UserManagerService.enforceCurrentUserIfVisibleBackgroundEnabled; import static com.android.window.flags.Flags.deleteCaptureDisplay; import android.accessibilityservice.AccessibilityGestureEvent; @@ -1100,11 +1101,14 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ if (svcConnTracingEnabled()) { logTraceSvcConn("performGlobalAction", "action=" + action); } + int currentUserId; synchronized (mLock) { if (!hasRightsToCurrentUserLocked()) { return false; } + currentUserId = mSystemSupport.getCurrentUserIdLocked(); } + enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId); final long identity = Binder.clearCallingIdentity(); try { return mSystemActionPerformer.performSystemAction(action); @@ -2750,6 +2754,11 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ @RequiresNoPermission @Override public void setAnimationScale(float scale) { + int currentUserId; + synchronized (mLock) { + currentUserId = mSystemSupport.getCurrentUserIdLocked(); + } + enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId); final long identity = Binder.clearCallingIdentity(); try { Settings.Global.putFloat( diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 0aa750e84a22..7cbb97e56b01 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -63,6 +63,7 @@ import static com.android.internal.accessibility.util.AccessibilityUtils.isUserS import static com.android.internal.util.FunctionalUtils.ignoreRemoteException; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import static com.android.server.accessibility.AccessibilityUserState.doesShortcutTargetsStringContain; +import static com.android.server.pm.UserManagerService.enforceCurrentUserIfVisibleBackgroundEnabled; import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import android.accessibilityservice.AccessibilityGestureEvent; @@ -309,6 +310,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private final PowerManager mPowerManager; + private final UserManager mUserManager; + private final WindowManagerInternal mWindowManagerService; private final AccessibilitySecurityPolicy mSecurityPolicy; @@ -507,6 +510,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub super(permissionEnforcer); mContext = context; mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + mUserManager = mContext.getSystemService(UserManager.class); mWindowManagerService = LocalServices.getService(WindowManagerInternal.class); mTraceManager = AccessibilityTraceManager.getInstance( mWindowManagerService.getAccessibilityController(), this, mLock); @@ -542,6 +546,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub super(PermissionEnforcer.fromContext(context)); mContext = context; mPowerManager = context.getSystemService(PowerManager.class); + mUserManager = context.getSystemService(UserManager.class); mWindowManagerService = LocalServices.getService(WindowManagerInternal.class); mTraceManager = AccessibilityTraceManager.getInstance( mWindowManagerService.getAccessibilityController(), this, mLock); @@ -1263,6 +1268,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub @EnforcePermission(MANAGE_ACCESSIBILITY) public void registerSystemAction(RemoteAction action, int actionId) { registerSystemAction_enforcePermission(); + int currentUserId; + synchronized (mLock) { + currentUserId = mCurrentUserId; + } + enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId); if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) { mTraceManager.logTrace(LOG_TAG + ".registerSystemAction", FLAGS_ACCESSIBILITY_MANAGER, "action=" + action + ";actionId=" + actionId); @@ -1279,6 +1289,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub @EnforcePermission(MANAGE_ACCESSIBILITY) public void unregisterSystemAction(int actionId) { unregisterSystemAction_enforcePermission(); + int currentUserId; + synchronized (mLock) { + currentUserId = mCurrentUserId; + } + enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId); if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) { mTraceManager.logTrace(LOG_TAG + ".unregisterSystemAction", FLAGS_ACCESSIBILITY_MANAGER, "actionId=" + actionId); @@ -1606,6 +1621,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub @EnforcePermission(STATUS_BAR_SERVICE) public void notifyAccessibilityButtonClicked(int displayId, String targetName) { notifyAccessibilityButtonClicked_enforcePermission(); + int currentUserId; + synchronized (mLock) { + currentUserId = mCurrentUserId; + } + enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId); if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) { mTraceManager.logTrace(LOG_TAG + ".notifyAccessibilityButtonClicked", FLAGS_ACCESSIBILITY_MANAGER, @@ -1634,6 +1654,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub @EnforcePermission(STATUS_BAR_SERVICE) public void notifyAccessibilityButtonVisibilityChanged(boolean shown) { notifyAccessibilityButtonVisibilityChanged_enforcePermission(); + int currentUserId; + synchronized (mLock) { + currentUserId = mCurrentUserId; + } + enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId); if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) { mTraceManager.logTrace(LOG_TAG + ".notifyAccessibilityButtonVisibilityChanged", FLAGS_ACCESSIBILITY_MANAGER, "shown=" + shown); @@ -1974,9 +1999,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub this, 0, oldUserState.mUserId)); } - // Announce user changes only if more that one exist. - UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); - final boolean announceNewUser = userManager.getUsers().size() > 1; + // Announce user changes only if more than one exist. + final boolean announceNewUser = mUserManager.getUsers().size() > 1; // The user changed. mCurrentUserId = userId; @@ -2017,10 +2041,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub synchronized (mLock) { AccessibilityUserState userState = getCurrentUserStateLocked(); if (userState.isHandlingAccessibilityEventsLocked()) { - UserManager userManager = (UserManager) mContext.getSystemService( - Context.USER_SERVICE); String message = mContext.getString(R.string.user_switched, - userManager.getUserInfo(mCurrentUserId).name); + mUserManager.getUserInfo(mCurrentUserId).name); AccessibilityEvent event = AccessibilityEvent.obtain( AccessibilityEvent.TYPE_ANNOUNCEMENT); event.getText().add(message); @@ -3185,6 +3207,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } + @GuardedBy("mLock") private void updateWindowsForAccessibilityCallbackLocked(AccessibilityUserState userState) { // We observe windows for accessibility only if there is at least // one bound service that can retrieve window content that specified @@ -3211,6 +3234,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub for (int i = 0; i < displays.size(); i++) { final Display display = displays.get(i); if (display != null) { + // When supporting visible background users, only track windows on the display + // assigned to the current user. The proxy displays are registered only to the + // current user. + if (UserManager.isVisibleBackgroundUsersEnabled() + && !mProxyManager.isProxyedDisplay(display.getDisplayId()) + && !mUmi.isUserVisible(mCurrentUserId, display.getDisplayId())) { + continue; + } if (observingWindows) { mA11yWindowManager.startTrackingWindows(display.getDisplayId(), mProxyManager.isProxyedDisplay(display.getDisplayId())); @@ -4799,6 +4830,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub throws RemoteException { registerProxyForDisplay_enforcePermission(); mSecurityPolicy.checkForAccessibilityPermissionOrRole(); + int currentUserId; + synchronized (mLock) { + currentUserId = mCurrentUserId; + } + enforceCurrentUserIfVisibleBackgroundEnabled(currentUserId); if (client == null) { return false; } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java index 6007bfd99e7b..9a81aa6cc506 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java @@ -594,10 +594,6 @@ public class AccessibilityWindowManager { private boolean windowMattersToAccessibilityLocked(AccessibilityWindow a11yWindow, int windowId, Region regionInScreen, Region unaccountedSpace) { - if (a11yWindow.ignoreRecentsAnimationForAccessibility()) { - return false; - } - if (a11yWindow.isFocused()) { return true; } diff --git a/services/core/Android.bp b/services/core/Android.bp index 89d796177da2..1b5b7e875db8 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -233,6 +233,7 @@ java_library_static { "stats_flags_lib", "core_os_flags_lib", "connectivity_flags_lib", + "device_config_service_flags_java", "dreams_flags_lib", "aconfig_new_storage_flags_lib", "powerstats_flags_lib", diff --git a/services/core/java/com/android/server/SerialService.java b/services/core/java/com/android/server/SerialService.java index 82c2038d8011..dbf144f0c63e 100644 --- a/services/core/java/com/android/server/SerialService.java +++ b/services/core/java/com/android/server/SerialService.java @@ -56,16 +56,11 @@ public class SerialService extends ISerialManager.Stub { } } - @android.ravenwood.annotation.RavenwoodReplace private static String[] getSerialPorts(Context context) { return context.getResources().getStringArray( com.android.internal.R.array.config_serialPorts); } - private static String[] getSerialPorts$ravenwood(Context context) { - return new String[0]; - } - public static class Lifecycle extends SystemService { private SerialService mService; diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index f1bdc05cce3c..d80b38e32b6c 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -12158,7 +12158,7 @@ public class ActivityManagerService extends IActivityManager.Stub opts.dumpProto = true; } else if ("--logstats".equals(opt)) { opts.mDumpAllocatorStats = true; - } else if ("-h".equals(opt)) { + } else if ("-h".equals(opt) || "--help".equals(opt)) { pw.println("meminfo dump options: [-a] [-d] [-c] [-s] [--oom] [process]"); pw.println(" -a: include all available information for each process."); pw.println(" -d: include dalvik details."); @@ -12168,10 +12168,13 @@ public class ActivityManagerService extends IActivityManager.Stub pw.println(" -p: dump also private dirty memory usage."); pw.println(" --oom: only show processes organized by oom adj."); pw.println(" --local: only collect details locally, don't call process."); + pw.println(" --logstats: dump native allocator stats to log"); pw.println(" --package: interpret process arg as package, dumping all"); pw.println(" processes that have loaded that package."); pw.println(" --checkin: dump data for a checkin"); pw.println(" --proto: dump data to proto"); + pw.println(" --logstats: log native allocator statistics."); + pw.println(" --unreachable: dump unreachable native memory with libmemunreachable."); pw.println("If [process] is specified it can be the name or "); pw.println("pid of a specific process to dump."); return; diff --git a/services/core/java/com/android/server/am/Android.bp b/services/core/java/com/android/server/am/Android.bp index 0294ffe6e151..ceba01e48961 100644 --- a/services/core/java/com/android/server/am/Android.bp +++ b/services/core/java/com/android/server/am/Android.bp @@ -9,3 +9,10 @@ java_aconfig_library { name: "am_flags_lib", aconfig_declarations: "am_flags", } + +java_aconfig_library { + name: "am_flags_host_lib", + host_supported: true, + libs: ["fake_device_config"], + aconfig_declarations: "am_flags", +} diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java index 4a7ad31cba3e..1b00cec90bc4 100644 --- a/services/core/java/com/android/server/am/AppStartInfoTracker.java +++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java @@ -16,7 +16,6 @@ package com.android.server.am; -import static android.app.ApplicationStartInfo.START_TIMESTAMP_LAUNCH; import static android.os.Process.THREAD_PRIORITY_BACKGROUND; import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; @@ -51,6 +50,8 @@ import android.util.proto.WireTypeMismatchException; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.ProcessMap; +import com.android.internal.os.Clock; +import com.android.internal.os.MonotonicClock; import com.android.server.IoThread; import com.android.server.ServiceThread; import com.android.server.SystemServiceManager; @@ -107,6 +108,16 @@ public final class AppStartInfoTracker { @VisibleForTesting boolean mEnabled = false; + /** + * Monotonic clock which does not reset on reboot. + * + * Time for offset is persisted along with records, see {@link #persistProcessStartInfo}. + * This does not follow the recommendation of {@link MonotonicClock} to persist on shutdown as + * it's ok in this case to lose any time change past the last persist as records added since + * then will be lost as well and the purpose of this clock is to keep records in order. + */ + @VisibleForTesting MonotonicClock mMonotonicClock = null; + /** Initialized in {@link #init} and read-only after that. */ @VisibleForTesting ActivityManagerService mService; @@ -203,6 +214,15 @@ public final class AppStartInfoTracker { IoThread.getHandler().post(() -> { loadExistingProcessStartInfo(); }); + + if (mMonotonicClock == null) { + // This should only happen if there are no persisted records, or if the records were + // persisted by a version without the monotonic clock. Either way, create a new clock + // with no offset. In the case of records with no monotonic time the value will default + // to 0 and all new records will correctly end up in front of them. + mMonotonicClock = new MonotonicClock(Clock.SYSTEM_CLOCK.elapsedRealtime(), + Clock.SYSTEM_CLOCK); + } } /** @@ -264,7 +284,7 @@ public final class AppStartInfoTracker { if (!mEnabled) { return; } - ApplicationStartInfo start = new ApplicationStartInfo(); + ApplicationStartInfo start = new ApplicationStartInfo(getMonotonicTime()); start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED); start.setIntent(intent); start.setStartType(ApplicationStartInfo.START_TYPE_UNSET); @@ -396,7 +416,7 @@ public final class AppStartInfoTracker { if (!mEnabled) { return; } - ApplicationStartInfo start = new ApplicationStartInfo(); + ApplicationStartInfo start = new ApplicationStartInfo(getMonotonicTime()); addBaseFieldsFromProcessRecord(start, app); start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED); start.addStartupTimestamp( @@ -422,7 +442,7 @@ public final class AppStartInfoTracker { if (!mEnabled) { return; } - ApplicationStartInfo start = new ApplicationStartInfo(); + ApplicationStartInfo start = new ApplicationStartInfo(getMonotonicTime()); addBaseFieldsFromProcessRecord(start, app); start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED); start.addStartupTimestamp( @@ -444,7 +464,7 @@ public final class AppStartInfoTracker { if (!mEnabled) { return; } - ApplicationStartInfo start = new ApplicationStartInfo(); + ApplicationStartInfo start = new ApplicationStartInfo(getMonotonicTime()); addBaseFieldsFromProcessRecord(start, app); start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED); start.addStartupTimestamp( @@ -461,7 +481,7 @@ public final class AppStartInfoTracker { if (!mEnabled) { return; } - ApplicationStartInfo start = new ApplicationStartInfo(); + ApplicationStartInfo start = new ApplicationStartInfo(getMonotonicTime()); addBaseFieldsFromProcessRecord(start, app); start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED); start.addStartupTimestamp( @@ -632,7 +652,8 @@ public final class AppStartInfoTracker { Collections.sort( list, (a, b) -> - Long.compare(getStartTimestamp(b), getStartTimestamp(a))); + Long.compare(b.getMonoticCreationTimeMs(), + a.getMonoticCreationTimeMs())); int size = list.size(); if (maxNum > 0) { size = Math.min(size, maxNum); @@ -898,6 +919,10 @@ public final class AppStartInfoTracker { case (int) AppsStartInfoProto.PACKAGES: loadPackagesFromProto(proto, next); break; + case (int) AppsStartInfoProto.MONOTONIC_TIME: + long monotonicTime = proto.readLong(AppsStartInfoProto.MONOTONIC_TIME); + mMonotonicClock = new MonotonicClock(monotonicTime, Clock.SYSTEM_CLOCK); + break; } } } catch (IOException | IllegalArgumentException | WireTypeMismatchException @@ -979,6 +1004,7 @@ public final class AppStartInfoTracker { mLastAppStartInfoPersistTimestamp = now; } } + proto.write(AppsStartInfoProto.MONOTONIC_TIME, getMonotonicTime()); if (succeeded) { proto.flush(); af.finishWrite(out); @@ -1099,13 +1125,12 @@ public final class AppStartInfoTracker { } } - /** Convenience method to obtain timestamp of beginning of start.*/ - private static long getStartTimestamp(ApplicationStartInfo startInfo) { - if (startInfo.getStartupTimestamps() == null - || !startInfo.getStartupTimestamps().containsKey(START_TIMESTAMP_LAUNCH)) { - return -1; + private long getMonotonicTime() { + if (mMonotonicClock == null) { + // This should never happen. Return 0 to not interfere with past or future records. + return 0; } - return startInfo.getStartupTimestamps().get(START_TIMESTAMP_LAUNCH); + return mMonotonicClock.monotonicTime(); } /** A container class of (@link android.app.ApplicationStartInfo) */ @@ -1143,7 +1168,7 @@ public final class AppStartInfoTracker { // Sort records so we can remove the least recent ones. Collections.sort(mInfos, (a, b) -> - Long.compare(getStartTimestamp(b), getStartTimestamp(a))); + Long.compare(b.getMonoticCreationTimeMs(), a.getMonoticCreationTimeMs())); // Remove records and trim list object back to size. mInfos.subList(0, mInfos.size() - getMaxCapacity()).clear(); @@ -1165,8 +1190,8 @@ public final class AppStartInfoTracker { long oldestTimeStamp = Long.MAX_VALUE; for (int i = 0; i < size; i++) { ApplicationStartInfo startInfo = mInfos.get(i); - if (getStartTimestamp(startInfo) < oldestTimeStamp) { - oldestTimeStamp = getStartTimestamp(startInfo); + if (startInfo.getMonoticCreationTimeMs() < oldestTimeStamp) { + oldestTimeStamp = startInfo.getMonoticCreationTimeMs(); oldestIndex = i; } } @@ -1176,7 +1201,7 @@ public final class AppStartInfoTracker { } mInfos.add(info); Collections.sort(mInfos, (a, b) -> - Long.compare(getStartTimestamp(b), getStartTimestamp(a))); + Long.compare(b.getMonoticCreationTimeMs(), a.getMonoticCreationTimeMs())); } /** @@ -1337,7 +1362,9 @@ public final class AppStartInfoTracker { mUid = proto.readInt(AppsStartInfoProto.Package.User.UID); break; case (int) AppsStartInfoProto.Package.User.APP_START_INFO: - ApplicationStartInfo info = new ApplicationStartInfo(); + // Create record with monotonic time 0 in case the persisted record does not + // have a create time. + ApplicationStartInfo info = new ApplicationStartInfo(0); info.readFromProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO); mInfos.add(info); break; diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index a7b2eb155558..8fe33d18152c 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -929,9 +929,9 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // For ordered broadcast, check if the receivers for the new broadcast is a superset // of those for the previous one as skipping and removing only one of them could result // in an inconsistent state. - if (testRecord.ordered || testRecord.prioritized) { + if (testRecord.ordered) { return containsAllReceivers(r, testRecord, recordsLookupCache); - } else if (testRecord.resultTo != null) { + } else if (testRecord.prioritized || testRecord.resultTo != null) { return testRecord.getDeliveryState(testIndex) == DELIVERY_DEFERRED ? r.containsReceiver(testRecord.receivers.get(testIndex)) : containsAllReceivers(r, testRecord, recordsLookupCache); diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 5d48d09adfbc..29373076c3b8 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -33,6 +33,7 @@ import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; +import com.android.providers.settings.Flags; import android.aconfigd.Aconfigd.StorageRequestMessage; import android.aconfigd.Aconfigd.StorageRequestMessages; @@ -51,6 +52,7 @@ import java.util.HashMap; import java.util.Map; import java.util.List; import java.util.ArrayList; +import java.util.Set; import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; /** @@ -457,6 +459,24 @@ public class SettingsToPropertiesMapper { } /** + * Send a request to aconfig storage to remove a flag local override. + * + * @param proto + * @param packageName the package of the flag + * @param flagName the name of the flag + */ + static void writeFlagOverrideRemovalRequest( + ProtoOutputStream proto, String packageName, String flagName) { + long msgsToken = proto.start(StorageRequestMessages.MSGS); + long msgToken = proto.start(StorageRequestMessage.REMOVE_LOCAL_OVERRIDE_MESSAGE); + proto.write(StorageRequestMessage.RemoveLocalOverrideMessage.PACKAGE_NAME, packageName); + proto.write(StorageRequestMessage.RemoveLocalOverrideMessage.FLAG_NAME, flagName); + proto.write(StorageRequestMessage.RemoveLocalOverrideMessage.REMOVE_ALL, false); + proto.end(msgToken); + proto.end(msgsToken); + } + + /** * deserialize a flag input proto stream and log * @param proto */ @@ -501,8 +521,15 @@ public class SettingsToPropertiesMapper { ProtoOutputStream requests = new ProtoOutputStream(); for (String flagName : props.getKeyset()) { String flagValue = props.getString(flagName, null); - if (flagName == null || flagValue == null) { - continue; + + if (Flags.syncLocalOverridesRemovalNewStorage()) { + if (flagName == null) { + continue; + } + } else { + if (flagName == null || flagValue == null) { + continue; + } } int idx = flagName.indexOf(":"); @@ -519,7 +546,13 @@ public class SettingsToPropertiesMapper { } String packageName = fullFlagName.substring(0, idx); String realFlagName = fullFlagName.substring(idx+1); - writeFlagOverrideRequest(requests, packageName, realFlagName, flagValue, true); + + if (Flags.syncLocalOverridesRemovalNewStorage() && flagValue == null) { + writeFlagOverrideRemovalRequest(requests, packageName, realFlagName); + } else { + writeFlagOverrideRequest(requests, packageName, realFlagName, flagValue, true); + } + ++num_requests; } diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java index e4c65bd2147d..8c5152fdb0d6 100644 --- a/services/core/java/com/android/server/app/GameManagerService.java +++ b/services/core/java/com/android/server/app/GameManagerService.java @@ -2307,7 +2307,7 @@ public final class GameManagerService extends IGameManagerService.Stub { return; } - final int userId = mContext.getUserId(); + final int userId = ActivityManager.getCurrentUser(); final boolean isNotGame = Arrays.stream(packages).noneMatch( p -> isPackageGame(p, userId)); synchronized (mUidObserverLock) { diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index ca907c57a858..1cf993521713 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -1714,6 +1714,10 @@ public class AudioDeviceBroker { sendIILMsg(MSG_IIL_BTLEAUDIO_TIMEOUT, SENDMSG_QUEUE, device, codec, address, delayMs); } + /*package*/ void setHearingAidTimeout(String address, int delayMs) { + sendLMsg(MSG_IL_BT_HEARING_AID_TIMEOUT, SENDMSG_QUEUE, address, delayMs); + } + /*package*/ void setAvrcpAbsoluteVolumeSupported(boolean supported) { synchronized (mDeviceStateLock) { mBtHelper.setAvrcpAbsoluteVolumeSupported(supported); @@ -1959,6 +1963,13 @@ public class AudioDeviceBroker { (String) msg.obj, msg.arg1, msg.arg2); } break; + case MSG_IL_BT_HEARING_AID_TIMEOUT: + // msg.obj == address of Hearing Aid device + synchronized (mDeviceStateLock) { + mDeviceInventory.onMakeHearingAidDeviceUnavailableNow( + (String) msg.obj); + } + break; case MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE: { final BtDeviceInfo btInfo = (BtDeviceInfo) msg.obj; final Pair<Integer, Boolean> codecAndChanged = mBtHelper.getCodecWithFallback( @@ -2234,6 +2245,7 @@ public class AudioDeviceBroker { private static final int MSG_L_SYNCHRONIZE_ADI_DEVICES_IN_INVENTORY = 58; private static final int MSG_IL_UPDATED_ADI_DEVICE_STATE = 59; private static final int MSG_L_SET_FORCE_BT_A2DP_USE_NO_MUTE = 60; + private static final int MSG_IL_BT_HEARING_AID_TIMEOUT = 61; private static boolean isMessageHandledUnderWakelock(int msgId) { switch(msgId) { @@ -2246,6 +2258,7 @@ public class AudioDeviceBroker { case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT: case MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT: case MSG_CHECK_MUTE_MUSIC: + case MSG_IL_BT_HEARING_AID_TIMEOUT: return true; default: return false; @@ -2330,6 +2343,7 @@ public class AudioDeviceBroker { case MSG_IL_BTA2DP_TIMEOUT: case MSG_IIL_BTLEAUDIO_TIMEOUT: case MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE: + case MSG_IL_BT_HEARING_AID_TIMEOUT: if (sLastDeviceConnectMsgTime >= time) { // add a little delay to make sure messages are ordered as expected time = sLastDeviceConnectMsgTime + 30; diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 8d8a54ea426d..a9bff8bf4bc3 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -1050,6 +1050,11 @@ public class AudioDeviceInventory { } } + /*package*/ void onMakeHearingAidDeviceUnavailableNow(String address) { + synchronized (mDevicesLock) { + makeHearingAidDeviceUnavailable(address); + } + } /** * Goes over all connected LE Audio devices in the provided group ID and @@ -1457,7 +1462,7 @@ public class AudioDeviceInventory { private int setDevicesRoleForCapturePreset(int capturePreset, int role, @NonNull List<AudioDeviceAttributes> devices) { return setDevicesRole(mAppliedPresetRoles, (p, r, d) -> { - return mAudioSystem.addDevicesRoleForCapturePreset(p, r, d); + return mAudioSystem.setDevicesRoleForCapturePreset(p, r, d); }, (p, r, d) -> { return mAudioSystem.clearDevicesRoleForCapturePreset(p, r); }, capturePreset, role, devices); @@ -1902,12 +1907,10 @@ public class AudioDeviceInventory { .set(MediaMetrics.Property.EVENT, "disconnectHearingAid") .record(); if (toRemove.size() > 0) { - /*final int delay = */ - checkSendBecomingNoisyIntentInt(DEVICE_OUT_HEARING_AID, + final int delay = checkSendBecomingNoisyIntentInt(DEVICE_OUT_HEARING_AID, AudioService.CONNECTION_STATE_DISCONNECTED, AudioSystem.DEVICE_NONE); toRemove.stream().forEach(deviceAddress -> - // TODO delay not used? - makeHearingAidDeviceUnavailable(deviceAddress /*, delay*/) + makeHearingAidDeviceUnavailableLater(deviceAddress, delay) ); } } @@ -2498,6 +2501,15 @@ public class AudioDeviceInventory { mDeviceBroker.postCheckCommunicationDeviceRemoval(ada); } + @GuardedBy("mDevicesLock") + private void makeHearingAidDeviceUnavailableLater( + String address, int delayMs) { + // the device will be made unavailable later, so consider it disconnected right away + mConnectedDevices.remove(DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address)); + // send the delayed message to make the device unavailable later + mDeviceBroker.setHearingAidTimeout(address, delayMs); + } + /** * Returns whether a device of type DEVICE_OUT_HEARING_AID is connected. * Visibility by APM plays no role diff --git a/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java b/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java index c5180afcce7d..5283eddd90fb 100644 --- a/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java +++ b/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java @@ -33,6 +33,7 @@ import static android.Manifest.permission.WRITE_SECURE_SETTINGS; import android.annotation.Nullable; import android.os.RemoteException; +import android.os.Trace; import android.os.UserHandle; import android.util.ArraySet; import android.util.IntArray; @@ -190,6 +191,7 @@ public class AudioServerPermissionProvider { mIsUpdateDeferred = true; return; } + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "audioserver_permission_update"); try { for (byte i = 0; i < PermissionEnum.ENUM_SIZE; i++) { var newPerms = getUidsHoldingPerm(i); @@ -203,6 +205,8 @@ public class AudioServerPermissionProvider { mDest = null; // We didn't necessarily finish mIsUpdateDeferred = true; + } finally { + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } } } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 53b04df4652e..14dca4e03b07 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -284,11 +284,16 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; import java.util.function.BooleanSupplier; import java.util.stream.Collectors; @@ -785,6 +790,8 @@ public class AudioService extends IAudioService.Stub private final BroadcastReceiver mReceiver = new AudioServiceBroadcastReceiver(); private final Executor mAudioServerLifecycleExecutor; + private final ConcurrentLinkedQueue<Future> mScheduledPermissionTasks = + new ConcurrentLinkedQueue(); private IMediaProjectionManager mProjectionService; // to validate projection token @@ -1092,7 +1099,8 @@ public class AudioService extends IAudioService.Stub public Lifecycle(Context context) { super(context); - var audioserverLifecycleExecutor = Executors.newSingleThreadExecutor(); + var audioserverLifecycleExecutor = Executors.newSingleThreadScheduledExecutor( + (Runnable r) -> new Thread(r, "audioserver_lifecycle")); var audioPolicyFacade = new DefaultAudioPolicyFacade(audioserverLifecycleExecutor); mService = new AudioService(context, AudioSystemAdapter.getDefaultAdapter(), @@ -1222,34 +1230,6 @@ public class AudioService extends IAudioService.Stub mBroadcastHandlerThread = new HandlerThread("AudioService Broadcast"); mBroadcastHandlerThread.start(); - // Listen to permission invalidations for the PermissionProvider - if (audioserverPermissions()) { - final Handler broadcastHandler = mBroadcastHandlerThread.getThreadHandler(); - mAudioSystem.listenForSystemPropertyChange(PermissionManager.CACHE_KEY_PACKAGE_INFO, - new Runnable() { - // Roughly chosen to be long enough to suppress the autocork behavior - // of the permission cache (50ms), and longer than the task could reasonably - // take, even with many packages and users, while not introducing visible - // permission leaks - since the app needs to restart, and trigger an action - // which requires permissions from audioserver before this delay. - // For RECORD_AUDIO, we are additionally protected by appops. - final long UPDATE_DELAY_MS = 110; - final AtomicLong scheduledUpdateTimestamp = new AtomicLong(0); - @Override - public void run() { - var currentTime = SystemClock.uptimeMillis(); - if (currentTime > scheduledUpdateTimestamp.get()) { - scheduledUpdateTimestamp.set(currentTime + UPDATE_DELAY_MS); - broadcastHandler.postAtTime( () -> - mAudioServerLifecycleExecutor.execute(mPermissionProvider - ::onPermissionStateChanged), - currentTime + UPDATE_DELAY_MS - ); - } - } - }); - } - mDeviceBroker = new AudioDeviceBroker(mContext, this, mAudioSystem); mIsSingleVolume = AudioSystem.isSingleVolume(context); @@ -1717,8 +1697,10 @@ public class AudioService extends IAudioService.Stub public void onSystemReady() { mSystemReady = true; + if (audioserverPermissions()) { + setupPermissionListener(); + } scheduleLoadSoundEffects(); - mDeviceBroker.onSystemReady(); if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_HDMI_CEC)) { @@ -10608,6 +10590,63 @@ public class AudioService extends IAudioService.Stub } } + /* Listen to permission invalidations for the PermissionProvider */ + private void setupPermissionListener() { + // Roughly chosen to be long enough to suppress the autocork behavior of the permission + // cache (50ms), while not introducing visible permission leaks - since the app needs to + // restart, and trigger an action which requires permissions from audioserver before this + // delay. For RECORD_AUDIO, we are additionally protected by appops. + final long UPDATE_DELAY_MS = 60; + // instanceof to simplify the construction requirements of AudioService for testing: no + // delayed execution during unit tests. + if (mAudioServerLifecycleExecutor instanceof ScheduledExecutorService exec) { + // We schedule and add from a this callback thread only (serially), so the task order on + // the serial executor matches the order on the task list. This list should almost + // always have only two elements, except in cases of serious system contention. + Runnable task = () -> mScheduledPermissionTasks.add(exec.schedule(() -> { + try { + // Clean up completed tasks before us to bound the queue length. Cancel any + // pending permission refresh tasks, after our own, since we are about to + // fulfill all of them. We must be the first non-completed task in the + // queue, since the execution order matches the queue order. Note, this + // task is the only writer on elements in the queue, and the task is + // serialized, so + // => no in-flight cancellation + // => exists at least one non-completed task (ourselves) + // => the queue is non-empty (only completed tasks removed) + final var iter = mScheduledPermissionTasks.iterator(); + while (iter.next().isDone()) { + iter.remove(); + } + // iter is on the first element which is not completed (us) + while (iter.hasNext()) { + if (!iter.next().cancel(false)) { + throw new AssertionError( + "Cancel should be infallible since we" + + "cancel from the executor"); + } + iter.remove(); + } + mPermissionProvider.onPermissionStateChanged(); + } catch (Exception e) { + // Handle executor routing exceptions to nowhere + Thread.getDefaultUncaughtExceptionHandler() + .uncaughtException(Thread.currentThread(), e); + } + }, + UPDATE_DELAY_MS, + TimeUnit.MILLISECONDS)); + mAudioSystem.listenForSystemPropertyChange( + PermissionManager.CACHE_KEY_PACKAGE_INFO, + task); + task.run(); + } else { + mAudioSystem.listenForSystemPropertyChange( + PermissionManager.CACHE_KEY_PACKAGE_INFO, + () -> mAudioServerLifecycleExecutor.execute( + mPermissionProvider::onPermissionStateChanged)); + } + } //========================================================================================== // Audio Focus @@ -10760,6 +10799,11 @@ public class AudioService extends IAudioService.Stub final long token = Binder.clearCallingIdentity(); try { + //TODO move inside HardeningEnforcer after refactor that moves permission checks + // in the blockFocusMethod + if (permissionOverridesCheck) { + mHardeningEnforcer.metricsLogFocusReq(/*blocked*/false, durationHint, uid); + } if (!permissionOverridesCheck && mHardeningEnforcer.blockFocusMethod(uid, HardeningEnforcer.METHOD_AUDIO_MANAGER_REQUEST_AUDIO_FOCUS, clientId, durationHint, callingPackageName, attributionTag, sdk)) { @@ -14663,6 +14707,20 @@ public class AudioService extends IAudioService.Stub return activeAssistantUids; } + @Override + /** @see AudioManager#permissionUpdateBarrier() */ + public void permissionUpdateBarrier() { + for (var x : List.copyOf(mScheduledPermissionTasks)) { + try { + x.get(); + } catch (CancellationException e) { + // Task completed + } catch (InterruptedException | ExecutionException e) { + Log.wtf(TAG, "Exception which should never occur", e); + } + } + } + List<String> getDeviceIdentityAddresses(AudioDeviceAttributes device) { return mDeviceBroker.getDeviceIdentityAddresses(device); } diff --git a/services/core/java/com/android/server/audio/HardeningEnforcer.java b/services/core/java/com/android/server/audio/HardeningEnforcer.java index 8ae04accb62f..3c509bca1b84 100644 --- a/services/core/java/com/android/server/audio/HardeningEnforcer.java +++ b/services/core/java/com/android/server/audio/HardeningEnforcer.java @@ -31,7 +31,9 @@ import android.os.Build; import android.os.UserHandle; import android.text.TextUtils; import android.util.Slog; +import android.util.SparseArray; +import com.android.modules.expresslog.Counter; import com.android.server.utils.EventLogger; import java.io.PrintWriter; @@ -55,6 +57,30 @@ public class HardeningEnforcer { final EventLogger mEventLogger = new EventLogger(LOG_NB_EVENTS, "Hardening enforcement"); + // capacity = 4 for each of the focus request types + static final SparseArray<String> METRIC_COUNTERS_FOCUS_DENIAL = new SparseArray<>(4); + static final SparseArray<String> METRIC_COUNTERS_FOCUS_GRANT = new SparseArray<>(4); + + static { + METRIC_COUNTERS_FOCUS_GRANT.put(AudioManager.AUDIOFOCUS_GAIN, + "media_audio.value_audio_focus_gain_granted"); + METRIC_COUNTERS_FOCUS_GRANT.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT, + "media_audio.value_audio_focus_gain_transient_granted"); + METRIC_COUNTERS_FOCUS_GRANT.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, + "media_audio.value_audio_focus_gain_transient_duck_granted"); + METRIC_COUNTERS_FOCUS_GRANT.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, + "media_audio.value_audio_focus_gain_transient_excl_granted"); + + METRIC_COUNTERS_FOCUS_DENIAL.put(AudioManager.AUDIOFOCUS_GAIN, + "media_audio.value_audio_focus_gain_appops_denial"); + METRIC_COUNTERS_FOCUS_DENIAL.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT, + "media_audio.value_audio_focus_gain_transient_appops_denial"); + METRIC_COUNTERS_FOCUS_DENIAL.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, + "media_audio.value_audio_focus_gain_transient_duck_appops_denial"); + METRIC_COUNTERS_FOCUS_DENIAL.put(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, + "media_audio.value_audio_focus_gain_transient_excl_appops_denial"); + } + /** * Matches calls from {@link AudioManager#setStreamVolume(int, int, int)} */ @@ -141,29 +167,49 @@ public class HardeningEnforcer { packageName = getPackNameForUid(callingUid); } + boolean blocked = true; if (noteOp(AppOpsManager.OP_TAKE_AUDIO_FOCUS, callingUid, packageName, attributionTag)) { if (DEBUG) { Slog.i(TAG, "blockFocusMethod pack:" + packageName + " NOT blocking"); } - return false; + blocked = false; } else if (targetSdk < Build.VERSION_CODES.VANILLA_ICE_CREAM) { if (DEBUG) { Slog.i(TAG, "blockFocusMethod pack:" + packageName + " NOT blocking due to sdk=" + targetSdk); } + blocked = false; + } + + metricsLogFocusReq(blocked, durationHint, callingUid); + + if (!blocked) { return false; } String errorMssg = "Focus request DENIED for uid:" + callingUid + " clientId:" + clientId + " req:" + durationHint + " procState:" + mActivityManager.getUidProcessState(callingUid); - - // TODO metrics mEventLogger.enqueueAndSlog(errorMssg, EventLogger.Event.ALOGI, TAG); return true; } + /*package*/ void metricsLogFocusReq(boolean blocked, int focusReq, int callingUid) { + final String metricId = blocked ? METRIC_COUNTERS_FOCUS_DENIAL.get(focusReq) + : METRIC_COUNTERS_FOCUS_GRANT.get(focusReq); + if (TextUtils.isEmpty(metricId)) { + Slog.e(TAG, "Bad string for focus metrics gain:" + focusReq + " blocked:" + blocked); + return; + } + try { + Counter.logIncrementWithUid(metricId, callingUid); + } catch (Exception e) { + Slog.e(TAG, "Counter error metricId:" + metricId + " for focus req:" + focusReq + + " from uid:" + callingUid, e); + } + } + private String getPackNameForUid(int uid) { final long token = Binder.clearCallingIdentity(); try { diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlResponseHandler.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlResponseHandler.java index cf677d541fb2..7b1186c9d4c7 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlResponseHandler.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/AidlResponseHandler.java @@ -80,13 +80,16 @@ public class AidlResponseHandler extends ISessionCallback.Stub { private final AuthSessionCoordinator mAuthSessionCoordinator; @NonNull private final AidlResponseHandlerCallback mAidlResponseHandlerCallback; + @NonNull + private final FaceUtils mBiometricUtils; public AidlResponseHandler(@NonNull Context context, @NonNull BiometricScheduler scheduler, int sensorId, int userId, @NonNull LockoutTracker lockoutTracker, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull AuthSessionCoordinator authSessionCoordinator, - @NonNull AidlResponseHandlerCallback aidlResponseHandlerCallback) { + @NonNull AidlResponseHandlerCallback aidlResponseHandlerCallback, + @NonNull FaceUtils biometricUtils) { mContext = context; mScheduler = scheduler; mSensorId = sensorId; @@ -95,6 +98,7 @@ public class AidlResponseHandler extends ISessionCallback.Stub { mLockoutResetDispatcher = lockoutResetDispatcher; mAuthSessionCoordinator = authSessionCoordinator; mAidlResponseHandlerCallback = aidlResponseHandlerCallback; + mBiometricUtils = biometricUtils; } @Override @@ -167,8 +171,7 @@ public class AidlResponseHandler extends ISessionCallback.Stub { } else { currentUserId = client.getTargetUserId(); } - final CharSequence name = FaceUtils.getInstance(mSensorId) - .getUniqueName(mContext, currentUserId); + final CharSequence name = mBiometricUtils.getUniqueName(mContext, currentUserId); final Face face = new Face(name, enrollmentId, mSensorId); handleResponse(FaceEnrollClient.class, (c) -> { diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java index 3eecc6de7450..d4ec573e1667 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java @@ -60,7 +60,6 @@ import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback; import com.android.server.biometrics.sensors.EnrollClient; import com.android.server.biometrics.sensors.face.FaceService; -import com.android.server.biometrics.sensors.face.FaceUtils; import java.io.IOException; import java.util.ArrayList; @@ -85,6 +84,7 @@ public class FaceEnrollClient extends EnrollClient<AidlSession> { private final int mMaxTemplatesPerUser; private final boolean mDebugConsent; private final @android.hardware.face.FaceEnrollOptions.EnrollReason int mEnrollReason; + private final BiometricUtils<Face> mBiometricUtils; private final ClientMonitorCallback mPreviewHandleDeleterCallback = new ClientMonitorCallback() { @@ -107,7 +107,8 @@ public class FaceEnrollClient extends EnrollClient<AidlSession> { @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, int maxTemplatesPerUser, boolean debugConsent, android.hardware.face.FaceEnrollOptions options, - @NonNull AuthenticationStateListeners authenticationStateListeners) { + @NonNull AuthenticationStateListeners authenticationStateListeners, + @NonNull BiometricUtils<Face> biometricUtils) { super(context, lazyDaemon, token, listener, userId, hardwareAuthToken, opPackageName, utils, timeoutSec, sensorId, false /* shouldVibrate */, logger, biometricContext, BiometricFaceConstants.reasonToMetric(options.getEnrollReason())); @@ -122,6 +123,7 @@ public class FaceEnrollClient extends EnrollClient<AidlSession> { mDebugConsent = debugConsent; mDisabledFeatures = disabledFeatures; mPreviewSurface = previewSurface; + mBiometricUtils = biometricUtils; Slog.w(TAG, "EnrollOptions " + android.hardware.face.FaceEnrollOptions.enrollReasonToString( options.getEnrollReason())); @@ -144,7 +146,7 @@ public class FaceEnrollClient extends EnrollClient<AidlSession> { @Override protected boolean hasReachedEnrollmentLimit() { - return FaceUtils.getInstance(getSensorId()).getBiometricsForUser(getContext(), + return mBiometricUtils.getBiometricsForUser(getContext(), getTargetUserId()).size() >= mMaxTemplatesPerUser; } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java index 964bf6cad63c..c27b7c483afc 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalCleanupClient.java @@ -30,7 +30,6 @@ import com.android.server.biometrics.sensors.BiometricUtils; import com.android.server.biometrics.sensors.InternalCleanupClient; import com.android.server.biometrics.sensors.InternalEnumerateClient; import com.android.server.biometrics.sensors.RemovalClient; -import com.android.server.biometrics.sensors.face.FaceUtils; import java.util.List; import java.util.Map; @@ -75,7 +74,7 @@ public class FaceInternalCleanupClient extends InternalCleanupClient<Face, AidlS @Override protected void onAddUnknownTemplate(int userId, @NonNull BiometricAuthenticator.Identifier identifier) { - FaceUtils.getInstance(getSensorId()).addBiometricForUser( + mBiometricUtils.addBiometricForUser( getContext(), getTargetUserId(), (Face) identifier); } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java index f0a418951505..bb213bfa79e6 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java @@ -72,7 +72,6 @@ import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.LockoutTracker; import com.android.server.biometrics.sensors.PerformanceTracker; import com.android.server.biometrics.sensors.SensorList; -import com.android.server.biometrics.sensors.face.FaceUtils; import com.android.server.biometrics.sensors.face.ServiceProvider; import com.android.server.biometrics.sensors.face.UsageStats; import com.android.server.biometrics.sensors.face.hidl.HidlToAidlSensorAdapter; @@ -326,8 +325,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { } if (Build.isDebuggable()) { - BiometricUtils<Face> utils = FaceUtils.getInstance( - mFaceSensors.keyAt(0)); + BiometricUtils<Face> utils = mFaceSensors.get( + mFaceSensors.keyAt(0)).getFaceUtilsInstance(); for (UserInfo user : UserManager.get(mContext).getAliveUsers()) { List<Face> enrollments = utils.getBiometricsForUser(mContext, user.id); Slog.d(getTag(), "Expecting enrollments for user " + user.id + ": " @@ -386,7 +385,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { new InvalidationRequesterClient<>(mContext, userId, sensorId, BiometricLogger.ofUnknown(mContext), mBiometricContext, - FaceUtils.getInstance(sensorId)); + mFaceSensors.get(sensorId).getFaceUtilsInstance()); scheduleForSensor(sensorId, client); }); } @@ -415,7 +414,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @NonNull @Override public List<Face> getEnrolledFaces(int sensorId, int userId) { - return FaceUtils.getInstance(sensorId).getBiometricsForUser(mContext, userId); + return mFaceSensors.get(sensorId).getFaceUtilsInstance() + .getBiometricsForUser(mContext, userId); } @Override @@ -497,13 +497,14 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { final FaceEnrollClient client = new FaceEnrollClient(mContext, mFaceSensors.get(sensorId).getLazySession(), token, new ClientMonitorCallbackConverter(receiver), userId, hardwareAuthToken, - opPackageName, id, FaceUtils.getInstance(sensorId), disabledFeatures, - ENROLL_TIMEOUT_SEC, previewSurface, sensorId, + opPackageName, id, mFaceSensors.get(sensorId).getFaceUtilsInstance(), + disabledFeatures, ENROLL_TIMEOUT_SEC, previewSurface, sensorId, createLogger(BiometricsProtoEnums.ACTION_ENROLL, BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, maxTemplatesPerUser, debugConsent, options, - mAuthenticationStateListeners); + mAuthenticationStateListeners, + mFaceSensors.get(sensorId).getFaceUtilsInstance()); scheduleForSensor(sensorId, client, mBiometricStateCallback); }); return id; @@ -615,7 +616,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @Override public void scheduleRemoveAll(int sensorId, @NonNull IBinder token, int userId, @NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) { - final List<Face> faces = FaceUtils.getInstance(sensorId) + final List<Face> faces = mFaceSensors.get(sensorId).getFaceUtilsInstance() .getBiometricsForUser(mContext, userId); final int[] faceIds = new int[faces.size()]; for (int i = 0; i < faces.size(); i++) { @@ -632,7 +633,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { final FaceRemovalClient client = new FaceRemovalClient(mContext, mFaceSensors.get(sensorId).getLazySession(), token, new ClientMonitorCallbackConverter(receiver), faceIds, userId, - opPackageName, FaceUtils.getInstance(sensorId), sensorId, + opPackageName, mFaceSensors.get(sensorId).getFaceUtilsInstance(), sensorId, createLogger(BiometricsProtoEnums.ACTION_REMOVE, BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), @@ -666,7 +667,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @NonNull IFaceServiceReceiver receiver, @NonNull String opPackageName) { mHandler.post(() -> { mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId); - final List<Face> faces = FaceUtils.getInstance(sensorId) + final List<Face> faces = mFaceSensors.get(sensorId).getFaceUtilsInstance() .getBiometricsForUser(mContext, userId); if (faces.isEmpty()) { Slog.w(getTag(), "Ignoring setFeature, no templates enrolled for user: " + userId); @@ -687,7 +688,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @NonNull ClientMonitorCallbackConverter callback, @NonNull String opPackageName) { mHandler.post(() -> { mFaceSensors.get(sensorId).scheduleFaceUpdateActiveUserClient(userId); - final List<Face> faces = FaceUtils.getInstance(sensorId) + final List<Face> faces = mFaceSensors.get(sensorId).getFaceUtilsInstance() .getBiometricsForUser(mContext, userId); if (faces.isEmpty()) { Slog.w(getTag(), "Ignoring getFeature, no templates enrolled for user: " + userId); @@ -727,7 +728,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, - FaceUtils.getInstance(sensorId), + mFaceSensors.get(sensorId).getFaceUtilsInstance(), mFaceSensors.get(sensorId).getAuthenticatorIds()); if (favorHalEnrollments) { client.setFavorHalEnrollments(); @@ -768,7 +769,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { JSONArray sets = new JSONArray(); for (UserInfo user : UserManager.get(mContext).getUsers()) { final int userId = user.getUserHandle().getIdentifier(); - final int c = FaceUtils.getInstance(sensorId) + final int c = mFaceSensors.get(sensorId).getFaceUtilsInstance() .getBiometricsForUser(mContext, userId).size(); JSONObject set = new JSONObject(); set.put("id", userId); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java index b0e7575689ba..6f9534993a3f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java @@ -158,7 +158,7 @@ public class Sensor { Slog.e(TAG, "Face sensor hardware unavailable."); mCurrentSession = null; } - }); + }, getFaceUtilsInstance()); return Sensor.this.getStartUserClient(resultController, sensorId, newUserId, provider); @@ -280,8 +280,7 @@ public class Sensor { final long userToken = proto.start(SensorStateProto.USER_STATES); proto.write(UserStateProto.USER_ID, userId); proto.write(UserStateProto.NUM_ENROLLED, - FaceUtils.getInstance(mSensorProperties.sensorId) - .getBiometricsForUser(mContext, userId).size()); + getFaceUtilsInstance().getBiometricsForUser(mContext, userId).size()); proto.end(userToken); } @@ -358,4 +357,8 @@ public class Sensor { Supplier<AidlSession> lazySession) { mLazySession = lazySession; } + + public FaceUtils getFaceUtilsInstance() { + return FaceUtils.getInstance(mSensorProperties.sensorId); + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapter.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapter.java index 9a4c29d7e978..444a6d18d27f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapter.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapter.java @@ -159,6 +159,11 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe } @Override + public FaceUtils getFaceUtilsInstance() { + return FaceUtils.getLegacyInstance(getSensorProperties().sensorId); + } + + @Override protected LockoutTracker getLockoutTracker(boolean forAuth) { return mLockoutTracker; } @@ -180,7 +185,8 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe mLockoutTracker, mLockoutResetDispatcher, mAuthSessionCoordinator, - mAidlResponseHandlerCallback); + mAidlResponseHandlerCallback, + getFaceUtilsInstance()); } private IBiometricsFace getIBiometricsFace() { @@ -247,8 +253,7 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe return new FaceUpdateActiveUserClient(getContext(), this::getIBiometricsFace, mUserStartedCallback, userId, TAG, getSensorProperties().sensorId, BiometricLogger.ofUnknown(getContext()), getBiometricContext(), - !FaceUtils.getInstance(getSensorProperties().sensorId).getBiometricsForUser( - getContext(), userId).isEmpty(), + !getFaceUtilsInstance().getBiometricsForUser(getContext(), userId).isEmpty(), getAuthenticatorIds()); } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlResponseHandler.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlResponseHandler.java index 6d1715f1d500..80b7cde3124c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlResponseHandler.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/AidlResponseHandler.java @@ -80,13 +80,16 @@ public class AidlResponseHandler extends ISessionCallback.Stub { private final AuthSessionCoordinator mAuthSessionCoordinator; @NonNull private final AidlResponseHandlerCallback mAidlResponseHandlerCallback; + @NonNull + private final FingerprintUtils mBiometricUtils; public AidlResponseHandler(@NonNull Context context, @NonNull BiometricScheduler scheduler, int sensorId, int userId, @NonNull LockoutTracker lockoutTracker, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull AuthSessionCoordinator authSessionCoordinator, - @NonNull AidlResponseHandlerCallback aidlResponseHandlerCallback) { + @NonNull AidlResponseHandlerCallback aidlResponseHandlerCallback, + @NonNull FingerprintUtils biometricUtils) { mContext = context; mScheduler = scheduler; mSensorId = sensorId; @@ -95,6 +98,7 @@ public class AidlResponseHandler extends ISessionCallback.Stub { mLockoutResetDispatcher = lockoutResetDispatcher; mAuthSessionCoordinator = authSessionCoordinator; mAidlResponseHandlerCallback = aidlResponseHandlerCallback; + mBiometricUtils = biometricUtils; } @Override @@ -158,8 +162,7 @@ public class AidlResponseHandler extends ISessionCallback.Stub { } else { currentUserId = client.getTargetUserId(); } - final CharSequence name = FingerprintUtils.getInstance(mSensorId) - .getUniqueName(mContext, currentUserId); + final CharSequence name = mBiometricUtils.getUniqueName(mContext, currentUserId); final Fingerprint fingerprint = new Fingerprint(name, currentUserId, enrollmentId, mSensorId); handleResponse(FingerprintEnrollClient.class, (c) -> { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java index 1fc517906c58..40b8a45beb36 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClient.java @@ -81,7 +81,7 @@ public class FingerprintInternalCleanupClient @Override protected void onAddUnknownTemplate(int userId, @NonNull BiometricAuthenticator.Identifier identifier) { - FingerprintUtils.getInstance(getSensorId()).addBiometricForUser( + mBiometricUtils.addBiometricForUser( getContext(), getTargetUserId(), (Fingerprint) identifier); } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java index 12baf00c1c4a..9edaa4e6d818 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java @@ -79,7 +79,6 @@ import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.LockoutTracker; import com.android.server.biometrics.sensors.PerformanceTracker; import com.android.server.biometrics.sensors.SensorList; -import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils; import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher; import com.android.server.biometrics.sensors.fingerprint.PowerPressHandler; import com.android.server.biometrics.sensors.fingerprint.ServiceProvider; @@ -354,8 +353,9 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi } if (Build.isDebuggable()) { - BiometricUtils<Fingerprint> utils = FingerprintUtils.getInstance( - mFingerprintSensors.keyAt(0)); + final int sensorId = mFingerprintSensors.keyAt(0); + final BiometricUtils<Fingerprint> utils = mFingerprintSensors.get(sensorId) + .getFingerprintUtilsInstance(); for (UserInfo user : UserManager.get(mContext).getAliveUsers()) { List<Fingerprint> enrollments = utils.getBiometricsForUser(mContext, user.id); Slog.d(getTag(), "Expecting enrollments for user " + user.id + ": " @@ -442,7 +442,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi new InvalidationRequesterClient<>(mContext, userId, sensorId, BiometricLogger.ofUnknown(mContext), mBiometricContext, - FingerprintUtils.getInstance(sensorId)); + mFingerprintSensors.get(sensorId).getFingerprintUtilsInstance()); scheduleForSensor(sensorId, client); }); } @@ -507,7 +507,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi final FingerprintEnrollClient client = new FingerprintEnrollClient(mContext, mFingerprintSensors.get(sensorId).getLazySession(), token, id, new ClientMonitorCallbackConverter(receiver), userId, hardwareAuthToken, - opPackageName, FingerprintUtils.getInstance(sensorId), + opPackageName, mFingerprintSensors.get(sensorId).getFingerprintUtilsInstance(), sensorId, createLogger(BiometricsProtoEnums.ACTION_ENROLL, BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, @@ -638,8 +638,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi public void scheduleRemoveAll(int sensorId, @NonNull IBinder token, @NonNull IFingerprintServiceReceiver receiver, int userId, @NonNull String opPackageName) { - final List<Fingerprint> fingers = FingerprintUtils.getInstance(sensorId) - .getBiometricsForUser(mContext, userId); + final List<Fingerprint> fingers = mFingerprintSensors.get(sensorId) + .getFingerprintUtilsInstance().getBiometricsForUser(mContext, userId); final int[] fingerIds = new int[fingers.size()]; for (int i = 0; i < fingers.size(); i++) { fingerIds[i] = fingers.get(i).getBiometricId(); @@ -655,11 +655,10 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi final FingerprintRemovalClient client = new FingerprintRemovalClient(mContext, mFingerprintSensors.get(sensorId).getLazySession(), token, new ClientMonitorCallbackConverter(receiver), fingerprintIds, userId, - opPackageName, FingerprintUtils.getInstance(sensorId), sensorId, - createLogger(BiometricsProtoEnums.ACTION_REMOVE, - BiometricsProtoEnums.CLIENT_UNKNOWN, - mAuthenticationStatsCollector), - mBiometricContext, + opPackageName, mFingerprintSensors.get(sensorId).getFingerprintUtilsInstance(), + sensorId, createLogger(BiometricsProtoEnums.ACTION_REMOVE, + BiometricsProtoEnums.CLIENT_UNKNOWN, + mAuthenticationStatsCollector), mBiometricContext, mFingerprintSensors.get(sensorId).getAuthenticatorIds()); scheduleForSensor(sensorId, client, mBiometricStateCallback); }); @@ -683,7 +682,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi BiometricsProtoEnums.CLIENT_UNKNOWN, mAuthenticationStatsCollector), mBiometricContext, - FingerprintUtils.getInstance(sensorId), + mFingerprintSensors.get(sensorId).getFingerprintUtilsInstance(), mFingerprintSensors.get(sensorId).getAuthenticatorIds()); if (favorHalEnrollments) { client.setFavorHalEnrollments(); @@ -706,14 +705,15 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @Override public void rename(int sensorId, int fingerId, int userId, @NonNull String name) { - FingerprintUtils.getInstance(sensorId) + mFingerprintSensors.get(sensorId).getFingerprintUtilsInstance() .renameBiometricForUser(mContext, userId, fingerId, name); } @NonNull @Override public List<Fingerprint> getEnrolledFingerprints(int sensorId, int userId) { - return FingerprintUtils.getInstance(sensorId).getBiometricsForUser(mContext, userId); + return mFingerprintSensors.get(sensorId).getFingerprintUtilsInstance() + .getBiometricsForUser(mContext, userId); } @Override @@ -842,7 +842,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi JSONArray sets = new JSONArray(); for (UserInfo user : UserManager.get(mContext).getUsers()) { final int userId = user.getUserHandle().getIdentifier(); - final int c = FingerprintUtils.getInstance(sensorId) + final int c = mFingerprintSensors.get(sensorId).getFingerprintUtilsInstance() .getBiometricsForUser(mContext, userId).size(); JSONObject set = new JSONObject(); set.put("id", userId); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java index 1c6dfe0f5b24..d12d7b2dc89a 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java @@ -170,7 +170,7 @@ public class Sensor { "Fingerprint sensor hardware unavailable."); mCurrentSession = null; } - }); + }, getFingerprintUtilsInstance()); return Sensor.this.getStartUserClient(resultController, sensorId, newUserId); @@ -187,7 +187,7 @@ public class Sensor { + halInterfaceVersion); mCurrentSession = new AidlSession(halInterfaceVersion, newSession, userIdStarted, resultController); - if (FingerprintUtils.getInstance(sensorId) + if (getFingerprintUtilsInstance() .isInvalidationInProgress(mContext, userIdStarted)) { Slog.w(TAG, "Scheduling unfinished invalidation request for " @@ -307,9 +307,8 @@ public class Sensor { final long userToken = proto.start(SensorStateProto.USER_STATES); proto.write(UserStateProto.USER_ID, userId); - proto.write(UserStateProto.NUM_ENROLLED, - FingerprintUtils.getInstance(mSensorProperties.sensorId) - .getBiometricsForUser(mContext, userId).size()); + proto.write(UserStateProto.NUM_ENROLLED, getFingerprintUtilsInstance() + .getBiometricsForUser(mContext, userId).size()); proto.end(userToken); } @@ -386,4 +385,8 @@ public class Sensor { public FingerprintProvider getProvider() { return mProvider; } + + public FingerprintUtils getFingerprintUtilsInstance() { + return FingerprintUtils.getInstance(mSensorProperties.sensorId); + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java index 3214b6d3363f..8f52d00ad830 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/HidlToAidlSensorAdapter.java @@ -148,6 +148,11 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe } @Override + public FingerprintUtils getFingerprintUtilsInstance() { + return FingerprintUtils.getLegacyInstance(getSensorProperties().sensorId); + } + + @Override @Nullable @VisibleForTesting protected AidlSession getSessionForUser(int userId) { @@ -186,7 +191,8 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe mLockoutTracker, mLockoutResetDispatcher, mAuthSessionCoordinator, - mAidlResponseHandlerCallback); + mAidlResponseHandlerCallback, + getFingerprintUtilsInstance()); } @VisibleForTesting IBiometricsFingerprint getIBiometricsFingerprint() { @@ -266,8 +272,7 @@ public class HidlToAidlSensorAdapter extends Sensor implements IHwBinder.DeathRe () -> getSession().getSession(), newUserId, TAG, getSensorProperties().sensorId, BiometricLogger.ofUnknown(getContext()), getBiometricContext(), () -> mCurrentUserId, - !FingerprintUtils.getInstance(getSensorProperties().sensorId) - .getBiometricsForUser(getContext(), + !getFingerprintUtilsInstance().getBiometricsForUser(getContext(), newUserId).isEmpty(), getAuthenticatorIds(), forceUpdateAuthenticatorIds, mUserStartedCallback); } 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 c31d1d8b271c..d909004e6381 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -1500,10 +1500,18 @@ public class DisplayModeDirector { } private void updateLayoutLimitedFrameRate(int displayId, @Nullable DisplayInfo info) { - Vote vote = info != null && info.layoutLimitedRefreshRate != null - ? Vote.forPhysicalRefreshRates(info.layoutLimitedRefreshRate.min, - info.layoutLimitedRefreshRate.max) : null; - mVotesStorage.updateVote(displayId, Vote.PRIORITY_LAYOUT_LIMITED_FRAME_RATE, vote); + Vote refreshRateVote = null; + Vote frameRateVote = null; + if (info != null && info.layoutLimitedRefreshRate != null) { + refreshRateVote = Vote.forPhysicalRefreshRates(info.layoutLimitedRefreshRate.min, + info.layoutLimitedRefreshRate.max); + frameRateVote = Vote.forRenderFrameRates(info.layoutLimitedRefreshRate.min, + info.layoutLimitedRefreshRate.max); + } + mVotesStorage.updateVote( + displayId, Vote.PRIORITY_LAYOUT_LIMITED_REFRESH_RATE, refreshRateVote); + mVotesStorage.updateVote( + displayId, Vote.PRIORITY_LAYOUT_LIMITED_FRAME_RATE, frameRateVote); } private void removeUserSettingDisplayPreferredSize(int displayId) { diff --git a/services/core/java/com/android/server/display/mode/Vote.java b/services/core/java/com/android/server/display/mode/Vote.java index 88ee044810db..459f9a6e8f13 100644 --- a/services/core/java/com/android/server/display/mode/Vote.java +++ b/services/core/java/com/android/server/display/mode/Vote.java @@ -110,37 +110,40 @@ interface Vote { int PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE = 13; // For concurrent displays we want to limit refresh rate on all displays - int PRIORITY_LAYOUT_LIMITED_FRAME_RATE = 14; + int PRIORITY_LAYOUT_LIMITED_REFRESH_RATE = 14; + + // For concurrent displays we want to limit refresh rate on all displays + int PRIORITY_LAYOUT_LIMITED_FRAME_RATE = 15; // For internal application to limit display modes to specific ids - int PRIORITY_SYSTEM_REQUESTED_MODES = 15; + int PRIORITY_SYSTEM_REQUESTED_MODES = 16; // PRIORITY_LOW_POWER_MODE_MODES limits display modes to specific refreshRate-vsync pairs if // Settings.Global.LOW_POWER_MODE is on. // Lower priority that PRIORITY_LOW_POWER_MODE_RENDER_RATE and if discarded (due to other // higher priority votes), render rate limit can still apply - int PRIORITY_LOW_POWER_MODE_MODES = 16; + int PRIORITY_LOW_POWER_MODE_MODES = 17; // PRIORITY_LOW_POWER_MODE_RENDER_RATE force the render frame rate to [0, 60HZ] if // Settings.Global.LOW_POWER_MODE is on. - int PRIORITY_LOW_POWER_MODE_RENDER_RATE = 17; + int PRIORITY_LOW_POWER_MODE_RENDER_RATE = 18; // PRIORITY_FLICKER_REFRESH_RATE_SWITCH votes for disabling refresh rate switching. If the // higher priority voters' result is a range, it will fix the rate to a single choice. // It's used to avoid refresh rate switches in certain conditions which may result in the // user seeing the display flickering when the switches occur. - int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 18; + int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 19; // Force display to [0, 60HZ] if skin temperature is at or above CRITICAL. - int PRIORITY_SKIN_TEMPERATURE = 19; + int PRIORITY_SKIN_TEMPERATURE = 20; // The proximity sensor needs the refresh rate to be locked in order to function, so this is // set to a high priority. - int PRIORITY_PROXIMITY = 20; + int PRIORITY_PROXIMITY = 21; // The Under-Display Fingerprint Sensor (UDFPS) needs the refresh rate to be locked in order // to function, so this needs to be the highest priority of all votes. - int PRIORITY_UDFPS = 21; + int PRIORITY_UDFPS = 22; @IntDef(prefix = { "PRIORITY_" }, value = { PRIORITY_DEFAULT_RENDER_FRAME_RATE, @@ -157,6 +160,7 @@ interface Vote { PRIORITY_SYNCHRONIZED_RENDER_FRAME_RATE, PRIORITY_LIMIT_MODE, PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE, + PRIORITY_LAYOUT_LIMITED_REFRESH_RATE, PRIORITY_LAYOUT_LIMITED_FRAME_RATE, PRIORITY_SYSTEM_REQUESTED_MODES, PRIORITY_LOW_POWER_MODE_MODES, @@ -283,6 +287,8 @@ interface Vote { return "PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE"; case PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE: return "PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE"; + case PRIORITY_LAYOUT_LIMITED_REFRESH_RATE: + return "PRIORITY_LAYOUT_LIMITED_REFRESH_RATE"; case PRIORITY_LAYOUT_LIMITED_FRAME_RATE: return "PRIORITY_LAYOUT_LIMITED_FRAME_RATE"; case PRIORITY_SYSTEM_REQUESTED_MODES: diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 7ce9ee60d421..2ad0d2a1b658 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -326,7 +326,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. * Figures out the target IME user ID for a given {@link Binder} IPC. * * @param callingProcessUserId the user ID of the calling process - * @return User ID to be used for this {@link Binder} call. + * @return the user ID to be used for this {@link Binder} call */ @GuardedBy("ImfLock.class") @UserIdInt @@ -336,6 +336,30 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } /** + * Figures out the targetIMuser for a given {@link Binder} IPC. In case + * {@code callingProcessUserId} is SYSTEM user, then it will return the owner of the display + * associated with the {@code client} passed as parameter. + * + * @param callingProcessUserId the user ID of the calling process + * @param client the input method client used to retrieve the user id in case + * {@code callingProcessUserId} is assigned to SYSTEM user + * @return the user ID to be used for this {@link Binder} call + */ + @GuardedBy("ImfLock.class") + @UserIdInt + @BinderThread + private int resolveImeUserIdLocked(@UserIdInt int callingProcessUserId, + @NonNull IInputMethodClient client) { + if (mConcurrentMultiUserModeEnabled + && callingProcessUserId == UserHandle.USER_SYSTEM) { + final var clientState = mClientController.getClient(client.asBinder()); + return mUserManagerInternal.getUserAssignedToDisplay( + clientState.mSelfReportedDisplayId); + } + return callingProcessUserId; + } + + /** * Figures out the target IME user ID associated with the given {@code displayId}. * * @param displayId the display ID to be queried about @@ -3069,7 +3093,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. synchronized (ImfLock.class) { final int uid = Binder.getCallingUid(); final int callingUserId = UserHandle.getUserId(uid); - final int userId = resolveImeUserIdLocked(callingUserId); + final int userId = resolveImeUserIdLocked(callingUserId, client); final boolean result = showSoftInputLocked(client, windowToken, statsToken, flags, lastClickToolType, resultReceiver, reason, uid, userId); // When ZeroJankProxy is enabled, the app has already received "true" as the return @@ -3515,7 +3539,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. synchronized (ImfLock.class) { final int uid = Binder.getCallingUid(); final int callingUserId = UserHandle.getUserId(uid); - final int userId = resolveImeUserIdLocked(callingUserId); + final int userId = resolveImeUserIdLocked(callingUserId, client); final boolean result = hideSoftInputLocked(client, windowToken, statsToken, flags, resultReceiver, reason, uid, userId); // When ZeroJankProxy is enabled, the app has already received "true" as the return diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java index d9e9e0021028..cf2cdc1500f8 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java @@ -33,6 +33,7 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.UserHandle; +import android.provider.Settings; import android.text.TextUtils; import android.util.Printer; import android.util.Slog; @@ -115,7 +116,11 @@ final class InputMethodMenuControllerNew { final var selectedImi = selectedIndex >= 0 ? items.get(selectedIndex).mImi : null; final var languageSettingsIntent = selectedImi != null ? selectedImi.createImeLanguageSettingsActivityIntent() : null; - final boolean hasLanguageSettingsButton = languageSettingsIntent != null; + final boolean isDeviceProvisioned = Settings.Global.getInt( + dialogWindowContext.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, + 0) != 0; + final boolean hasLanguageSettingsButton = languageSettingsIntent != null + && isDeviceProvisioned; if (hasLanguageSettingsButton) { final View buttonBar = contentView .requireViewById(com.android.internal.R.id.button_bar); diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java index 008746c0423c..4fa711262a08 100644 --- a/services/core/java/com/android/server/notification/GroupHelper.java +++ b/services/core/java/com/android/server/notification/GroupHelper.java @@ -122,8 +122,9 @@ public class GroupHelper { getNotificationShadeSections(); private static List<NotificationSectioner> getNotificationShadeSections() { + ArrayList<NotificationSectioner> sectionsList = new ArrayList<>(); if (android.service.notification.Flags.notificationClassification()) { - return List.of( + sectionsList.addAll(List.of( new NotificationSectioner("PromotionsSection", 0, (record) -> NotificationChannel.PROMOTIONS_ID.equals(record.getChannel().getId())), new NotificationSectioner("SocialSection", 0, (record) -> @@ -131,18 +132,36 @@ public class GroupHelper { new NotificationSectioner("NewsSection", 0, (record) -> NotificationChannel.NEWS_ID.equals(record.getChannel().getId())), new NotificationSectioner("RecsSection", 0, (record) -> - NotificationChannel.RECS_ID.equals(record.getChannel().getId())), - new NotificationSectioner("AlertingSection", 0, (record) -> - record.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT), - new NotificationSectioner("SilentSection", 1, (record) -> - record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT)); - } else { - return List.of( - new NotificationSectioner("AlertingSection", 0, (record) -> - record.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT), - new NotificationSectioner("SilentSection", 1, (record) -> - record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT)); + NotificationChannel.RECS_ID.equals(record.getChannel().getId())))); } + + if (Flags.notificationForceGroupConversations()) { + // add priority people section + sectionsList.add(new NotificationSectioner("PeopleSection(priority)", 1, (record) -> + record.isConversation() && record.getChannel().isImportantConversation())); + + if (android.app.Flags.sortSectionByTime()) { + // add single people (alerting) section + sectionsList.add(new NotificationSectioner("PeopleSection", 0, + NotificationRecord::isConversation)); + } else { + // add people alerting section + sectionsList.add(new NotificationSectioner("PeopleSection(alerting)", 1, (record) -> + record.isConversation() + && record.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT)); + // add people silent section + sectionsList.add(new NotificationSectioner("PeopleSection(silent)", 1, (record) -> + record.isConversation() + && record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT)); + } + } + + sectionsList.addAll(List.of( + new NotificationSectioner("AlertingSection", 0, (record) -> + record.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT), + new NotificationSectioner("SilentSection", 1, (record) -> + record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT))); + return sectionsList; } public GroupHelper(Context context, PackageManager packageManager, int autoGroupAtCount, @@ -830,25 +849,94 @@ public class GroupHelper { } } + // The list of notification operations required after the channel update final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>(); - final Set<FullyQualifiedGroupKey> oldGroups = - new HashSet<>(mAggregatedNotifications.keySet()); - for (FullyQualifiedGroupKey oldFullAggKey : oldGroups) { + // Check any already auto-grouped notifications that may need to be re-grouped + // after the channel update + notificationsToMove.addAll( + getAutogroupedNotificationsMoveOps(userId, pkgName, + notificationsToCheck)); + + // Check any ungrouped notifications that may need to be auto-grouped + // after the channel update + notificationsToMove.addAll( + getUngroupedNotificationsMoveOps(userId, pkgName, notificationsToCheck)); + + // Batch move to new section + if (!notificationsToMove.isEmpty()) { + moveNotificationsToNewSection(userId, pkgName, notificationsToMove); + } + } + } + + @GuardedBy("mAggregatedNotifications") + private List<NotificationMoveOp> getAutogroupedNotificationsMoveOps(int userId, String pkgName, + ArrayMap<String, NotificationRecord> notificationsToCheck) { + final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>(); + final Set<FullyQualifiedGroupKey> oldGroups = + new HashSet<>(mAggregatedNotifications.keySet()); + // Move auto-grouped updated notifications from the old groups to the new groups (section) + for (FullyQualifiedGroupKey oldFullAggKey : oldGroups) { + // Only check aggregate groups that match the same userId & packageName + if (pkgName.equals(oldFullAggKey.pkg) && userId == oldFullAggKey.userId) { + final ArrayMap<String, NotificationAttributes> notificationsInAggGroup = + mAggregatedNotifications.get(oldFullAggKey); + if (notificationsInAggGroup == null) { + continue; + } + + FullyQualifiedGroupKey newFullAggregateGroupKey = null; + for (String key : notificationsInAggGroup.keySet()) { + if (notificationsToCheck.get(key) != null) { + // check if section changes + NotificationSectioner sectioner = getSection(notificationsToCheck.get(key)); + if (sectioner == null) { + continue; + } + newFullAggregateGroupKey = new FullyQualifiedGroupKey(userId, pkgName, + sectioner); + if (!oldFullAggKey.equals(newFullAggregateGroupKey)) { + if (DEBUG) { + Log.i(TAG, "Change section on channel update: " + key); + } + notificationsToMove.add( + new NotificationMoveOp(notificationsToCheck.get(key), + oldFullAggKey, newFullAggregateGroupKey)); + notificationsToCheck.remove(key); + } + } + } + } + } + return notificationsToMove; + } + + @GuardedBy("mAggregatedNotifications") + private List<NotificationMoveOp> getUngroupedNotificationsMoveOps(int userId, String pkgName, + final ArrayMap<String, NotificationRecord> notificationsToCheck) { + final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>(); + // Move any remaining ungrouped updated notifications from the old ungrouped list + // to the new ungrouped section list, if necessary + if (!notificationsToCheck.isEmpty()) { + final Set<FullyQualifiedGroupKey> oldUngroupedSectionKeys = + new HashSet<>(mUngroupedAbuseNotifications.keySet()); + for (FullyQualifiedGroupKey oldFullAggKey : oldUngroupedSectionKeys) { // Only check aggregate groups that match the same userId & packageName if (pkgName.equals(oldFullAggKey.pkg) && userId == oldFullAggKey.userId) { - final ArrayMap<String, NotificationAttributes> notificationsInAggGroup = - mAggregatedNotifications.get(oldFullAggKey); - if (notificationsInAggGroup == null) { + final ArrayMap<String, NotificationAttributes> ungroupedOld = + mUngroupedAbuseNotifications.get(oldFullAggKey); + if (ungroupedOld == null) { continue; } FullyQualifiedGroupKey newFullAggregateGroupKey = null; - for (String key : notificationsInAggGroup.keySet()) { - if (notificationsToCheck.get(key) != null) { + final Set<String> ungroupedKeys = new HashSet<>(ungroupedOld.keySet()); + for (String key : ungroupedKeys) { + NotificationRecord record = notificationsToCheck.get(key); + if (record != null) { // check if section changes - NotificationSectioner sectioner = getSection( - notificationsToCheck.get(key)); + NotificationSectioner sectioner = getSection(record); if (sectioner == null) { continue; } @@ -856,41 +944,22 @@ public class GroupHelper { sectioner); if (!oldFullAggKey.equals(newFullAggregateGroupKey)) { if (DEBUG) { - Log.i(TAG, "Change section on channel update: " + key); + Log.i(TAG, "Change ungrouped section: " + key); } notificationsToMove.add( - new NotificationMoveOp(notificationsToCheck.get(key), - oldFullAggKey, newFullAggregateGroupKey)); + new NotificationMoveOp(record, oldFullAggKey, + newFullAggregateGroupKey)); + notificationsToCheck.remove(key); + //Remove from previous ungrouped list + ungroupedOld.remove(key); } } } - - if (newFullAggregateGroupKey != null) { - // Add any notifications left ungrouped to the new section - ArrayMap<String, NotificationAttributes> ungrouped = - mUngroupedAbuseNotifications.get(newFullAggregateGroupKey); - if (ungrouped != null) { - for (NotificationRecord r : notificationList) { - if (ungrouped.containsKey(r.getKey())) { - if (DEBUG) { - Log.i(TAG, "Add previously ungrouped: " + r); - } - notificationsToMove.add( - new NotificationMoveOp(r, null, newFullAggregateGroupKey)); - } - } - //Cleanup mUngroupedAbuseNotifications - mUngroupedAbuseNotifications.remove(newFullAggregateGroupKey); - } - } + mUngroupedAbuseNotifications.put(oldFullAggKey, ungroupedOld); } } - - // Batch move to new section - if (!notificationsToMove.isEmpty()) { - moveNotificationsToNewSection(userId, pkgName, notificationsToMove); - } } + return notificationsToMove; } @GuardedBy("mAggregatedNotifications") @@ -898,6 +967,7 @@ public class GroupHelper { final List<NotificationMoveOp> notificationsToMove) { record GroupUpdateOp(FullyQualifiedGroupKey groupKey, NotificationRecord record, boolean hasSummary) { } + // Bundled operations to apply to groups affected by the channel update ArrayMap<FullyQualifiedGroupKey, GroupUpdateOp> groupsToUpdate = new ArrayMap<>(); for (NotificationMoveOp moveOp: notificationsToMove) { @@ -923,35 +993,36 @@ public class GroupHelper { // Only add once, for triggering notification if (!groupsToUpdate.containsKey(oldFullAggregateGroupKey)) { groupsToUpdate.put(oldFullAggregateGroupKey, - new GroupUpdateOp(oldFullAggregateGroupKey, record, true)); + new GroupUpdateOp(oldFullAggregateGroupKey, record, true)); } } - // Add/update aggregate summary for new group + // Add moved notifications to the ungrouped list for new group and do grouping + // after all notifications have been handled if (newFullAggregateGroupKey != null) { final ArrayMap<String, NotificationAttributes> newAggregatedNotificationsAttrs = mAggregatedNotifications.getOrDefault(newFullAggregateGroupKey, new ArrayMap<>()); - boolean newGroupExists = !newAggregatedNotificationsAttrs.isEmpty(); - newAggregatedNotificationsAttrs.put(record.getKey(), - new NotificationAttributes(record.getFlags(), - record.getNotification().getSmallIcon(), - record.getNotification().color, - record.getNotification().visibility, - record.getNotification().getGroupAlertBehavior(), - record.getChannel().getId())); - mAggregatedNotifications.put(newFullAggregateGroupKey, - newAggregatedNotificationsAttrs); + boolean hasSummary = !newAggregatedNotificationsAttrs.isEmpty(); + ArrayMap<String, NotificationAttributes> ungrouped = + mUngroupedAbuseNotifications.getOrDefault(newFullAggregateGroupKey, + new ArrayMap<>()); + ungrouped.put(record.getKey(), new NotificationAttributes( + record.getFlags(), + record.getNotification().getSmallIcon(), + record.getNotification().color, + record.getNotification().visibility, + record.getNotification().getGroupAlertBehavior(), + record.getChannel().getId())); + mUngroupedAbuseNotifications.put(newFullAggregateGroupKey, ungrouped); + + record.setOverrideGroupKey(null); // Only add once, for triggering notification if (!groupsToUpdate.containsKey(newFullAggregateGroupKey)) { groupsToUpdate.put(newFullAggregateGroupKey, - new GroupUpdateOp(newFullAggregateGroupKey, record, newGroupExists)); + new GroupUpdateOp(newFullAggregateGroupKey, record, hasSummary)); } - - // Add notification to new group. do not request resort - record.setOverrideGroupKey(null); - mCallback.addAutoGroup(record.getKey(), newFullAggregateGroupKey.toString(), false); } } @@ -959,18 +1030,26 @@ public class GroupHelper { for (FullyQualifiedGroupKey groupKey : groupsToUpdate.keySet()) { final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs = mAggregatedNotifications.getOrDefault(groupKey, new ArrayMap<>()); - if (aggregatedNotificationsAttrs.isEmpty()) { - mCallback.removeAutoGroupSummary(userId, pkgName, groupKey.toString()); - mAggregatedNotifications.remove(groupKey); - } else { - NotificationRecord triggeringNotification = groupsToUpdate.get(groupKey).record; - boolean hasSummary = groupsToUpdate.get(groupKey).hasSummary; + final ArrayMap<String, NotificationAttributes> ungrouped = + mUngroupedAbuseNotifications.getOrDefault(groupKey, new ArrayMap<>()); + + NotificationRecord triggeringNotification = groupsToUpdate.get(groupKey).record; + boolean hasSummary = groupsToUpdate.get(groupKey).hasSummary; + //Group needs to be created/updated + if (ungrouped.size() >= mAutoGroupAtCount + || (hasSummary && !aggregatedNotificationsAttrs.isEmpty())) { NotificationSectioner sectioner = getSection(triggeringNotification); if (sectioner == null) { continue; } - updateAggregateAppGroup(groupKey, triggeringNotification.getKey(), hasSummary, - sectioner.mSummaryId); + aggregateUngroupedNotifications(groupKey, triggeringNotification.getKey(), + ungrouped, hasSummary, sectioner.mSummaryId); + } else { + // Remove empty groups + if (aggregatedNotificationsAttrs.isEmpty() && hasSummary) { + mCallback.removeAutoGroupSummary(userId, pkgName, groupKey.toString()); + mAggregatedNotifications.remove(groupKey); + } } } } @@ -1327,8 +1406,10 @@ public class GroupHelper { } private boolean isNotificationGroupable(final NotificationRecord record) { - if (record.isConversation()) { - return false; + if (!Flags.notificationForceGroupConversations()) { + if (record.isConversation()) { + return false; + } } Notification notification = record.getSbn().getNotification(); diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index ee3f48d9bd3f..6ff0e04bca77 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -485,7 +485,7 @@ public class ZenModeHelper { newConfig = mConfig.copy(); ZenRule rule = new ZenRule(); populateZenRule(pkg, automaticZenRule, rule, origin, /* isNew= */ true); - rule = maybeRestoreRemovedRule(newConfig, rule, automaticZenRule, origin); + rule = maybeRestoreRemovedRule(newConfig, pkg, rule, automaticZenRule, origin); newConfig.automaticRules.put(rule.id, rule); maybeReplaceDefaultRule(newConfig, automaticZenRule); @@ -498,7 +498,7 @@ public class ZenModeHelper { } @GuardedBy("mConfigLock") - private ZenRule maybeRestoreRemovedRule(ZenModeConfig config, ZenRule ruleToAdd, + private ZenRule maybeRestoreRemovedRule(ZenModeConfig config, String pkg, ZenRule ruleToAdd, AutomaticZenRule azrToAdd, @ConfigOrigin int origin) { if (!Flags.modesApi()) { return ruleToAdd; @@ -522,10 +522,18 @@ public class ZenModeHelper { if (origin != ORIGIN_APP) { return ruleToAdd; // Okay to create anew. } + if (Flags.modesUi()) { + if (!Objects.equals(ruleToRestore.pkg, pkg) + || !Objects.equals(ruleToRestore.component, azrToAdd.getOwner())) { + // Apps are not allowed to change the owner via updateAutomaticZenRule(). Thus, if + // they have to, delete+add is their only option. + return ruleToAdd; + } + } // "Preserve" the previous rule by considering the azrToAdd an update instead. // Only app-modifiable fields will actually be modified. - populateZenRule(ruleToRestore.pkg, azrToAdd, ruleToRestore, origin, /* isNew= */ false); + populateZenRule(pkg, azrToAdd, ruleToRestore, origin, /* isNew= */ false); return ruleToRestore; } @@ -757,7 +765,9 @@ public class ZenModeHelper { try { ApplicationInfo applicationInfo = mPm.getApplicationInfo(pkg, 0); rule.name = applicationInfo.loadLabel(mPm).toString(); - rule.iconResName = drawableResIdToResName(pkg, applicationInfo.icon); + if (!Flags.modesUi()) { + rule.iconResName = drawableResIdToResName(pkg, applicationInfo.icon); + } } catch (PackageManager.NameNotFoundException e) { // Should not happen, since it's the app calling us (?) Log.w(TAG, "Package not found for creating implicit zen rule"); @@ -1742,6 +1752,15 @@ public class ZenModeHelper { manualRulePolicy.overwrittenWith(automaticRule.zenPolicy); } } + + if (Flags.modesApi() && Flags.modesUi() + && config.version < ZenModeConfig.XML_VERSION_MODES_UI) { + // Clear icons from implicit rules. App icons are not suitable for some + // surfaces, so juse use a default (the user can select a different one). + if (ZenModeConfig.isImplicitRuleId(automaticRule.id)) { + automaticRule.iconResName = null; + } + } } } diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index 7265cff19077..aac2c404fd38 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -149,3 +149,10 @@ flag { description: "This flag enables forced auto-grouping singleton groups" bug: "336488844" } + +flag { + name: "notification_force_group_conversations" + namespace: "systemui" + description: "This flag enables forced auto-grouping conversations" + bug: "336488844" +} diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index ada6659b4a94..1317866af006 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -1415,8 +1415,13 @@ final class InstallPackageHelper { + " an sdk library <" + parsedPackage.getSdkLibraryName() + ">" + " without changing the versionMajor, but the" - + " targetSdkVersion or minSdkVersion has changed." - ); + + " targetSdkVersion or minSdkVersion has changed:" + + " Old targetSdkVersion: " + oldTargetSdk + + " new targetSdkVersion: " + newTargetSdk + + " Old minSdkVersion: " + oldMinSdk + + " new minSdkVersion: " + newMinSdk + + " versionMajor: " + newVersionMajor + ); } } } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 4a2d36736bae..a683a8c54849 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -25,6 +25,7 @@ import static android.content.pm.PackageManager.FEATURE_AUTOMOTIVE; import static android.content.pm.PackageManager.FEATURE_EMBEDDED; import static android.content.pm.PackageManager.FEATURE_LEANBACK; import static android.content.pm.PackageManager.FEATURE_WATCH; +import static android.os.UserHandle.USER_SYSTEM; import static android.os.UserManager.DEV_CREATE_OVERRIDE_PROPERTY; import static android.os.UserManager.DISALLOW_USER_SWITCH; import static android.os.UserManager.SYSTEM_USER_MODE_EMULATION_PROPERTY; @@ -2518,6 +2519,38 @@ public class UserManagerService extends IUserManager.Stub { } /** + * This method validates whether calling user is valid in visible background users feature. + * Valid user is the current user or the system or in the same profile group as the current + * user. Visible background users are not valid calling users. + */ + public static void enforceCurrentUserIfVisibleBackgroundEnabled(@UserIdInt int currentUserId) { + if (!UserManager.isVisibleBackgroundUsersEnabled()) { + return; + } + final int callingUserId = UserHandle.getCallingUserId(); + if (DBG) { + Slog.d(LOG_TAG, "enforceValidCallingUser: callingUserId=" + callingUserId + + " isSystemUser=" + (callingUserId == USER_SYSTEM) + + " currentUserId=" + currentUserId + + " callingPid=" + Binder.getCallingPid() + + " callingUid=" + Binder.getCallingUid()); + } + final long ident = Binder.clearCallingIdentity(); + try { + if (callingUserId != USER_SYSTEM && callingUserId != currentUserId + && !UserManagerService.getInstance() + .isSameProfileGroup(callingUserId, currentUserId)) { + throw new SecurityException( + "Invalid calling user on devices that enable visible background users. " + + "callingUserId=" + callingUserId + " currentUserId=" + + currentUserId); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + /** * Gets the current and target user ids as a {@link Pair}, calling * {@link ActivityManagerInternal} directly (and without performing any permission check). * diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index ba3de33d865c..f96706e474be 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -1076,16 +1076,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { private void interceptPowerKeyUp(KeyEvent event, boolean canceled) { // Inform the StatusBar; but do not allow it to consume the event. sendSystemKeyToStatusBarAsync(event); - - final boolean handled = canceled || mPowerKeyHandled; - - if (!handled) { - if ((event.getFlags() & KeyEvent.FLAG_LONG_PRESS) == 0) { - // Abort possibly stuck animations only when power key up without long press case. - mHandler.post(mWindowManagerFuncs::triggerAnimationFailsafe); - } - } - finishPowerKeyPress(); } diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java index 67f5f27b42eb..989c8a802b36 100644 --- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java +++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java @@ -313,12 +313,6 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants { } /** - * Hint to window manager that the user has started a navigation action that should - * abort animations that have no timeout, in case they got stuck. - */ - void triggerAnimationFailsafe(); - - /** * The keyguard showing state has changed */ void onKeyguardShowingAndNotOccludedChanged(); diff --git a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java index 2f1641980784..46e779fb0c45 100644 --- a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java +++ b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java @@ -149,8 +149,7 @@ public class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStat // WiFi keeps an accumulated total of stats. Keep the last WiFi stats so we can compute a delta. // (This is unlike Bluetooth, where BatteryStatsImpl is left responsible for taking the delta.) @GuardedBy("mWorkerLock") - private WifiActivityEnergyInfo mLastWifiInfo = - new WifiActivityEnergyInfo(0, 0, 0, 0, 0, 0); + private WifiActivityEnergyInfo mLastWifiInfo = null; /** * Maps an {@link EnergyConsumerType} to it's corresponding {@link EnergyConsumer#id}s, @@ -827,8 +826,18 @@ public class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStat return null; } + /** + * Return a delta WifiActivityEnergyInfo from the last WifiActivityEnergyInfo passed to the + * method. + */ + @VisibleForTesting @GuardedBy("mWorkerLock") - private WifiActivityEnergyInfo extractDeltaLocked(WifiActivityEnergyInfo latest) { + public WifiActivityEnergyInfo extractDeltaLocked(WifiActivityEnergyInfo latest) { + if (mLastWifiInfo == null) { + // This is the first time WifiActivityEnergyInfo has been collected since system boot. + // Use this first WifiActivityEnergyInfo as the starting point for all accumulations. + mLastWifiInfo = latest; + } final long timePeriodMs = latest.getTimeSinceBootMillis() - mLastWifiInfo.getTimeSinceBootMillis(); final long lastScanMs = mLastWifiInfo.getControllerScanDurationMillis(); diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index 1d3de574991e..b45651d7aafc 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -1167,7 +1167,6 @@ public class BatteryStatsImpl extends BatteryStats { private static final int USB_DATA_CONNECTED = 2; int mUsbDataState = USB_DATA_UNKNOWN; - private static final int GPS_SIGNAL_QUALITY_NONE = 2; int mGpsSignalQualityBin = -1; final StopwatchTimer[] mGpsSignalQualityTimer = new StopwatchTimer[GnssSignalQuality.NUM_GNSS_SIGNAL_QUALITY_LEVELS]; @@ -5528,7 +5527,7 @@ public class BatteryStatsImpl extends BatteryStats { mHistory.recordStateStopEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.STATE_GPS_ON_FLAG, uid, "gnss"); mHistory.recordGpsSignalQualityEvent(elapsedRealtimeMs, uptimeMs, - GPS_SIGNAL_QUALITY_NONE); + HistoryItem.GNSS_SIGNAL_QUALITY_NONE); stopAllGpsSignalQualityTimersLocked(-1, elapsedRealtimeMs); mGpsSignalQualityBin = -1; if (mPowerStatsCollectorEnabled.get(BatteryConsumer.POWER_COMPONENT_GNSS)) { @@ -15487,7 +15486,8 @@ public class BatteryStatsImpl extends BatteryStats { final long txTimeMs = counter.getTxTimeCounters()[0].getCountLocked(which); final long totalControllerActivityTimeMs = computeBatteryRealtime(mClock.elapsedRealtime() * 1000, which) / 1000; - final long sleepTimeMs = totalControllerActivityTimeMs - (idleTimeMs + rxTimeMs + txTimeMs); + final long sleepTimeMs = Math.max(0, + totalControllerActivityTimeMs - (idleTimeMs + rxTimeMs + txTimeMs)); final long energyConsumedMaMs = counter.getPowerCounter().getCountLocked(which); final long monitoredRailChargeConsumedMaMs = counter.getMonitoredRailChargeConsumedMaMs().getCountLocked(which); diff --git a/services/core/java/com/android/server/power/stats/BinaryStatePowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/BinaryStatePowerStatsProcessor.java index 393fa39cdff6..03df46aa1701 100644 --- a/services/core/java/com/android/server/power/stats/BinaryStatePowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/BinaryStatePowerStatsProcessor.java @@ -115,6 +115,12 @@ abstract class BinaryStatePowerStatsProcessor extends PowerStatsProcessor { mInitiatingUid = mUidResolver.mapUid(item.eventTag.uid); } } else { + if (mInitiatingUid == Process.INVALID_UID) { + if (item.eventCode == (BatteryStats.HistoryItem.EVENT_STATE_CHANGE + | BatteryStats.HistoryItem.EVENT_FLAG_FINISH)) { + mInitiatingUid = mUidResolver.mapUid(item.eventTag.uid); + } + } recordUsageDuration(mPowerStats, mInitiatingUid, item.time); mInitiatingUid = Process.INVALID_UID; if (!mEnergyConsumerSupported) { diff --git a/services/core/java/com/android/server/power/stats/GnssPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/GnssPowerStatsProcessor.java index 572bde9b9266..0b287109cfa6 100644 --- a/services/core/java/com/android/server/power/stats/GnssPowerStatsProcessor.java +++ b/services/core/java/com/android/server/power/stats/GnssPowerStatsProcessor.java @@ -27,15 +27,15 @@ import com.android.internal.os.PowerStats; import java.util.Arrays; public class GnssPowerStatsProcessor extends BinaryStatePowerStatsProcessor { - private int mGnssSignalLevel = GnssSignalQuality.GNSS_SIGNAL_QUALITY_UNKNOWN; - private long mGnssSignalLevelTimestamp; - private final long[] mGnssSignalDurations = - new long[GnssSignalQuality.NUM_GNSS_SIGNAL_QUALITY_LEVELS]; private static final GnssPowerStatsLayout sStatsLayout = new GnssPowerStatsLayout(); private final UsageBasedPowerEstimator[] mSignalLevelEstimators = new UsageBasedPowerEstimator[GnssSignalQuality.NUM_GNSS_SIGNAL_QUALITY_LEVELS]; private final boolean mUseSignalLevelEstimators; private long[] mTmpDeviceStatsArray; + private int mGnssSignalLevel; + private long mGnssSignalLevelTimestamp; + private final long[] mGnssSignalDurations = + new long[GnssSignalQuality.NUM_GNSS_SIGNAL_QUALITY_LEVELS]; public GnssPowerStatsProcessor(PowerProfile powerProfile, PowerStatsUidResolver uidResolver) { super(BatteryConsumer.POWER_COMPONENT_GNSS, uidResolver, @@ -55,20 +55,33 @@ public class GnssPowerStatsProcessor extends BinaryStatePowerStatsProcessor { } @Override - protected @BinaryState int getBinaryState(BatteryStats.HistoryItem item) { - if ((item.states & BatteryStats.HistoryItem.STATE_GPS_ON_FLAG) == 0) { - mGnssSignalLevel = GnssSignalQuality.GNSS_SIGNAL_QUALITY_UNKNOWN; - return STATE_OFF; - } + void start(PowerComponentAggregatedPowerStats stats, long timestampMs) { + super.start(stats, timestampMs); - noteGnssSignalLevel(item); - return STATE_ON; + mGnssSignalLevelTimestamp = timestampMs; + mGnssSignalLevel = GnssSignalQuality.GNSS_SIGNAL_QUALITY_UNKNOWN; + Arrays.fill(mGnssSignalDurations, 0); } - private void noteGnssSignalLevel(BatteryStats.HistoryItem item) { - int signalLevel = (item.states2 & BatteryStats.HistoryItem.STATE2_GPS_SIGNAL_QUALITY_MASK) - >> BatteryStats.HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT; - if (signalLevel >= GnssSignalQuality.NUM_GNSS_SIGNAL_QUALITY_LEVELS) { + @Override + protected @BinaryState int getBinaryState(BatteryStats.HistoryItem item) { + return (item.states & BatteryStats.HistoryItem.STATE_GPS_ON_FLAG) != 0 + ? STATE_ON : STATE_OFF; + } + + @Override + void noteStateChange(PowerComponentAggregatedPowerStats stats, BatteryStats.HistoryItem item) { + super.noteStateChange(stats, item); + + int signalLevel; + if ((item.states & BatteryStats.HistoryItem.STATE_GPS_ON_FLAG) != 0) { + signalLevel = (item.states2 & BatteryStats.HistoryItem.STATE2_GPS_SIGNAL_QUALITY_MASK) + >> BatteryStats.HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT; + if (signalLevel >= GnssSignalQuality.NUM_GNSS_SIGNAL_QUALITY_LEVELS) { + // Default GNSS signal quality to GOOD for the purposes of power attribution + signalLevel = GnssSignalQuality.GNSS_SIGNAL_QUALITY_GOOD; + } + } else { signalLevel = GnssSignalQuality.GNSS_SIGNAL_QUALITY_UNKNOWN; } if (signalLevel == mGnssSignalLevel) { diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java index ac56043faa7d..b35a0a772ff2 100644 --- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java @@ -71,6 +71,7 @@ import static com.android.server.stats.pull.ProcfsMemoryUtil.getProcessCmdlines; import static com.android.server.stats.pull.ProcfsMemoryUtil.readCmdlineFromProcfs; import static com.android.server.stats.pull.ProcfsMemoryUtil.readMemorySnapshotFromProcfs; import static com.android.server.stats.pull.netstats.NetworkStatsUtils.fromPublicNetworkStats; +import static com.android.server.stats.pull.netstats.NetworkStatsUtils.isAddEntriesSupported; import static libcore.io.IoUtils.closeQuietly; @@ -1388,14 +1389,22 @@ public class StatsPullAtomService extends SystemService { @NonNull private static NetworkStats removeEmptyEntries(NetworkStats stats) { - NetworkStats ret = new NetworkStats(0, 1); + final ArrayList<NetworkStats.Entry> entries = new ArrayList<>(); for (NetworkStats.Entry e : stats) { if (e.getRxBytes() != 0 || e.getRxPackets() != 0 || e.getTxBytes() != 0 || e.getTxPackets() != 0 || e.getOperations() != 0) { - ret = ret.addEntry(e); + entries.add(e); } } - return ret; + if (isAddEntriesSupported()) { + return new NetworkStats(0, entries.size()).addEntries(entries); + } else { + NetworkStats outputStats = new NetworkStats(0L, 1); + for (NetworkStats.Entry e : entries) { + outputStats = outputStats.addEntry(e); + } + return outputStats; + } } private void addNetworkStats(int atomTag, @NonNull List<StatsEvent> ret, @@ -1720,11 +1729,19 @@ public class StatsPullAtomService extends SystemService { @NonNull private NetworkStats sliceNetworkStats(@NonNull NetworkStats stats, @NonNull Function<NetworkStats.Entry, NetworkStats.Entry> slicer) { - NetworkStats ret = new NetworkStats(0, 1); + final ArrayList<NetworkStats.Entry> entries = new ArrayList(); for (NetworkStats.Entry e : stats) { - ret = ret.addEntry(slicer.apply(e)); + entries.add(slicer.apply(e)); + } + if (isAddEntriesSupported()) { + return new NetworkStats(0, entries.size()).addEntries(entries); + } else { + NetworkStats outputStats = new NetworkStats(0L, 1); + for (NetworkStats.Entry e : entries) { + outputStats = outputStats.addEntry(e); + } + return outputStats; } - return ret; } private void registerWifiBytesTransferBackground() { diff --git a/services/core/java/com/android/server/stats/pull/netstats/NetworkStatsUtils.java b/services/core/java/com/android/server/stats/pull/netstats/NetworkStatsUtils.java index de5885201ea4..0318bdd61473 100644 --- a/services/core/java/com/android/server/stats/pull/netstats/NetworkStatsUtils.java +++ b/services/core/java/com/android/server/stats/pull/netstats/NetworkStatsUtils.java @@ -24,6 +24,9 @@ import static android.net.NetworkStats.SET_ALL; import android.app.usage.NetworkStats; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.stats.Flags; + +import java.util.ArrayList; /** * Utility methods for accessing {@link android.net.NetworkStats}. @@ -35,12 +38,21 @@ public class NetworkStatsUtils { */ public static android.net.NetworkStats fromPublicNetworkStats( NetworkStats publiceNetworkStats) { - android.net.NetworkStats stats = new android.net.NetworkStats(0L, 0); + final ArrayList<android.net.NetworkStats.Entry> entries = new ArrayList<>(); while (publiceNetworkStats.hasNextBucket()) { NetworkStats.Bucket bucket = new NetworkStats.Bucket(); publiceNetworkStats.getNextBucket(bucket); - final android.net.NetworkStats.Entry entry = fromBucket(bucket); - stats = stats.addEntry(entry); + entries.add(fromBucket(bucket)); + } + android.net.NetworkStats stats = new android.net.NetworkStats(0L, 1); + // The new API is only supported on devices running the mainline version of `NetworkStats`. + // It should always be used when available for memory efficiency. + if (isAddEntriesSupported()) { + stats = stats.addEntries(entries); + } else { + for (android.net.NetworkStats.Entry entry : entries) { + stats = stats.addEntry(entry); + } } return stats; } @@ -106,4 +118,8 @@ public class NetworkStatsUtils { } return 0; } + + public static boolean isAddEntriesSupported() { + return Flags.netstatsUseAddEntries(); + } } diff --git a/services/core/java/com/android/server/stats/stats_flags.aconfig b/services/core/java/com/android/server/stats/stats_flags.aconfig index f360837e1d28..afea3038bcbb 100644 --- a/services/core/java/com/android/server/stats/stats_flags.aconfig +++ b/services/core/java/com/android/server/stats/stats_flags.aconfig @@ -1,6 +1,20 @@ package: "com.android.server.stats" container: "system" +# Note: To ensure compatibility across all release configurations, initiate the ramp-up process +# only after the 'com.android.net.flags.netstats_add_entries' flag has been fully deployed. +# This flag provides the necessary API from the Connectivity module. +# The flag was added because the flag 'com.android.net.flags.netstats_add_entries' for API +# is already being rolled out, and modifying behavior during an active rollout might +# lead to unwanted issues. +flag { + name: "netstats_use_add_entries" + namespace: "statsd" + description: "Use NetworkStats#addEntries to reduce memory footprint" + bug: "335680025" + is_fixed_read_only: true +} + flag { name: "add_mobile_bytes_transfer_by_proc_state_puller" namespace: "statsd" diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java index 2faa68a9948f..09d2a0263f2e 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java @@ -168,11 +168,6 @@ public interface StatusBarManagerInternal { */ void onDisplayReady(int displayId); - /** - * Notifies System UI whether the recents animation is running. - */ - void onRecentsAnimationStateChanged(boolean running); - /** @see com.android.internal.statusbar.IStatusBar#onSystemBarAttributesChanged */ void onSystemBarAttributesChanged(int displayId, @Appearance int appearance, AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme, diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 7d812ee8c5cd..0fd59670436e 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -699,17 +699,6 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override - public void onRecentsAnimationStateChanged(boolean running) { - IStatusBar bar = mBar; - if (bar != null) { - try { - bar.onRecentsAnimationStateChanged(running); - } catch (RemoteException ex) {} - } - - } - - @Override public void onSystemBarAttributesChanged(int displayId, @Appearance int appearance, AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme, @Behavior int behavior, @InsetsType int requestedVisibleTypes, diff --git a/services/core/java/com/android/server/tracing/TracingServiceProxy.java b/services/core/java/com/android/server/tracing/TracingServiceProxy.java index 480db25ec606..8e24e9f8a4c0 100644 --- a/services/core/java/com/android/server/tracing/TracingServiceProxy.java +++ b/services/core/java/com/android/server/tracing/TracingServiceProxy.java @@ -38,6 +38,7 @@ import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor.AutoCloseInputStream; import android.os.ParcelFileDescriptor.AutoCloseOutputStream; import android.os.UserHandle; +import android.provider.Settings; import android.service.tracing.TraceReportService; import android.tracing.ITracingServiceProxy; import android.tracing.TraceReportParams; @@ -87,6 +88,8 @@ public class TracingServiceProxy extends SystemService { TRACING_SERVICE_REPORT_EVENT__EVENT__TRACING_SERVICE_REPORT_SVC_PERM_MISSING; private static final int REPORT_SVC_COMM_ERROR = TRACING_SERVICE_REPORT_EVENT__EVENT__TRACING_SERVICE_REPORT_SVC_COMM_ERROR; + private static final String NOTIFY_SESSION_ENDED_SETTING = "should_notify_trace_session_ended"; + private static final int ENABLED = 1; private final Context mContext; private final PackageManager mPackageManager; @@ -97,10 +100,22 @@ public class TracingServiceProxy extends SystemService { /** * Notifies system tracing app that a tracing session has ended. sessionStolen is ignored, * as trace sessions are no longer stolen and are always cloned instead. + * <p> + * Cases exist where user-flows besides Traceur's QS Tile may end long-trace sessions. In + * these cases, a Global int will be set to flag the upcoming notifyTraceSessionEnded call + * as purposely muted once. */ @Override public void notifyTraceSessionEnded(boolean sessionStolen /* unused */) { - TracingServiceProxy.this.notifyTraceur(); + long identity = Binder.clearCallingIdentity(); + if (Settings.Global.getInt(mContext.getContentResolver(), + NOTIFY_SESSION_ENDED_SETTING, ENABLED) == ENABLED) { + TracingServiceProxy.this.notifyTraceur(); + } else { + Settings.Global.putInt(mContext.getContentResolver(), NOTIFY_SESSION_ENDED_SETTING, + ENABLED); + } + Binder.restoreCallingIdentity(identity); } @Override diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java b/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java index 65fc7b2c5c39..d10ef319e187 100644 --- a/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java +++ b/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java @@ -16,6 +16,7 @@ package com.android.server.vibrator; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.res.Resources; import android.content.res.XmlResourceParser; @@ -28,7 +29,10 @@ import android.text.TextUtils; import android.util.Slog; import android.util.SparseArray; import android.util.Xml; +import android.view.InputDevice; +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.XmlUtils; import com.android.internal.vibrator.persistence.XmlParserException; import com.android.internal.vibrator.persistence.XmlReader; @@ -42,6 +46,7 @@ import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.Reader; +import java.util.Locale; /** * Class that loads custom {@link VibrationEffect} to be performed for each @@ -92,105 +97,146 @@ final class HapticFeedbackCustomization { private static final String ATTRIBUTE_ID = "id"; /** - * Parses the haptic feedback vibration customization XML file for the device, and provides a - * mapping of the customized effect IDs to their respective {@link VibrationEffect}s. - * - * <p>This is potentially expensive, so avoid calling repeatedly. One call is enough, and the - * caller should process the returned mapping (if any) for further queries. - * - * @param res {@link Resources} object to be used for reading the device's resources. - * @return a {@link SparseArray} that maps each customized haptic feedback effect ID to its - * respective {@link VibrationEffect}, or {@code null}, if the device has not configured - * a file for haptic feedback constants customization. - * @throws {@link IOException} if an IO error occurs while parsing the customization XML. - * @throws {@link CustomizationParserException} for any non-IO error that occurs when parsing - * the XML, like an invalid XML content or an invalid haptic feedback constant. + * A {@link SparseArray} that maps each customized haptic feedback effect ID to its + * respective {@link VibrationEffect}. If this is empty, system's default vibration will be + * used. */ - @Nullable - static SparseArray<VibrationEffect> loadVibrations(Resources res, VibratorInfo vibratorInfo) - throws CustomizationParserException, IOException { - try { - return loadVibrationsInternal(res, vibratorInfo); - } catch (VibrationXmlParser.ParseFailedException - | XmlParserException - | XmlPullParserException e) { - throw new CustomizationParserException( - "Error parsing haptic feedback customization file.", e); - } - } + @NonNull + private final SparseArray<VibrationEffect> mHapticCustomizations; - @Nullable - private static SparseArray<VibrationEffect> loadVibrationsInternal( - Resources res, VibratorInfo vibratorInfo) throws - CustomizationParserException, - IOException, - XmlParserException, - XmlPullParserException { + /** + * A {@link SparseArray} similar to {@link mHapticCustomizations} but for rotary input source + * specific customization. + */ + @NonNull + private final SparseArray<VibrationEffect> mHapticCustomizationsForSourceRotary; + + /** + * A {@link SparseArray} similar to {@link mHapticCustomizations} but for touch screen input + * source specific customization. + */ + @NonNull + private final SparseArray<VibrationEffect> mHapticCustomizationsForSourceTouchScreen; + + HapticFeedbackCustomization(Resources res, VibratorInfo vibratorInfo) { if (!Flags.hapticFeedbackVibrationOemCustomizationEnabled()) { Slog.d(TAG, "Haptic feedback customization feature is not enabled."); - return null; + mHapticCustomizations = new SparseArray<>(); + mHapticCustomizationsForSourceRotary = new SparseArray<>(); + mHapticCustomizationsForSourceTouchScreen = new SparseArray<>(); + return; } - // Old loading path that reads customization from file at dir defined by config. - TypedXmlPullParser parser = readCustomizationFile(res); - if (parser == null) { - // When old loading path doesn't succeed, try loading customization from resources. - parser = readCustomizationResources(res); - } - if (parser == null) { - Slog.d(TAG, "No loadable haptic feedback customization."); - return null; + // Load base customizations. + SparseArray<VibrationEffect> hapticCustomizations; + hapticCustomizations = loadCustomizedFeedbackVibrationFromFile(res, vibratorInfo); + if (hapticCustomizations.size() == 0) { + // Input source customized haptic feedback was directly added in res. So, no need to old + // loading path. + hapticCustomizations = loadCustomizedFeedbackVibrationFromRes(res, vibratorInfo, + R.xml.haptic_feedback_customization); } + mHapticCustomizations = hapticCustomizations; - XmlUtils.beginDocument(parser, TAG_CONSTANTS); - XmlValidator.checkTagHasNoUnexpectedAttributes(parser); - int rootDepth = parser.getDepth(); + // Load customizations specified by input sources. + if (android.os.vibrator.Flags.hapticFeedbackInputSourceCustomizationEnabled()) { + mHapticCustomizationsForSourceRotary = + loadCustomizedFeedbackVibrationFromRes(res, vibratorInfo, + R.xml.haptic_feedback_customization_source_rotary_encoder); + mHapticCustomizationsForSourceTouchScreen = + loadCustomizedFeedbackVibrationFromRes(res, vibratorInfo, + R.xml.haptic_feedback_customization_source_touchscreen); + } else { + mHapticCustomizationsForSourceRotary = new SparseArray<>(); + mHapticCustomizationsForSourceTouchScreen = new SparseArray<>(); + } + } - SparseArray<VibrationEffect> mapping = new SparseArray<>(); - while (XmlReader.readNextTagWithin(parser, rootDepth)) { - XmlValidator.checkStartTag(parser, TAG_CONSTANT); - int customizationDepth = parser.getDepth(); + @VisibleForTesting + HapticFeedbackCustomization(@NonNull SparseArray<VibrationEffect> hapticCustomizations, + @NonNull SparseArray<VibrationEffect> hapticCustomizationsForSourceRotary, + @NonNull SparseArray<VibrationEffect> hapticCustomizationsForSourceTouchScreen) { + mHapticCustomizations = hapticCustomizations; + mHapticCustomizationsForSourceRotary = hapticCustomizationsForSourceRotary; + mHapticCustomizationsForSourceTouchScreen = hapticCustomizationsForSourceTouchScreen; + } - // Only attribute in tag is the `id` attribute. - XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_ID); - int effectId = XmlReader.readAttributeIntNonNegative(parser, ATTRIBUTE_ID); - if (mapping.contains(effectId)) { - throw new CustomizationParserException( - "Multiple customizations found for effect " + effectId); - } + @Nullable + VibrationEffect getEffect(int effectId) { + return mHapticCustomizations.get(effectId); + } - // Move the parser one step into the `<constant>` tag. - XmlValidator.checkParserCondition( - XmlReader.readNextTagWithin(parser, customizationDepth), - "Unsupported empty customization tag for effect " + effectId); + @Nullable + VibrationEffect getEffect(int effectId, int inputSource) { + VibrationEffect resultVibration = null; + if ((InputDevice.SOURCE_ROTARY_ENCODER & inputSource) != 0) { + resultVibration = mHapticCustomizationsForSourceRotary.get(effectId); + } else if ((InputDevice.SOURCE_TOUCHSCREEN & inputSource) != 0) { + resultVibration = mHapticCustomizationsForSourceTouchScreen.get(effectId); + } + if (resultVibration == null) { + resultVibration = mHapticCustomizations.get(effectId); + } + return resultVibration; + } - ParsedVibration parsedVibration = VibrationXmlParser.parseElement( - parser, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS); - VibrationEffect effect = parsedVibration.resolve(vibratorInfo); - if (effect != null) { - if (effect.getDuration() == Long.MAX_VALUE) { - throw new CustomizationParserException(String.format( - "Vibration for effect ID %d is repeating, which is not allowed as a" - + " haptic feedback: %s", effectId, effect)); - } - mapping.put(effectId, effect); + /** + * Parses the haptic feedback vibration customization XML file for the device whose directory is + * specified by config. See {@link R.string.config_hapticFeedbackCustomizationFile}. + * + * @return Return a mapping of the customized effect IDs to their respective + * {@link VibrationEffect}s. + */ + @NonNull + private static SparseArray<VibrationEffect> loadCustomizedFeedbackVibrationFromFile( + Resources res, VibratorInfo vibratorInfo) { + try { + TypedXmlPullParser parser = readCustomizationFile(res); + if (parser == null) { + Slog.d(TAG, "No loadable haptic feedback customization from file."); + return new SparseArray<>(); } - - XmlReader.readEndTag(parser, TAG_CONSTANT, customizationDepth); + return parseVibrations(parser, vibratorInfo); + } catch (XmlPullParserException | XmlParserException | IOException e) { + Slog.e(TAG, "Error parsing haptic feedback customizations from file", e); + return new SparseArray<>(); } + } - // Make checks that the XML ends well. - XmlReader.readEndTag(parser, TAG_CONSTANTS, rootDepth); - XmlReader.readDocumentEndTag(parser); - - return mapping; + /** + * Parses the haptic feedback vibration customization XML resource for the device. + * + * @return Return a mapping of the customized effect IDs to their respective + * {@link VibrationEffect}s. + */ + @NonNull + private static SparseArray<VibrationEffect> loadCustomizedFeedbackVibrationFromRes( + Resources res, VibratorInfo vibratorInfo, int xmlResId) { + try { + TypedXmlPullParser parser = readCustomizationResources(res, xmlResId); + if (parser == null) { + Slog.d(TAG, "No loadable haptic feedback customization from res."); + return new SparseArray<>(); + } + return parseVibrations(parser, vibratorInfo); + } catch (XmlPullParserException | XmlParserException | IOException e) { + Slog.e(TAG, "Error parsing haptic feedback customizations from res", e); + return new SparseArray<>(); + } } // TODO(b/356412421): deprecate old path related files. private static TypedXmlPullParser readCustomizationFile(Resources res) throws XmlPullParserException { - String customizationFile = res.getString( - com.android.internal.R.string.config_hapticFeedbackCustomizationFile); + String customizationFile; + try { + customizationFile = res.getString( + R.string.config_hapticFeedbackCustomizationFile); + } catch (Resources.NotFoundException e) { + Slog.e(TAG, "Customization file directory config not found.", e); + return null; + } + if (TextUtils.isEmpty(customizationFile)) { return null; } @@ -211,13 +257,14 @@ final class HapticFeedbackCustomization { return parser; } - private static TypedXmlPullParser readCustomizationResources(Resources res) { + @Nullable + private static TypedXmlPullParser readCustomizationResources(Resources res, int xmlResId) { if (!Flags.loadHapticFeedbackVibrationCustomizationFromResources()) { return null; } final XmlResourceParser resParser; try { - resParser = res.getXml(com.android.internal.R.xml.haptic_feedback_customization); + resParser = res.getXml(xmlResId); } catch (Resources.NotFoundException e) { Slog.e(TAG, "Haptic customization resource not found.", e); return null; @@ -226,16 +273,52 @@ final class HapticFeedbackCustomization { return XmlUtils.makeTyped(resParser); } - /** - * Represents an error while parsing a haptic feedback customization XML. - */ - static final class CustomizationParserException extends Exception { - private CustomizationParserException(String message) { - super(message); - } + @NonNull + private static SparseArray<VibrationEffect> parseVibrations(TypedXmlPullParser parser, + VibratorInfo vibratorInfo) + throws XmlPullParserException, IOException, XmlParserException { + XmlUtils.beginDocument(parser, TAG_CONSTANTS); + XmlValidator.checkTagHasNoUnexpectedAttributes(parser); + int rootDepth = parser.getDepth(); + + SparseArray<VibrationEffect> mapping = new SparseArray<>(); + while (XmlReader.readNextTagWithin(parser, rootDepth)) { + XmlValidator.checkStartTag(parser, TAG_CONSTANT); + int customizationDepth = parser.getDepth(); - private CustomizationParserException(String message, Throwable cause) { - super(message, cause); + // Only attribute in tag is the `id` attribute. + XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_ID); + int effectId = XmlReader.readAttributeIntNonNegative(parser, ATTRIBUTE_ID); + if (mapping.contains(effectId)) { + Slog.e(TAG, "Multiple customizations found for effect " + effectId); + return new SparseArray<>(); + } + + // Move the parser one step into the `<constant>` tag. + XmlValidator.checkParserCondition( + XmlReader.readNextTagWithin(parser, customizationDepth), + "Unsupported empty customization tag for effect " + effectId); + + ParsedVibration parsedVibration = VibrationXmlParser.parseElement( + parser, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS); + VibrationEffect effect = parsedVibration.resolve(vibratorInfo); + if (effect != null) { + if (effect.getDuration() == Long.MAX_VALUE) { + Slog.e(TAG, String.format(Locale.getDefault(), + "Vibration for effect ID %d is repeating, which is not allowed as a" + + " haptic feedback: %s", effectId, effect)); + return new SparseArray<>(); + } + mapping.put(effectId, effect); + } + + XmlReader.readEndTag(parser, TAG_CONSTANT, customizationDepth); } + + // Make checks that the XML ends well. + XmlReader.readEndTag(parser, TAG_CONSTANTS, rootDepth); + XmlReader.readDocumentEndTag(parser); + + return mapping; } } diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java index 07610872b208..cae6b34d4c73 100644 --- a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java +++ b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java @@ -16,19 +16,17 @@ package com.android.server.vibrator; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.res.Resources; import android.os.VibrationAttributes; import android.os.VibrationEffect; -import android.os.Vibrator; import android.os.VibratorInfo; -import android.util.Slog; -import android.util.SparseArray; import android.view.HapticFeedbackConstants; +import android.view.InputDevice; import com.android.internal.annotations.VisibleForTesting; -import java.io.IOException; import java.io.PrintWriter; /** @@ -52,39 +50,29 @@ public final class HapticFeedbackVibrationProvider { private final boolean mHapticTextHandleEnabled; // Vibrator effect for haptic feedback during boot when safe mode is enabled. private final VibrationEffect mSafeModeEnabledVibrationEffect; - // Haptic feedback vibration customizations specific to the device. - // If present and valid, a vibration here will be used for an effect. - // Otherwise, the system's default vibration will be used. - @Nullable private final SparseArray<VibrationEffect> mHapticCustomizations; - private float mKeyboardVibrationFixedAmplitude; + private final HapticFeedbackCustomization mHapticFeedbackCustomization; - public HapticFeedbackVibrationProvider(Resources res, Vibrator vibrator) { - this(res, vibrator.getInfo()); - } + private float mKeyboardVibrationFixedAmplitude; public HapticFeedbackVibrationProvider(Resources res, VibratorInfo vibratorInfo) { - this(res, vibratorInfo, loadHapticCustomizations(res, vibratorInfo)); + this(res, vibratorInfo, new HapticFeedbackCustomization(res, vibratorInfo)); } - @VisibleForTesting HapticFeedbackVibrationProvider( - Resources res, - VibratorInfo vibratorInfo, - @Nullable SparseArray<VibrationEffect> hapticCustomizations) { + @VisibleForTesting + HapticFeedbackVibrationProvider(Resources res, VibratorInfo vibratorInfo, + HapticFeedbackCustomization hapticFeedbackCustomization) { mVibratorInfo = vibratorInfo; mHapticTextHandleEnabled = res.getBoolean( com.android.internal.R.bool.config_enableHapticTextHandle); + mHapticFeedbackCustomization = hapticFeedbackCustomization; + + VibrationEffect safeModeVibration = mHapticFeedbackCustomization.getEffect( + HapticFeedbackConstants.SAFE_MODE_ENABLED); + mSafeModeEnabledVibrationEffect = safeModeVibration != null ? safeModeVibration + : VibrationSettings.createEffectFromResource(res, + com.android.internal.R.array.config_safeModeEnabledVibePattern); - if (hapticCustomizations != null && hapticCustomizations.size() == 0) { - hapticCustomizations = null; - } - mHapticCustomizations = hapticCustomizations; - mSafeModeEnabledVibrationEffect = - effectHasCustomization(HapticFeedbackConstants.SAFE_MODE_ENABLED) - ? mHapticCustomizations.get(HapticFeedbackConstants.SAFE_MODE_ENABLED) - : VibrationSettings.createEffectFromResource( - res, - com.android.internal.R.array.config_safeModeEnabledVibePattern); mKeyboardVibrationFixedAmplitude = res.getFloat( com.android.internal.R.dimen.config_keyboardHapticFeedbackFixedAmplitude); if (mKeyboardVibrationFixedAmplitude < 0 || mKeyboardVibrationFixedAmplitude > 1) { @@ -100,88 +88,40 @@ public final class HapticFeedbackVibrationProvider { * @return a {@link VibrationEffect} for the given haptic feedback effect ID, or {@code null} if * the provided effect ID is not supported. */ - @Nullable public VibrationEffect getVibrationForHapticFeedback(int effectId) { - switch (effectId) { - case HapticFeedbackConstants.CONTEXT_CLICK: - case HapticFeedbackConstants.GESTURE_END: - case HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE: - case HapticFeedbackConstants.SCROLL_TICK: - case HapticFeedbackConstants.SEGMENT_TICK: - return getVibration(effectId, VibrationEffect.EFFECT_TICK); - - case HapticFeedbackConstants.TEXT_HANDLE_MOVE: - if (!mHapticTextHandleEnabled) { - return null; - } - // fallthrough - case HapticFeedbackConstants.CLOCK_TICK: - case HapticFeedbackConstants.SEGMENT_FREQUENT_TICK: - return getVibration(effectId, VibrationEffect.EFFECT_TEXTURE_TICK); - - case HapticFeedbackConstants.KEYBOARD_RELEASE: - case HapticFeedbackConstants.KEYBOARD_TAP: // == KEYBOARD_PRESS - return getKeyboardVibration(effectId); - - case HapticFeedbackConstants.VIRTUAL_KEY_RELEASE: - case HapticFeedbackConstants.DRAG_CROSSING: - return getVibration( - effectId, - VibrationEffect.EFFECT_TICK, - /* fallbackForPredefinedEffect= */ false); - - case HapticFeedbackConstants.VIRTUAL_KEY: - case HapticFeedbackConstants.EDGE_RELEASE: - case HapticFeedbackConstants.CALENDAR_DATE: - case HapticFeedbackConstants.CONFIRM: - case HapticFeedbackConstants.BIOMETRIC_CONFIRM: - case HapticFeedbackConstants.GESTURE_START: - case HapticFeedbackConstants.SCROLL_ITEM_FOCUS: - case HapticFeedbackConstants.SCROLL_LIMIT: - return getVibration(effectId, VibrationEffect.EFFECT_CLICK); - - case HapticFeedbackConstants.LONG_PRESS: - case HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON: - case HapticFeedbackConstants.DRAG_START: - case HapticFeedbackConstants.EDGE_SQUEEZE: - return getVibration(effectId, VibrationEffect.EFFECT_HEAVY_CLICK); - - case HapticFeedbackConstants.REJECT: - case HapticFeedbackConstants.BIOMETRIC_REJECT: - return getVibration(effectId, VibrationEffect.EFFECT_DOUBLE_CLICK); - - case HapticFeedbackConstants.SAFE_MODE_ENABLED: - return mSafeModeEnabledVibrationEffect; - - case HapticFeedbackConstants.ASSISTANT_BUTTON: - return getAssistantButtonVibration(); - - case HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE: - return getVibration( - effectId, - VibrationEffect.Composition.PRIMITIVE_TICK, - /* primitiveScale= */ 0.4f, - VibrationEffect.EFFECT_TEXTURE_TICK); - - case HapticFeedbackConstants.TOGGLE_ON: - return getVibration( - effectId, - VibrationEffect.Composition.PRIMITIVE_TICK, - /* primitiveScale= */ 0.5f, - VibrationEffect.EFFECT_TICK); - - case HapticFeedbackConstants.TOGGLE_OFF: - return getVibration( - effectId, - VibrationEffect.Composition.PRIMITIVE_LOW_TICK, - /* primitiveScale= */ 0.2f, - VibrationEffect.EFFECT_TEXTURE_TICK); + @Nullable public VibrationEffect getVibration(int effectId) { + if (!isFeedbackConstantEnabled(effectId)) { + return null; + } + VibrationEffect customizedVibration = mHapticFeedbackCustomization.getEffect(effectId); + if (customizedVibration != null) { + return customizedVibration; + } + return getVibrationForHapticFeedback(effectId); + } - case HapticFeedbackConstants.NO_HAPTICS: - default: - return null; + /** + * Provides the {@link VibrationEffect} for a given haptic feedback effect ID (provided in + * {@link HapticFeedbackConstants}). + * + * @param effectId the haptic feedback effect ID whose respective vibration we want to get. + * @param inputSource the {@link InputDevice.Source} that customizes the haptic feedback + * corresponding to the {@code effectId}. + * @return a {@link VibrationEffect} for the given haptic feedback effect ID, or {@code null} if + * the provided effect ID is not supported. + */ + @Nullable public VibrationEffect getVibration(int effectId, int inputSource) { + if (!isFeedbackConstantEnabled(effectId)) { + return null; + } + VibrationEffect customizedVibration = mHapticFeedbackCustomization.getEffect(effectId, + inputSource); + if (customizedVibration != null) { + return customizedVibration; } + return getVibrationForHapticFeedback(effectId); } + // TODO(b/354049335): handle input source customized VibrationAttributes. /** * Provides the {@link VibrationAttributes} that should be used for a haptic feedback. * @@ -255,61 +195,106 @@ public final class HapticFeedbackVibrationProvider { pw.print("mHapticTextHandleEnabled="); pw.println(mHapticTextHandleEnabled); } - private VibrationEffect getVibration(int effectId, int predefinedVibrationEffectId) { - return getVibration( - effectId, predefinedVibrationEffectId, /* fallbackForPredefinedEffect= */ true); + private boolean isFeedbackConstantEnabled(int effectId) { + return switch (effectId) { + case HapticFeedbackConstants.TEXT_HANDLE_MOVE -> mHapticTextHandleEnabled; + case HapticFeedbackConstants.NO_HAPTICS -> false; + default -> true; + }; } /** - * Returns the customized vibration for {@code hapticFeedbackId}, or - * {@code predefinedVibrationEffectId} if a customization does not exist for the haptic - * feedback. - * - * <p>If a customization does not exist and the default predefined effect is to be returned, - * {@code fallbackForPredefinedEffect} will be used to decide whether or not to fallback - * to a generic pattern if the predefined effect is not hardware supported. - * - * @see VibrationEffect#get(int, boolean) + * Get {@link VibrationEffect} respective {@code effectId} from platform-wise mapping. This + * method doesn't include OEM customizations. */ - private VibrationEffect getVibration( - int hapticFeedbackId, - int predefinedVibrationEffectId, - boolean fallbackForPredefinedEffect) { - if (effectHasCustomization(hapticFeedbackId)) { - return mHapticCustomizations.get(hapticFeedbackId); + @Nullable + private VibrationEffect getVibrationForHapticFeedback(int effectId) { + switch (effectId) { + case HapticFeedbackConstants.CONTEXT_CLICK: + case HapticFeedbackConstants.GESTURE_END: + case HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE: + case HapticFeedbackConstants.SCROLL_TICK: + case HapticFeedbackConstants.SEGMENT_TICK: + return VibrationEffect.get(VibrationEffect.EFFECT_TICK); + + case HapticFeedbackConstants.TEXT_HANDLE_MOVE: + case HapticFeedbackConstants.CLOCK_TICK: + case HapticFeedbackConstants.SEGMENT_FREQUENT_TICK: + return VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK); + + case HapticFeedbackConstants.KEYBOARD_RELEASE: + case HapticFeedbackConstants.KEYBOARD_TAP: // == KEYBOARD_PRESS + // keyboard effect is not customized by the input source. + return getKeyboardVibration(effectId); + + case HapticFeedbackConstants.VIRTUAL_KEY_RELEASE: + case HapticFeedbackConstants.DRAG_CROSSING: + return VibrationEffect.get(VibrationEffect.EFFECT_TICK, /* fallback= */ false); + + case HapticFeedbackConstants.VIRTUAL_KEY: + case HapticFeedbackConstants.EDGE_RELEASE: + case HapticFeedbackConstants.CALENDAR_DATE: + case HapticFeedbackConstants.CONFIRM: + case HapticFeedbackConstants.BIOMETRIC_CONFIRM: + case HapticFeedbackConstants.GESTURE_START: + case HapticFeedbackConstants.SCROLL_ITEM_FOCUS: + case HapticFeedbackConstants.SCROLL_LIMIT: + return VibrationEffect.get(VibrationEffect.EFFECT_CLICK); + + case HapticFeedbackConstants.LONG_PRESS: + case HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON: + case HapticFeedbackConstants.DRAG_START: + case HapticFeedbackConstants.EDGE_SQUEEZE: + return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK); + + case HapticFeedbackConstants.REJECT: + case HapticFeedbackConstants.BIOMETRIC_REJECT: + return VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK); + + case HapticFeedbackConstants.SAFE_MODE_ENABLED: + // safe mode effect is not customized by the input source. + return mSafeModeEnabledVibrationEffect; + + case HapticFeedbackConstants.ASSISTANT_BUTTON: + // assistant effect is not customized by the input source. + return getAssistantButtonVibration(); + + case HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE: + return getVibration( + VibrationEffect.Composition.PRIMITIVE_TICK, + /* primitiveScale= */ 0.4f, + VibrationEffect.EFFECT_TEXTURE_TICK); + + case HapticFeedbackConstants.TOGGLE_ON: + return getVibration( + VibrationEffect.Composition.PRIMITIVE_TICK,/* primitiveScale= */ 0.5f, + VibrationEffect.EFFECT_TICK); + + case HapticFeedbackConstants.TOGGLE_OFF: + return getVibration( + VibrationEffect.Composition.PRIMITIVE_LOW_TICK, + /* primitiveScale= */ 0.2f, + VibrationEffect.EFFECT_TEXTURE_TICK); + + case HapticFeedbackConstants.NO_HAPTICS: + default: + return null; } - return VibrationEffect.get(predefinedVibrationEffectId, fallbackForPredefinedEffect); } - /** - * Returns the customized vibration for {@code hapticFeedbackId}, or some fallback vibration if - * a customization does not exist for the ID. - * - * <p>The fallback will be a primitive composition formed of {@code primitiveId} and - * {@code primitiveScale}, if the primitive is supported. Otherwise, it will be a predefined - * vibration of {@code elsePredefinedVibrationEffectId}. - */ - private VibrationEffect getVibration( - int hapticFeedbackId, - int primitiveId, - float primitiveScale, - int elsePredefinedVibrationEffectId) { - if (effectHasCustomization(hapticFeedbackId)) { - return mHapticCustomizations.get(hapticFeedbackId); - } + @NonNull + private VibrationEffect getVibration(int primitiveId, float primitiveScale, + int predefinedVibrationEffectId) { if (mVibratorInfo.isPrimitiveSupported(primitiveId)) { return VibrationEffect.startComposition() .addPrimitive(primitiveId, primitiveScale) .compose(); - } else { - return VibrationEffect.get(elsePredefinedVibrationEffectId); } + return VibrationEffect.get(predefinedVibrationEffectId); } + @NonNull private VibrationEffect getAssistantButtonVibration() { - if (effectHasCustomization(HapticFeedbackConstants.ASSISTANT_BUTTON)) { - return mHapticCustomizations.get(HapticFeedbackConstants.ASSISTANT_BUTTON); - } if (mVibratorInfo.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE) && mVibratorInfo.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_TICK)) { // quiet ramp, short pause, then sharp tick @@ -322,15 +307,8 @@ public final class HapticFeedbackVibrationProvider { return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK); } - private boolean effectHasCustomization(int effectId) { - return mHapticCustomizations != null && mHapticCustomizations.contains(effectId); - } - + @NonNull private VibrationEffect getKeyboardVibration(int effectId) { - if (effectHasCustomization(effectId)) { - return mHapticCustomizations.get(effectId); - } - int primitiveId; int predefinedEffectId; boolean predefinedEffectFallback; @@ -354,8 +332,7 @@ public final class HapticFeedbackVibrationProvider { .compose(); } } - return getVibration(effectId, predefinedEffectId, - /* fallbackForPredefinedEffect= */ predefinedEffectFallback); + return VibrationEffect.get(predefinedEffectId, predefinedEffectFallback); } private VibrationAttributes createKeyboardVibrationAttributes( @@ -367,17 +344,6 @@ public final class HapticFeedbackVibrationProvider { return IME_FEEDBACK_VIBRATION_ATTRIBUTES; } - @Nullable - private static SparseArray<VibrationEffect> loadHapticCustomizations( - Resources res, VibratorInfo vibratorInfo) { - try { - return HapticFeedbackCustomization.loadVibrations(res, vibratorInfo); - } catch (IOException | HapticFeedbackCustomization.CustomizationParserException e) { - Slog.e(TAG, "Unable to load haptic customizations.", e); - return null; - } - } - private static boolean shouldBypassInterruptionPolicy(int effectId) { switch (effectId) { case HapticFeedbackConstants.SCROLL_TICK: diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index dd16d2433a64..c143bebdd4a5 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -487,39 +487,19 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { HalVibration performHapticFeedbackInternal( int uid, int deviceId, String opPkg, int constant, String reason, IBinder token, int flags, int privFlags) { - // Make sure we report the constant id in the requested haptic feedback reason. reason = "performHapticFeedback(constant=" + constant + "): " + reason; - HapticFeedbackVibrationProvider hapticVibrationProvider = getHapticVibrationProvider(); - if (hapticVibrationProvider == null) { - Slog.e(TAG, "performHapticFeedback; haptic vibration provider not ready."); - logAndRecordPerformHapticFeedbackAttempt(uid, deviceId, opPkg, reason, - Vibration.Status.IGNORED_ERROR_SCHEDULING); - return null; - } - - if (hapticVibrationProvider.isRestrictedHapticFeedback(constant) - && !hasPermission(android.Manifest.permission.VIBRATE_SYSTEM_CONSTANTS)) { - Slog.w(TAG, "performHapticFeedback; no permission for system constant " + constant); - logAndRecordPerformHapticFeedbackAttempt(uid, deviceId, opPkg, reason, - Vibration.Status.IGNORED_MISSING_PERMISSION); + Vibration.Status ignoreStatus = shouldIgnoreHapticFeedback(constant, reason, + hapticVibrationProvider); + if (ignoreStatus != null) { + logAndRecordPerformHapticFeedbackAttempt(uid, deviceId, opPkg, reason, ignoreStatus); return null; } - - VibrationEffect effect = hapticVibrationProvider.getVibrationForHapticFeedback(constant); - if (effect == null) { - Slog.w(TAG, "performHapticFeedback; vibration absent for constant " + constant); - logAndRecordPerformHapticFeedbackAttempt(uid, deviceId, opPkg, reason, - Vibration.Status.IGNORED_UNSUPPORTED); - return null; - } - - CombinedVibration vib = CombinedVibration.createParallel(effect); - VibrationAttributes attrs = hapticVibrationProvider.getVibrationAttributesForHapticFeedback( - constant, flags, privFlags); - VibratorFrameworkStatsLogger.logPerformHapticsFeedbackIfKeyboard(uid, constant); - return vibrateWithoutPermissionCheck(uid, deviceId, opPkg, vib, attrs, reason, token); + return performHapticFeedbackWithEffect(uid, deviceId, opPkg, constant, reason, token, + hapticVibrationProvider.getVibration(constant), + hapticVibrationProvider.getVibrationAttributesForHapticFeedback( + constant, flags, privFlags)); } /** @@ -532,12 +512,35 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { HalVibration performHapticFeedbackForInputDeviceInternal( int uid, int deviceId, String opPkg, int constant, int inputDeviceId, int inputSource, String reason, IBinder token, int flags, int privFlags) { - // TODO(b/355543835): implement input device specific logic. - if (DEBUG) { - Slog.d(TAG, "performHapticFeedbackForInput: input device specific not implemented."); + // Make sure we report the constant id in the requested haptic feedback reason. + reason = "performHapticFeedbackForInputDevice(constant=" + constant + ", inputDeviceId=" + + inputDeviceId + ", inputSource=" + inputSource + "): " + reason; + HapticFeedbackVibrationProvider hapticVibrationProvider = getHapticVibrationProvider(); + Vibration.Status ignoreStatus = shouldIgnoreHapticFeedback(constant, reason, + hapticVibrationProvider); + if (ignoreStatus != null) { + logAndRecordPerformHapticFeedbackAttempt(uid, deviceId, opPkg, reason, ignoreStatus); + return null; } - return performHapticFeedbackInternal(uid, deviceId, opPkg, constant, reason, /* token= */ - this, flags, privFlags); + return performHapticFeedbackWithEffect(uid, deviceId, opPkg, constant, reason, token, + hapticVibrationProvider.getVibration(constant, inputSource), + hapticVibrationProvider.getVibrationAttributesForHapticFeedback( + constant, flags, privFlags)); + } + + private HalVibration performHapticFeedbackWithEffect(int uid, int deviceId, String opPkg, + int constant, String reason, IBinder token, VibrationEffect effect, + VibrationAttributes attrs) { + if (effect == null) { + logAndRecordPerformHapticFeedbackAttempt(uid, deviceId, opPkg, reason, + Vibration.Status.IGNORED_UNSUPPORTED); + Slog.w(TAG, + "performHapticFeedbackWithEffect; vibration absent for constant " + constant); + return null; + } + CombinedVibration vib = CombinedVibration.createParallel(effect); + VibratorFrameworkStatsLogger.logPerformHapticsFeedbackIfKeyboard(uid, constant); + return vibrateWithoutPermissionCheck(uid, deviceId, opPkg, vib, attrs, reason, token); } /** @@ -1237,6 +1240,21 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { return null; } + @Nullable + private Vibration.Status shouldIgnoreHapticFeedback(int constant, String reason, + HapticFeedbackVibrationProvider hapticVibrationProvider) { + if (hapticVibrationProvider == null) { + Slog.e(TAG, reason + "; haptic vibration provider not ready."); + return Vibration.Status.IGNORED_ERROR_SCHEDULING; + } + if (hapticVibrationProvider.isRestrictedHapticFeedback(constant) + && !hasPermission(android.Manifest.permission.VIBRATE_SYSTEM_CONSTANTS)) { + Slog.w(TAG, reason + "; no permission for system constant " + constant); + return Vibration.Status.IGNORED_MISSING_PERMISSION; + } + return null; + } + /** * Return true if the vibration has the same token and usage belongs to given usage class. * diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 10ce8c273736..7cbacd6b0b82 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -1764,15 +1764,7 @@ final class AccessibilityController { IBinder topFocusedWindowToken = null; synchronized (mService.mGlobalLock) { - // If there is a recents animation running, then use the animation target as the - // top window state. Otherwise,do not send the windows if there is no top focus as - // the window manager is still looking for where to put it. We will do the work when - // we get a focus change callback. - final RecentsAnimationController controller = - mService.getRecentsAnimationController(); - final WindowState topFocusedWindowState = controller != null - ? controller.getTargetAppMainWindow() - : getTopFocusWindow(); + final WindowState topFocusedWindowState = getTopFocusWindow(); if (topFocusedWindowState == null) { if (DEBUG) { Slog.d(LOG_TAG, "top focused window is null, compute it again later"); @@ -1907,10 +1899,6 @@ final class AccessibilityController { private boolean windowMattersToAccessibility(AccessibilityWindow a11yWindow, Region regionInScreen, Region unaccountedSpace) { - if (a11yWindow.ignoreRecentsAnimationForAccessibility()) { - return false; - } - if (a11yWindow.isFocused()) { return true; } diff --git a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java index d8e7c77309cd..fd2a909f8b05 100644 --- a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java +++ b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java @@ -659,7 +659,6 @@ public final class AccessibilityWindowsPopulator extends WindowInfosListener { private boolean mIsPIPMenu; private boolean mIsFocused; private boolean mShouldMagnify; - private boolean mIgnoreDuetoRecentsAnimation; private final Region mTouchableRegionInScreen = new Region(); private final Region mTouchableRegionInWindow = new Region(); private WindowInfo mWindowInfo; @@ -692,10 +691,6 @@ public final class AccessibilityWindowsPopulator extends WindowInfosListener { instance.mIsFocused = windowState != null && windowState.isFocused(); instance.mShouldMagnify = windowState == null || windowState.shouldMagnify(); - final RecentsAnimationController controller = service.getRecentsAnimationController(); - instance.mIgnoreDuetoRecentsAnimation = windowState != null && controller != null - && controller.shouldIgnoreForAccessibility(windowState); - final Rect windowFrame = new Rect(inputWindowHandle.frame); getTouchableRegionInWindow(instance.mShouldMagnify, inputWindowHandle.touchableRegion, instance.mTouchableRegionInWindow, windowFrame, magnificationInverseMatrix, @@ -793,13 +788,6 @@ public final class AccessibilityWindowsPopulator extends WindowInfosListener { } /** - * @return true if it's running the recent animation but not the target app. - */ - public boolean ignoreRecentsAnimationForAccessibility() { - return mIgnoreDuetoRecentsAnimation; - } - - /** * @return true if this window is the trusted overlay. */ public boolean isTrustedOverlay() { @@ -909,7 +897,6 @@ public final class AccessibilityWindowsPopulator extends WindowInfosListener { + ", privateFlag=0x" + Integer.toHexString(mPrivateFlags) + ", focused=" + mIsFocused + ", shouldMagnify=" + mShouldMagnify - + ", ignoreDuetoRecentsAnimation=" + mIgnoreDuetoRecentsAnimation + ", isTrustedOverlay=" + isTrustedOverlay() + ", regionInScreen=" + mTouchableRegionInScreen + ", touchableRegion=" + mTouchableRegionInWindow diff --git a/services/core/java/com/android/server/wm/ActionChain.java b/services/core/java/com/android/server/wm/ActionChain.java new file mode 100644 index 000000000000..c697d33ed4e2 --- /dev/null +++ b/services/core/java/com/android/server/wm/ActionChain.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.Slog; + +import com.android.window.flags.Flags; + +/** + * Represents a chain of WM actions where each action is "caused by" the prior action (except the + * first one of course). A whole chain is associated with one Transition (in fact, the purpose + * of this object is to communicate, to all callees, which transition they are part of). + * + * A single action is defined as "one logical thing requested of WM". This usually corresponds to + * each ingress-point into the process. For example, when starting an activity: + * * the first action is to pause the current/top activity. + * At this point, control leaves the process while the activity pauses. + * * Then WM receives completePause (a new ingress). This is a new action that gets linked + * to the prior action. This action involves resuming the next activity, at which point, + * control leaves the process again. + * * Eventually, when everything is done, we will have formed a chain of actions. + * + * We don't technically need to hold onto each prior action in the chain once a new action has + * been linked to the same transition; however, keeping the whole chain enables improved + * debugging and the ability to detect anomalies. + */ +public class ActionChain { + private static final String TAG = "TransitionChain"; + + /** + * Normal link type. This means the action was expected and is properly linked to the + * current chain. + */ + static final int TYPE_NORMAL = 0; + + /** + * This is the "default" link. It means we haven't done anything to properly track this case + * so it may or may not be correct. It represents the behavior as if there was no tracking. + * + * Any type that has "default" behavior uses the global "collecting transition" if it exists, + * otherwise it doesn't use any transition. + */ + static final int TYPE_DEFAULT = 1; + + /** + * This means the action was performed via a legacy code-path. These should be removed + * eventually. This will have the "default" behavior. + */ + static final int TYPE_LEGACY = 2; + + /** This is for a test. */ + static final int TYPE_TEST = 3; + + /** This is finishing a transition. Collection isn't supported during this. */ + static final int TYPE_FINISH = 4; + + /** + * Something unexpected happened so this action was started to recover from the unexpected + * state. This means that a "real" chain-link couldn't be determined. For now, the behavior of + * this is the same as "default". + */ + static final int TYPE_FAILSAFE = 5; + + /** + * Types of chain links (ie. how is this action associated with the chain it is linked to). + * @hide + */ + @IntDef(prefix = { "TYPE_" }, value = { + TYPE_NORMAL, + TYPE_DEFAULT, + TYPE_LEGACY, + TYPE_TEST, + TYPE_FINISH, + TYPE_FAILSAFE + }) + public @interface LinkType {} + + /** Identifies the entry-point of this action. */ + @NonNull + final String mSource; + + /** Reference to ATMS. TEMPORARY! ONLY USE THIS WHEN tracker_plumbing flag is DISABLED! */ + @Nullable + ActivityTaskManagerService mTmpAtm; + + /** The transition that this chain's changes belong to. */ + @Nullable + Transition mTransition; + + /** The previous action in the chain. */ + @Nullable + ActionChain mPrevious = null; + + /** Classification of how this action is connected to the chain. */ + @LinkType int mType = TYPE_NORMAL; + + /** When this Action started. */ + long mCreateTimeMs; + + private ActionChain(String source, @LinkType int type, Transition transit) { + mSource = source; + mCreateTimeMs = System.currentTimeMillis(); + mType = type; + mTransition = transit; + if (mTransition != null) { + mTransition.recordChain(this); + } + } + + private Transition getTransition() { + if (!Flags.transitTrackerPlumbing()) { + return mTmpAtm.getTransitionController().getCollectingTransition(); + } + return mTransition; + } + + boolean isFinishing() { + return mType == TYPE_FINISH; + } + + /** + * Some common checks to determine (and report) whether this chain has a collecting transition. + */ + private boolean expectCollecting() { + if (getTransition() == null) { + Slog.e(TAG, "Can't collect into a chain with no transition"); + return false; + } + if (isFinishing()) { + Slog.e(TAG, "Trying to collect into a finished transition"); + return false; + } + if (mTransition.mController.getCollectingTransition() != mTransition) { + Slog.e(TAG, "Mismatch between current collecting (" + + mTransition.mController.getCollectingTransition() + ") and chain (" + + mTransition + ")"); + return false; + } + return true; + } + + /** + * Helper to collect a container into the associated transition. This will automatically do + * nothing if the chain isn't associated with a collecting transition. + */ + void collect(@NonNull WindowContainer wc) { + if (!wc.mTransitionController.isShellTransitionsEnabled()) return; + if (!expectCollecting()) return; + getTransition().collect(wc); + } + + /** + * An interface for creating and tracking action chains. + */ + static class Tracker { + private final ActivityTaskManagerService mAtm; + + Tracker(ActivityTaskManagerService atm) { + mAtm = atm; + } + + private ActionChain makeChain(String source, @LinkType int type, Transition transit) { + final ActionChain out = new ActionChain(source, type, transit); + if (!Flags.transitTrackerPlumbing()) { + out.mTmpAtm = mAtm; + } + return out; + } + + private ActionChain makeChain(String source, @LinkType int type) { + return makeChain(source, type, + mAtm.getTransitionController().getCollectingTransition()); + } + + /** + * Starts tracking a normal action. + * @see #TYPE_NORMAL + */ + @NonNull + ActionChain start(String source, Transition transit) { + return makeChain(source, TYPE_NORMAL, transit); + } + + /** @see #TYPE_DEFAULT */ + @NonNull + ActionChain startDefault(String source) { + return makeChain(source, TYPE_DEFAULT); + } + + /** + * Starts tracking an action that finishes a transition. + * @see #TYPE_NORMAL + */ + @NonNull + ActionChain startFinish(String source, Transition finishTransit) { + return makeChain(source, TYPE_FINISH, finishTransit); + } + + /** @see #TYPE_LEGACY */ + @NonNull + ActionChain startLegacy(String source) { + return makeChain(source, TYPE_LEGACY, null); + } + + /** @see #TYPE_FAILSAFE */ + @NonNull + ActionChain startFailsafe(String source) { + return makeChain(source, TYPE_FAILSAFE); + } + } + + /** Helpers for usage in tests. */ + @NonNull + static ActionChain test() { + return new ActionChain("test", TYPE_TEST, null /* transition */); + } + + @NonNull + static ActionChain testFinish(Transition toFinish) { + return new ActionChain("test", TYPE_FINISH, toFinish); + } +} diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index fb2bf39c0b7b..2ce1aa422601 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -1275,10 +1275,8 @@ class ActivityMetricsLogger { final ActivityRecord r = info.mLastLaunchedActivity; final long lastTopLossTime = r.topResumedStateLossTime; final WindowManagerService wm = mSupervisor.mService.mWindowManager; - final Object controller = wm.getRecentsAnimationController(); mLoggerHandler.postDelayed(() -> { - if (lastTopLossTime != r.topResumedStateLossTime - || controller != wm.getRecentsAnimationController()) { + if (lastTopLossTime != r.topResumedStateLossTime) { // Skip if the animation was finished in a short time. return; } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 5ba8433146dc..21908d95c605 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -226,7 +226,6 @@ import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_F import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_NONE; import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_WINDOWING_MODE_RESIZE; import static com.android.server.wm.ActivityTaskManagerService.getInputDispatchingTimeoutMillisLocked; -import static com.android.server.wm.DesktopModeHelper.canEnterDesktopMode; import static com.android.server.wm.IdentifierProto.HASH_CODE; import static com.android.server.wm.IdentifierProto.TITLE; import static com.android.server.wm.IdentifierProto.USER_ID; @@ -234,7 +233,6 @@ import static com.android.server.wm.StartingData.AFTER_TRANSACTION_COPY_TO_CLIEN import static com.android.server.wm.StartingData.AFTER_TRANSACTION_REMOVE_DIRECTLY; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION; import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_VISIBLE; import static com.android.server.wm.TaskPersister.DEBUG; @@ -637,12 +635,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A private SizeConfigurationBuckets mSizeConfigurations; - /** - * The precomputed display insets for resolving configuration. It will be non-null if - * {@link #shouldCreateAppCompatDisplayInsets} returns {@code true}. - */ - private AppCompatDisplayInsets mAppCompatDisplayInsets; - @VisibleForTesting final TaskFragment.ConfigOverrideHint mResolveConfigHint; @@ -792,22 +784,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @NonNull final AppCompatController mAppCompatController; - /** - * The scale to fit at least one side of the activity to its parent. If the activity uses - * 1920x1080, and the actually size on the screen is 960x540, then the scale is 0.5. - */ - private float mSizeCompatScale = 1f; - - /** - * The bounds in global coordinates for activity in size compatibility mode. - * @see ActivityRecord#hasSizeCompatBounds() - */ - private Rect mSizeCompatBounds; - - // Whether this activity is in size compatibility mode because its bounds don't fit in parent - // naturally. - private boolean mInSizeCompatModeForBounds = false; - // Whether the activity is eligible to be letterboxed for fixed orientation with respect to its // requested orientation, even when it's letterbox for another reason (e.g., size compat mode) // and therefore #isLetterboxedForFixedOrientationAndAspectRatio returns false. @@ -1257,10 +1233,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mPendingRelaunchCount != 0) { pw.print(prefix); pw.print("mPendingRelaunchCount="); pw.println(mPendingRelaunchCount); } - if (mSizeCompatScale != 1f || mSizeCompatBounds != null) { - pw.println(prefix + "mSizeCompatScale=" + mSizeCompatScale + " mSizeCompatBounds=" - + mSizeCompatBounds); - } if (mRemovingFromDisplay) { pw.println(prefix + "mRemovingFromDisplay=" + mRemovingFromDisplay); } @@ -2647,7 +2619,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A || mStartingWindow == null || mTransferringSplashScreenState == TRANSFER_SPLASH_SCREEN_FINISH // skip copy splash screen to client if it was resized - || (mStartingData != null && mStartingData.mResizedFromTransfer)) { + || (mStartingData != null && mStartingData.mResizedFromTransfer) + || isRelaunching()) { return false; } if (isTransferringSplashScreen()) { @@ -4969,9 +4942,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A newIntents.add(intent); } - final boolean isSleeping() { - final Task rootTask = getRootTask(); - return rootTask != null ? rootTask.shouldSleepActivities() : mAtmService.isSleepingLocked(); + boolean isSleeping() { + return task != null ? task.shouldSleepActivities() : mAtmService.isSleepingLocked(); } /** @@ -4995,7 +4967,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final ReferrerIntent rintent = new ReferrerIntent(intent, getFilteredReferrer(referrer), callerToken); boolean unsent = true; - final boolean isTopActivityWhileSleeping = isTopRunningActivity() && isSleeping(); + final boolean isTopActivityWhileSleeping = isSleeping() && isTopRunningActivity(); // We want to immediately deliver the intent to the activity if: // - It is currently resumed or paused. i.e. it is currently visible to the user and we want @@ -5570,10 +5542,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return false; } if (!mDisplayContent.mAppTransition.isTransitionSet()) { - // Defer committing visibility for non-home app which is animating by recents. - if (isActivityTypeHome() || !isAnimating(PARENTS, ANIMATION_TYPE_RECENTS)) { - return false; - } + return false; } if (mWaitForEnteringPinnedMode && mVisible == visible) { // If the visibility is not changed during enter PIP, we don't want to include it in @@ -5727,8 +5696,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A private void postApplyAnimation(boolean visible, boolean fromTransition) { final boolean usingShellTransitions = mTransitionController.isShellTransitionsEnabled(); final boolean delayed = !usingShellTransitions && isAnimating(PARENTS | CHILDREN, - ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_WINDOW_ANIMATION - | ANIMATION_TYPE_RECENTS); + ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_WINDOW_ANIMATION); if (!delayed && !usingShellTransitions) { // We aren't delayed anything, but exiting windows rely on the animation finished // callback being called in case the ActivityRecord was pretending to be delayed, @@ -5750,7 +5718,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // animation and aren't in RESUMED state. Otherwise, we'll update client visibility in // onAnimationFinished or activityStopped. if (visible || (mState != RESUMED && (usingShellTransitions || !isAnimating( - PARENTS, ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS)))) { + PARENTS, ANIMATION_TYPE_APP_TRANSITION)))) { setClientVisible(visible); } @@ -5862,7 +5830,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A void setState(State state, String reason) { ProtoLog.v(WM_DEBUG_STATES, "State movement: %s from:%s to:%s reason:%s", - this, getState(), state, reason); + this, mState, state, reason); if (state == mState) { // No need to do anything if state doesn't change. @@ -6187,7 +6155,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Now for any activities that aren't visible to the user, make sure they no longer are // keeping the screen frozen. if (DEBUG_VISIBILITY) { - Slog.v(TAG_VISIBILITY, "Making invisible: " + this + ", state=" + getState()); + Slog.v(TAG_VISIBILITY, "Making invisible: " + this + ", state=" + mState); } try { final boolean canEnterPictureInPicture = checkEnterPictureInPictureState( @@ -6203,7 +6171,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } setVisibility(false); - switch (getState()) { + switch (mState) { case STOPPING: case STOPPED: // Reset the flag indicating that an app can enter picture-in-picture once the @@ -6439,7 +6407,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mTaskSupervisor.mStoppingActivities.remove(this); if (getDisplayArea().allResumedActivitiesComplete()) { // Construct the compat environment at a relatively stable state if needed. - updateAppCompatDisplayInsets(); + mAppCompatController.getAppCompatSizeCompatModePolicy().updateAppCompatDisplayInsets(); mRootWindowContainer.executeAppTransitionForAllDisplay(); } @@ -7728,7 +7696,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Ensure that the activity content is hidden when the decor surface is boosted to // prevent UI redressing attack. && !isDecorSurfaceBoosted) - || isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS + || isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_PREDICT_BACK); if (mSurfaceControl != null) { @@ -8072,7 +8040,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A != getRequestedConfigurationOrientation(false /*forDisplay */)) { // Do not change the requested configuration now, because this will be done when setting // the orientation below with the new mAppCompatDisplayInsets - clearSizeCompatModeAttributes(); + mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatModeAttributes(); } ProtoLog.v(WM_DEBUG_ORIENTATION, "Setting requested orientation %s for %s", @@ -8205,18 +8173,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @Nullable AppCompatDisplayInsets getAppCompatDisplayInsets() { - if (mAppCompatController.getTransparentPolicy().isRunning()) { - return mAppCompatController.getTransparentPolicy().getInheritedAppCompatDisplayInsets(); - } - return mAppCompatDisplayInsets; - } - - /** - * @return The {@code true} if the current instance has {@link mAppCompatDisplayInsets} without - * considering the inheritance implemented in {@link #getAppCompatDisplayInsets()} - */ - boolean hasAppCompatDisplayInsetsWithoutInheritance() { - return mAppCompatDisplayInsets != null; + return mAppCompatController.getAppCompatSizeCompatModePolicy().getAppCompatDisplayInsets(); } /** @@ -8224,7 +8181,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * density than its parent or its bounds don't fit in parent naturally. */ boolean inSizeCompatMode() { - if (mInSizeCompatModeForBounds) { + final AppCompatSizeCompatModePolicy scmPolicy = mAppCompatController + .getAppCompatSizeCompatModePolicy(); + if (scmPolicy.isInSizeCompatModeForBounds()) { return true; } if (getAppCompatDisplayInsets() == null || !shouldCreateAppCompatDisplayInsets() @@ -8321,69 +8280,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @Override boolean hasSizeCompatBounds() { - return mSizeCompatBounds != null; - } - - // TODO(b/36505427): Consider moving this method and similar ones to ConfigurationContainer. - private void updateAppCompatDisplayInsets() { - if (getAppCompatDisplayInsets() != null || !shouldCreateAppCompatDisplayInsets()) { - // The override configuration is set only once in size compatibility mode. - return; - } - - Configuration overrideConfig = getRequestedOverrideConfiguration(); - final Configuration fullConfig = getConfiguration(); - - // Ensure the screen related fields are set. It is used to prevent activity relaunch - // when moving between displays. For screenWidthDp and screenWidthDp, because they - // are relative to bounds and density, they will be calculated in - // {@link Task#computeConfigResourceOverrides} and the result will also be - // relatively fixed. - overrideConfig.colorMode = fullConfig.colorMode; - overrideConfig.densityDpi = fullConfig.densityDpi; - // The smallest screen width is the short side of screen bounds. Because the bounds - // and density won't be changed, smallestScreenWidthDp is also fixed. - overrideConfig.smallestScreenWidthDp = fullConfig.smallestScreenWidthDp; - if (ActivityInfo.isFixedOrientation(getOverrideOrientation())) { - // lock rotation too. When in size-compat, onConfigurationChanged will watch for and - // apply runtime rotation changes. - overrideConfig.windowConfiguration.setRotation( - fullConfig.windowConfiguration.getRotation()); - } - - final Rect letterboxedContainerBounds = mAppCompatController - .getAppCompatAspectRatioPolicy().getLetterboxedContainerBounds(); - - // The role of AppCompatDisplayInsets is like the override bounds. - mAppCompatDisplayInsets = - new AppCompatDisplayInsets( - mDisplayContent, this, letterboxedContainerBounds, - mResolveConfigHint.mUseOverrideInsetsForConfig); - } - - private void clearSizeCompatModeAttributes() { - mInSizeCompatModeForBounds = false; - final float lastSizeCompatScale = mSizeCompatScale; - mSizeCompatScale = 1f; - if (mSizeCompatScale != lastSizeCompatScale) { - forAllWindows(WindowState::updateGlobalScale, false /* traverseTopToBottom */); - } - mSizeCompatBounds = null; - mAppCompatDisplayInsets = null; - mAppCompatController.getTransparentPolicy().clearInheritedAppCompatDisplayInsets(); - } - - @VisibleForTesting - void clearSizeCompatMode() { - clearSizeCompatModeAttributes(); - // Clear config override in #updateAppCompatDisplayInsets(). - final int activityType = getActivityType(); - final Configuration overrideConfig = getRequestedOverrideConfiguration(); - overrideConfig.unset(); - // Keep the activity type which was set when attaching to a task to prevent leaving it - // undefined. - overrideConfig.windowConfiguration.setActivityType(activityType); - onRequestedOverrideConfigurationChanged(overrideConfig); + return mAppCompatController.getAppCompatSizeCompatModePolicy().hasSizeCompatBounds(); } @Override @@ -8401,7 +8298,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @Override float getCompatScale() { - return hasSizeCompatBounds() ? mSizeCompatScale : super.getCompatScale(); + // We need to invoke {#getCompatScale()} only if the CompatScale is not available. + return mAppCompatController.getAppCompatSizeCompatModePolicy() + .getCompatScaleIfAvailable(ActivityRecord.super::getCompatScale); } @Override @@ -8469,8 +8368,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A resolveAspectRatioRestriction(newParentConfiguration); } final AppCompatDisplayInsets appCompatDisplayInsets = getAppCompatDisplayInsets(); + final AppCompatSizeCompatModePolicy scmPolicy = + mAppCompatController.getAppCompatSizeCompatModePolicy(); if (appCompatDisplayInsets != null) { - resolveSizeCompatModeConfiguration(newParentConfiguration, appCompatDisplayInsets); + scmPolicy.resolveSizeCompatModeConfiguration(newParentConfiguration, + appCompatDisplayInsets, mTmpBounds); } else if (inMultiWindowMode() && !isFixedOrientationLetterboxAllowed) { // We ignore activities' requested orientation in multi-window modes. They may be // taken into consideration in resolveFixedOrientationConfiguration call above. @@ -8488,13 +8390,14 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (!Flags.immersiveAppRepositioning() && !mAppCompatController.getAppCompatAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio() - && !mInSizeCompatModeForBounds + && !scmPolicy.isInSizeCompatModeForBounds() && !mAppCompatController.getAppCompatAspectRatioOverrides() .hasFullscreenOverride()) { resolveAspectRatioRestriction(newParentConfiguration); } - if (isFixedOrientationLetterboxAllowed || mAppCompatDisplayInsets != null + if (isFixedOrientationLetterboxAllowed + || scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance() // In fullscreen, can be letterboxed for aspect ratio. || !inMultiWindowMode()) { updateResolvedBoundsPosition(newParentConfiguration); @@ -8502,7 +8405,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A boolean isIgnoreOrientationRequest = mDisplayContent != null && mDisplayContent.getIgnoreOrientationRequest(); - if (mAppCompatDisplayInsets == null + if (!scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance() // for size compat mode set in updateAppCompatDisplayInsets // Fixed orientation letterboxing is possible on both large screen devices // with ignoreOrientationRequest enabled and on phones in split screen even with @@ -8551,7 +8454,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A info.neverSandboxDisplayApis(sConstrainDisplayApisConfig), info.alwaysSandboxDisplayApis(sConstrainDisplayApisConfig), !matchParentBounds(), - mAppCompatDisplayInsets != null, + scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance(), shouldCreateAppCompatDisplayInsets()); } resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds); @@ -8575,7 +8478,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return Rect.copyOrNull(mResolveConfigHint.mParentAppBoundsOverride); } - private void computeConfigByResolveHint(@NonNull Configuration resolvedConfig, + void computeConfigByResolveHint(@NonNull Configuration resolvedConfig, @NonNull Configuration parentConfig) { task.computeConfigResourceOverrides(resolvedConfig, parentConfig, mResolveConfigHint); // Reset the temp info which should only take effect for the specified computation. @@ -8627,7 +8530,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mAppCompatController.getTransparentPolicy().isRunning()) { return mAppCompatController.getTransparentPolicy().getInheritedAppCompatState(); } - if (mInSizeCompatModeForBounds) { + final AppCompatSizeCompatModePolicy scmPolicy = mAppCompatController + .getAppCompatSizeCompatModePolicy(); + if (scmPolicy.isInSizeCompatModeForBounds()) { return APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE; } // Letterbox for fixed orientation. This check returns true only when an activity is @@ -8663,8 +8568,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (resolvedBounds.isEmpty()) { return; } - final Rect screenResolvedBounds = - mSizeCompatBounds != null ? mSizeCompatBounds : resolvedBounds; + final AppCompatSizeCompatModePolicy scmPolicy = + mAppCompatController.getAppCompatSizeCompatModePolicy(); + final Rect screenResolvedBounds = scmPolicy.replaceResolvedBoundsIfNeeded(resolvedBounds); final Rect parentAppBounds = mResolveConfigHint.mParentAppBoundsOverride; final Rect parentBounds = newParentConfiguration.windowConfiguration.getBounds(); final float screenResolvedBoundsWidth = screenResolvedBounds.width(); @@ -8720,14 +8626,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A - screenResolvedBounds.top + parentAppBounds.top); } } - - if (mSizeCompatBounds != null) { - mSizeCompatBounds.offset(offsetX , offsetY); - final int dy = mSizeCompatBounds.top - resolvedBounds.top; - final int dx = mSizeCompatBounds.left - resolvedBounds.left; - offsetBounds(resolvedConfig, dx, dy); - } else { - offsetBounds(resolvedConfig, offsetX, offsetY); + // If in SCM, apply offset to resolved bounds relative to size compat bounds. If + // not, apply directly to resolved bounds. + if (!scmPolicy.applyOffsetIfNeeded(resolvedBounds, resolvedConfig, offsetX, offsetY)) { + AppCompatUtils.offsetBounds(resolvedConfig, offsetX, offsetY); } // If the top is aligned with parentAppBounds add the vertical insets back so that the app @@ -8735,9 +8637,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (resolvedConfig.windowConfiguration.getAppBounds().top == parentAppBounds.top && !isImmersiveMode) { resolvedConfig.windowConfiguration.getBounds().top = parentBounds.top; - if (mSizeCompatBounds != null) { - mSizeCompatBounds.top = parentBounds.top; - } + scmPolicy.alignToTopIfNeeded(parentBounds); } // Since bounds has changed, the configuration needs to be computed accordingly. @@ -8747,13 +8647,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // easier to resolve the relative position in parent container. However, if the activity is // scaled, the position should follow the scale because the configuration will be sent to // the client which is expected to be in a scaled environment. - if (mSizeCompatScale != 1f) { - final int screenPosX = resolvedBounds.left; - final int screenPosY = resolvedBounds.top; - final int dx = (int) (screenPosX / mSizeCompatScale + 0.5f) - screenPosX; - final int dy = (int) (screenPosY / mSizeCompatScale + 0.5f) - screenPosY; - offsetBounds(resolvedConfig, dx, dy); - } + scmPolicy.applySizeCompatScaleIfNeeded(resolvedBounds, resolvedConfig); } boolean isImmersiveMode(@NonNull Rect parentBounds) { @@ -8775,7 +8669,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @NonNull Rect getScreenResolvedBounds() { final Configuration resolvedConfig = getResolvedOverrideConfiguration(); final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds(); - return mSizeCompatBounds != null ? mSizeCompatBounds : resolvedBounds; + final AppCompatSizeCompatModePolicy scmPolicy = + mAppCompatController.getAppCompatSizeCompatModePolicy(); + return scmPolicy.replaceResolvedBoundsIfNeeded(resolvedBounds); } void recomputeConfiguration() { @@ -8927,8 +8823,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return; } final AppCompatDisplayInsets mAppCompatDisplayInsets = getAppCompatDisplayInsets(); + final AppCompatSizeCompatModePolicy scmPolicy = + mAppCompatController.getAppCompatSizeCompatModePolicy(); - if (mAppCompatDisplayInsets != null + if (scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance() && !mAppCompatDisplayInsets.mIsInFixedOrientationOrAspectRatioLetterbox) { // App prefers to keep its original size. // If the size compat is from previous fixed orientation letterboxing, we may want to @@ -8981,7 +8879,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A .applyDesiredAspectRatio(newParentConfig, parentBounds, resolvedBounds, containingBoundsWithInsets, containingBounds); - if (mAppCompatDisplayInsets != null) { + if (scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance()) { mAppCompatDisplayInsets.getBoundsByRotation(mTmpBounds, newParentConfig.windowConfiguration.getRotation()); if (resolvedBounds.width() != mTmpBounds.width() @@ -9042,246 +8940,15 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } - /** - * Resolves consistent screen configuration for orientation and rotation changes without - * inheriting the parent bounds. - */ - private void resolveSizeCompatModeConfiguration(Configuration newParentConfiguration, - @NonNull AppCompatDisplayInsets appCompatDisplayInsets) { - final Configuration resolvedConfig = getResolvedOverrideConfiguration(); - final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds(); - - // When an activity needs to be letterboxed because of fixed orientation, use fixed - // orientation bounds (stored in resolved bounds) instead of parent bounds since the - // activity will be displayed within them even if it is in size compat mode. They should be - // saved here before resolved bounds are overridden below. - final boolean useResolvedBounds = Flags.immersiveAppRepositioning() - ? mAppCompatController.getAppCompatAspectRatioPolicy() - .isAspectRatioApplied() - : mAppCompatController.getAppCompatAspectRatioPolicy() - .isLetterboxedForFixedOrientationAndAspectRatio(); - final Rect containerBounds = useResolvedBounds - ? new Rect(resolvedBounds) - : newParentConfiguration.windowConfiguration.getBounds(); - final Rect containerAppBounds = useResolvedBounds - ? new Rect(resolvedConfig.windowConfiguration.getAppBounds()) - : mResolveConfigHint.mParentAppBoundsOverride; - - final int requestedOrientation = getRequestedConfigurationOrientation(); - final boolean orientationRequested = requestedOrientation != ORIENTATION_UNDEFINED; - final int parentOrientation = mResolveConfigHint.mUseOverrideInsetsForConfig - ? mResolveConfigHint.mTmpOverrideConfigOrientation - : newParentConfiguration.orientation; - final int orientation = orientationRequested - ? requestedOrientation - // We should use the original orientation of the activity when possible to avoid - // forcing the activity in the opposite orientation. - : appCompatDisplayInsets.mOriginalRequestedOrientation != ORIENTATION_UNDEFINED - ? appCompatDisplayInsets.mOriginalRequestedOrientation - : parentOrientation; - int rotation = newParentConfiguration.windowConfiguration.getRotation(); - final boolean isFixedToUserRotation = mDisplayContent == null - || mDisplayContent.getDisplayRotation().isFixedToUserRotation(); - if (!isFixedToUserRotation && !appCompatDisplayInsets.mIsFloating) { - // Use parent rotation because the original display can be rotated. - resolvedConfig.windowConfiguration.setRotation(rotation); - } else { - final int overrideRotation = resolvedConfig.windowConfiguration.getRotation(); - if (overrideRotation != ROTATION_UNDEFINED) { - rotation = overrideRotation; - } - } - - // Use compat insets to lock width and height. We should not use the parent width and height - // because apps in compat mode should have a constant width and height. The compat insets - // are locked when the app is first launched and are never changed after that, so we can - // rely on them to contain the original and unchanging width and height of the app. - final Rect containingAppBounds = new Rect(); - final Rect containingBounds = mTmpBounds; - appCompatDisplayInsets.getContainerBounds(containingAppBounds, containingBounds, rotation, - orientation, orientationRequested, isFixedToUserRotation); - resolvedBounds.set(containingBounds); - // The size of floating task is fixed (only swap), so the aspect ratio is already correct. - if (!appCompatDisplayInsets.mIsFloating) { - mAppCompatController.getAppCompatAspectRatioPolicy() - .applyAspectRatioForLetterbox(resolvedBounds, containingAppBounds, - containingBounds); - } - - // Use resolvedBounds to compute other override configurations such as appBounds. The bounds - // are calculated in compat container space. The actual position on screen will be applied - // later, so the calculation is simpler that doesn't need to involve offset from parent. - mResolveConfigHint.mTmpCompatInsets = appCompatDisplayInsets; - computeConfigByResolveHint(resolvedConfig, newParentConfiguration); - // Use current screen layout as source because the size of app is independent to parent. - resolvedConfig.screenLayout = computeScreenLayout( - getConfiguration().screenLayout, resolvedConfig.screenWidthDp, - resolvedConfig.screenHeightDp); - - // Use parent orientation if it cannot be decided by bounds, so the activity can fit inside - // the parent bounds appropriately. - if (resolvedConfig.screenWidthDp == resolvedConfig.screenHeightDp) { - resolvedConfig.orientation = parentOrientation; - } - - // Below figure is an example that puts an activity which was launched in a larger container - // into a smaller container. - // The outermost rectangle is the real display bounds. - // "@" is the container app bounds (parent bounds or fixed orientation bounds) - // "#" is the {@code resolvedBounds} that applies to application. - // "*" is the {@code mSizeCompatBounds} that used to show on screen if scaled. - // ------------------------------ - // | | - // | @@@@*********@@@@### | - // | @ * * @ # | - // | @ * * @ # | - // | @ * * @ # | - // | @@@@*********@@@@ # | - // ---------#--------------#----- - // # # - // ################ - // The application is still layouted in "#" since it was launched, and it will be visually - // scaled and positioned to "*". - - final Rect resolvedAppBounds = resolvedConfig.windowConfiguration.getAppBounds(); - - // Calculates the scale the size compatibility bounds into the region which is available - // to application. - final float lastSizeCompatScale = mSizeCompatScale; - updateSizeCompatScale(resolvedAppBounds, containerAppBounds); - - final int containerTopInset = containerAppBounds.top - containerBounds.top; - final boolean topNotAligned = - containerTopInset != resolvedAppBounds.top - resolvedBounds.top; - if (mSizeCompatScale != 1f || topNotAligned) { - if (mSizeCompatBounds == null) { - mSizeCompatBounds = new Rect(); - } - mSizeCompatBounds.set(resolvedAppBounds); - mSizeCompatBounds.offsetTo(0, 0); - mSizeCompatBounds.scale(mSizeCompatScale); - // The insets are included in height, e.g. the area of real cutout shouldn't be scaled. - mSizeCompatBounds.bottom += containerTopInset; - } else { - mSizeCompatBounds = null; - } - if (mSizeCompatScale != lastSizeCompatScale) { - forAllWindows(WindowState::updateGlobalScale, false /* traverseTopToBottom */); - } - - // The position will be later adjusted in updateResolvedBoundsPosition. - // Above coordinates are in "@" space, now place "*" and "#" to screen space. - final boolean fillContainer = resolvedBounds.equals(containingBounds); - final int screenPosX = fillContainer ? containerBounds.left : containerAppBounds.left; - final int screenPosY = fillContainer ? containerBounds.top : containerAppBounds.top; - - if (screenPosX != 0 || screenPosY != 0) { - if (mSizeCompatBounds != null) { - mSizeCompatBounds.offset(screenPosX, screenPosY); - } - // Add the global coordinates and remove the local coordinates. - final int dx = screenPosX - resolvedBounds.left; - final int dy = screenPosY - resolvedBounds.top; - offsetBounds(resolvedConfig, dx, dy); - } - - mInSizeCompatModeForBounds = - isInSizeCompatModeForBounds(resolvedAppBounds, containerAppBounds); - } - - void updateSizeCompatScale(Rect resolvedAppBounds, Rect containerAppBounds) { - mSizeCompatScale = mAppCompatController.getTransparentPolicy() - .findOpaqueNotFinishingActivityBelow() - .map(activityRecord -> activityRecord.mSizeCompatScale) - .orElseGet(() -> calculateSizeCompatScale(resolvedAppBounds, containerAppBounds)); - } - - private float calculateSizeCompatScale(Rect resolvedAppBounds, Rect containerAppBounds) { - final int contentW = resolvedAppBounds.width(); - final int contentH = resolvedAppBounds.height(); - final int viewportW = containerAppBounds.width(); - final int viewportH = containerAppBounds.height(); - // Allow an application to be up-scaled if its window is smaller than its - // original container or if it's a freeform window in desktop mode. - boolean shouldAllowUpscaling = !(contentW <= viewportW && contentH <= viewportH) - || (canEnterDesktopMode(mAtmService.mContext) - && getWindowingMode() == WINDOWING_MODE_FREEFORM); - return shouldAllowUpscaling ? Math.min( - (float) viewportW / contentW, (float) viewportH / contentH) : 1f; - } - - private boolean isInSizeCompatModeForBounds(final Rect appBounds, final Rect containerBounds) { - if (mAppCompatController.getTransparentPolicy().isRunning()) { - // To avoid wrong app behaviour, we decided to disable SCM when a translucent activity - // is letterboxed. - return false; - } - final int appWidth = appBounds.width(); - final int appHeight = appBounds.height(); - final int containerAppWidth = containerBounds.width(); - final int containerAppHeight = containerBounds.height(); - - if (containerAppWidth == appWidth && containerAppHeight == appHeight) { - // Matched the container bounds. - return false; - } - if (containerAppWidth > appWidth && containerAppHeight > appHeight) { - // Both sides are smaller than the container. - return true; - } - if (containerAppWidth < appWidth || containerAppHeight < appHeight) { - // One side is larger than the container. - return true; - } - - // The rest of the condition is that only one side is smaller than the container, but it - // still needs to exclude the cases where the size is limited by the fixed aspect ratio. - final float maxAspectRatio = getMaxAspectRatio(); - if (maxAspectRatio > 0) { - final float aspectRatio = (0.5f + Math.max(appWidth, appHeight)) - / Math.min(appWidth, appHeight); - if (aspectRatio >= maxAspectRatio) { - // The current size has reached the max aspect ratio. - return false; - } - } - final float minAspectRatio = getMinAspectRatio(); - if (minAspectRatio > 0) { - // The activity should have at least the min aspect ratio, so this checks if the - // container still has available space to provide larger aspect ratio. - final float containerAspectRatio = - (0.5f + Math.max(containerAppWidth, containerAppHeight)) - / Math.min(containerAppWidth, containerAppHeight); - if (containerAspectRatio <= minAspectRatio) { - // The long side has reached the parent. - return false; - } - } - return true; - } - - /** @return The horizontal / vertical offset of putting the content in the center of viewport.*/ - private static int getCenterOffset(int viewportDim, int contentDim) { - return (int) ((viewportDim - contentDim + 1) * 0.5f); - } - - private static void offsetBounds(Configuration inOutConfig, int offsetX, int offsetY) { - inOutConfig.windowConfiguration.getBounds().offset(offsetX, offsetY); - inOutConfig.windowConfiguration.getAppBounds().offset(offsetX, offsetY); - } - @Override public Rect getBounds() { // TODO(b/268458693): Refactor configuration inheritance in case of translucent activities final Rect superBounds = super.getBounds(); + final AppCompatSizeCompatModePolicy scmPolicy = + mAppCompatController.getAppCompatSizeCompatModePolicy(); return mAppCompatController.getTransparentPolicy().findOpaqueNotFinishingActivityBelow() .map(ActivityRecord::getBounds) - .orElseGet(() -> { - if (mSizeCompatBounds != null) { - return mSizeCompatBounds; - } - return superBounds; - }); + .orElseGet(() -> scmPolicy.getAppSizeCompatBoundsIfAvailable(superBounds)); } @Override @@ -9612,7 +9279,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mVisibleRequested) { // Calling from here rather than resolveOverrideConfiguration to ensure that this is // called after full config is updated in ConfigurationContainer#onConfigurationChanged. - updateAppCompatDisplayInsets(); + mAppCompatController.getAppCompatSizeCompatModePolicy().updateAppCompatDisplayInsets(); } // Short circuit: if the two full configurations are equal (the common case), then there is @@ -9952,7 +9619,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Reset the existing override configuration so it can be updated according to the latest // configuration. - clearSizeCompatMode(); + mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); if (!attachedToProcess()) { return; diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 2f74a9d5816b..03c7fd1e3ffe 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -438,13 +438,10 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { /** It is set from keyguard-going-away to set-keyguard-shown. */ static final int DEMOTE_TOP_REASON_DURING_UNLOCKING = 1; - /** It is set if legacy recents animation is running. */ - static final int DEMOTE_TOP_REASON_ANIMATING_RECENTS = 1 << 1; @Retention(RetentionPolicy.SOURCE) @IntDef({ DEMOTE_TOP_REASON_DURING_UNLOCKING, - DEMOTE_TOP_REASON_ANIMATING_RECENTS, }) @interface DemoteTopReason {} @@ -798,6 +795,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { WindowOrganizerController mWindowOrganizerController; TaskOrganizerController mTaskOrganizerController; TaskFragmentOrganizerController mTaskFragmentOrganizerController; + ActionChain.Tracker mChainTracker; @Nullable private BackgroundActivityStartCallback mBackgroundActivityStartCallback; @@ -872,6 +870,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mInternal = new LocalService(); GL_ES_VERSION = SystemProperties.getInt("ro.opengles.version", GL_ES_VERSION_UNDEFINED); mWindowOrganizerController = new WindowOrganizerController(this); + mChainTracker = new ActionChain.Tracker(this); mTaskOrganizerController = mWindowOrganizerController.mTaskOrganizerController; mTaskFragmentOrganizerController = mWindowOrganizerController.mTaskFragmentOrganizerController; @@ -1777,19 +1776,15 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { @Override public void preloadRecentsActivity(Intent intent) { enforceTaskPermission("preloadRecentsActivity()"); - final int callingPid = Binder.getCallingPid(); - final int callingUid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { final ComponentName recentsComponent = mRecentTasks.getRecentsComponent(); final String recentsFeatureId = mRecentTasks.getRecentsComponentFeatureId(); final int recentsUid = mRecentTasks.getRecentsComponentUid(); - final WindowProcessController caller = getProcessController(callingPid, callingUid); - final RecentsAnimation anim = new RecentsAnimation(this, mTaskSupervisor, - getActivityStartController(), mWindowManager, intent, recentsComponent, - recentsFeatureId, recentsUid, caller); + getActivityStartController(), intent, recentsComponent, + recentsFeatureId, recentsUid); anim.preloadRecentsActivity(); } } finally { @@ -3624,6 +3619,11 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { final long token = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { + boolean isPowerModePreApplied = false; + if (mPowerModeReasons == 0) { + startPowerMode(POWER_MODE_REASON_START_ACTIVITY); + isPowerModePreApplied = true; + } // Keyguard asked us to clear the home task snapshot before going away, so do that. if ((flags & KEYGUARD_GOING_AWAY_FLAG_TO_LAUNCHER_CLEAR_SNAPSHOT) != 0) { mActivityClientController.invalidateHomeTaskSnapshot(null /* token */); @@ -3632,9 +3632,19 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mDemoteTopAppReasons |= DEMOTE_TOP_REASON_DURING_UNLOCKING; } - mRootWindowContainer.forAllDisplays(displayContent -> { - mKeyguardController.keyguardGoingAway(displayContent.getDisplayId(), flags); - }); + boolean foundResumed = false; + for (int i = mRootWindowContainer.getChildCount() - 1; i >= 0; i--) { + final DisplayContent dc = mRootWindowContainer.getChildAt(i); + final boolean wasNoResumed = dc.mFocusedApp == null + || !dc.mFocusedApp.isState(RESUMED); + mKeyguardController.keyguardGoingAway(dc.mDisplayId, flags); + if (wasNoResumed && dc.mFocusedApp != null && dc.mFocusedApp.isState(RESUMED)) { + foundResumed = true; + } + } + if (isPowerModePreApplied && !foundResumed) { + endPowerMode(POWER_MODE_REASON_START_ACTIVITY); + } } WallpaperManagerInternal wallpaperManagerInternal = getWallpaperManagerInternal(); if (wallpaperManagerInternal != null) { diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java index 3c3b77374d70..173362c16728 100644 --- a/services/core/java/com/android/server/wm/AppCompatController.java +++ b/services/core/java/com/android/server/wm/AppCompatController.java @@ -46,6 +46,8 @@ class AppCompatController { private final AppCompatDeviceStateQuery mAppCompatDeviceStateQuery; @NonNull private final AppCompatLetterboxPolicy mAppCompatLetterboxPolicy; + @NonNull + private final AppCompatSizeCompatModePolicy mAppCompatSizeCompatModePolicy; AppCompatController(@NonNull WindowManagerService wmService, @NonNull ActivityRecord activityRecord) { @@ -67,6 +69,8 @@ class AppCompatController { wmService.mAppCompatConfiguration); mDesktopAppCompatAspectRatioPolicy = new DesktopAppCompatAspectRatioPolicy(activityRecord, mAppCompatOverrides, mTransparentPolicy, wmService.mAppCompatConfiguration); + mAppCompatSizeCompatModePolicy = new AppCompatSizeCompatModePolicy(mActivityRecord, + mAppCompatOverrides); } @NonNull @@ -152,9 +156,15 @@ class AppCompatController { return mAppCompatOverrides.getAppCompatLetterboxOverrides(); } + @NonNull + AppCompatSizeCompatModePolicy getAppCompatSizeCompatModePolicy() { + return mAppCompatSizeCompatModePolicy; + } + void dump(@NonNull PrintWriter pw, @NonNull String prefix) { getTransparentPolicy().dump(pw, prefix); getAppCompatLetterboxPolicy().dump(pw, prefix); + getAppCompatSizeCompatModePolicy().dump(pw, prefix); } } diff --git a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java new file mode 100644 index 000000000000..3be266e2951b --- /dev/null +++ b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java @@ -0,0 +1,439 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.app.WindowConfiguration.ROTATION_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.content.res.Configuration.ORIENTATION_UNDEFINED; + +import static com.android.server.wm.DesktopModeHelper.canEnterDesktopMode; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.graphics.Rect; + +import com.android.window.flags.Flags; + +import java.io.PrintWriter; +import java.util.function.DoubleSupplier; + +/** + * Encapsulate logic related to the SizeCompatMode. + */ +class AppCompatSizeCompatModePolicy { + + @NonNull + private final ActivityRecord mActivityRecord; + @NonNull + private final AppCompatOverrides mAppCompatOverrides; + + // Whether this activity is in size compatibility mode because its bounds don't fit in parent + // naturally. + private boolean mInSizeCompatModeForBounds = false; + /** + * The scale to fit at least one side of the activity to its parent. If the activity uses + * 1920x1080, and the actually size on the screen is 960x540, then the scale is 0.5. + */ + private float mSizeCompatScale = 1f; + + /** + * The bounds in global coordinates for activity in size compatibility mode. + * @see #hasSizeCompatBounds() + */ + private Rect mSizeCompatBounds; + + /** + * The precomputed display insets for resolving configuration. It will be non-null if + * {@link #shouldCreateAppCompatDisplayInsets} returns {@code true}. + */ + @Nullable + private AppCompatDisplayInsets mAppCompatDisplayInsets; + + AppCompatSizeCompatModePolicy(@NonNull ActivityRecord activityRecord, + @NonNull AppCompatOverrides appCompatOverrides) { + mActivityRecord = activityRecord; + mAppCompatOverrides = appCompatOverrides; + } + + boolean isInSizeCompatModeForBounds() { + return mInSizeCompatModeForBounds; + } + + void setInSizeCompatModeForBounds(boolean inSizeCompatModeForBounds) { + mInSizeCompatModeForBounds = inSizeCompatModeForBounds; + } + + boolean hasSizeCompatBounds() { + return mSizeCompatBounds != null; + } + + /** + * @return The {@code true} if the current instance has {@link mAppCompatDisplayInsets} without + * considering the inheritance implemented in {@link #getAppCompatDisplayInsets()} + */ + boolean hasAppCompatDisplayInsetsWithoutInheritance() { + return mAppCompatDisplayInsets != null; + } + + @Nullable + AppCompatDisplayInsets getAppCompatDisplayInsets() { + final TransparentPolicy transparentPolicy = mActivityRecord.mAppCompatController + .getTransparentPolicy(); + if (transparentPolicy.isRunning()) { + return transparentPolicy.getInheritedAppCompatDisplayInsets(); + } + return mAppCompatDisplayInsets; + } + + float getCompatScaleIfAvailable(@NonNull DoubleSupplier scaleWhenNotAvailable) { + return hasSizeCompatBounds() ? mSizeCompatScale + : (float) scaleWhenNotAvailable.getAsDouble(); + } + + @NonNull + Rect getAppSizeCompatBoundsIfAvailable(@NonNull Rect boundsWhenNotAvailable) { + return hasSizeCompatBounds() ? mSizeCompatBounds : boundsWhenNotAvailable; + } + + @NonNull + Rect replaceResolvedBoundsIfNeeded(@NonNull Rect resolvedBounds) { + return hasSizeCompatBounds() ? mSizeCompatBounds : resolvedBounds; + } + + boolean applyOffsetIfNeeded(@NonNull Rect resolvedBounds, + @NonNull Configuration resolvedConfig, int offsetX, int offsetY) { + if (hasSizeCompatBounds()) { + mSizeCompatBounds.offset(offsetX , offsetY); + final int dy = mSizeCompatBounds.top - resolvedBounds.top; + final int dx = mSizeCompatBounds.left - resolvedBounds.left; + AppCompatUtils.offsetBounds(resolvedConfig, dx, dy); + return true; + } + return false; + } + + void alignToTopIfNeeded(@NonNull Rect parentBounds) { + if (hasSizeCompatBounds()) { + mSizeCompatBounds.top = parentBounds.top; + } + } + + void applySizeCompatScaleIfNeeded(@NonNull Rect resolvedBounds, + @NonNull Configuration resolvedConfig) { + if (mSizeCompatScale != 1f) { + final int screenPosX = resolvedBounds.left; + final int screenPosY = resolvedBounds.top; + final int dx = (int) (screenPosX / mSizeCompatScale + 0.5f) - screenPosX; + final int dy = (int) (screenPosY / mSizeCompatScale + 0.5f) - screenPosY; + AppCompatUtils.offsetBounds(resolvedConfig, dx, dy); + } + } + + void updateSizeCompatScale(@NonNull Rect resolvedAppBounds, @NonNull Rect containerAppBounds) { + mSizeCompatScale = mActivityRecord.mAppCompatController.getTransparentPolicy() + .findOpaqueNotFinishingActivityBelow() + .map(activityRecord -> mSizeCompatScale) + .orElseGet(() -> calculateSizeCompatScale(resolvedAppBounds, containerAppBounds)); + } + + void clearSizeCompatModeAttributes() { + mInSizeCompatModeForBounds = false; + final float lastSizeCompatScale = mSizeCompatScale; + mSizeCompatScale = 1f; + if (mSizeCompatScale != lastSizeCompatScale) { + mActivityRecord.forAllWindows(WindowState::updateGlobalScale, + false /* traverseTopToBottom */); + } + mSizeCompatBounds = null; + mAppCompatDisplayInsets = null; + mActivityRecord.mAppCompatController.getTransparentPolicy() + .clearInheritedAppCompatDisplayInsets(); + } + + void clearSizeCompatMode() { + clearSizeCompatModeAttributes(); + // Clear config override in #updateAppCompatDisplayInsets(). + final int activityType = mActivityRecord.getActivityType(); + final Configuration overrideConfig = mActivityRecord.getRequestedOverrideConfiguration(); + overrideConfig.unset(); + // Keep the activity type which was set when attaching to a task to prevent leaving it + // undefined. + overrideConfig.windowConfiguration.setActivityType(activityType); + mActivityRecord.onRequestedOverrideConfigurationChanged(overrideConfig); + } + + void dump(@NonNull PrintWriter pw, @NonNull String prefix) { + if (mSizeCompatScale != 1f || hasSizeCompatBounds()) { + pw.println(prefix + "mSizeCompatScale=" + mSizeCompatScale + " mSizeCompatBounds=" + + mSizeCompatBounds); + } + } + + /** + * Resolves consistent screen configuration for orientation and rotation changes without + * inheriting the parent bounds. + */ + void resolveSizeCompatModeConfiguration(@NonNull Configuration newParentConfiguration, + @NonNull AppCompatDisplayInsets appCompatDisplayInsets, @NonNull Rect tmpBounds) { + final Configuration resolvedConfig = mActivityRecord.getResolvedOverrideConfiguration(); + final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds(); + + // When an activity needs to be letterboxed because of fixed orientation, use fixed + // orientation bounds (stored in resolved bounds) instead of parent bounds since the + // activity will be displayed within them even if it is in size compat mode. They should be + // saved here before resolved bounds are overridden below. + final AppCompatAspectRatioPolicy aspectRatioPolicy = mActivityRecord.mAppCompatController + .getAppCompatAspectRatioPolicy(); + final boolean useResolvedBounds = Flags.immersiveAppRepositioning() + ? aspectRatioPolicy.isAspectRatioApplied() + : aspectRatioPolicy.isLetterboxedForFixedOrientationAndAspectRatio(); + final Rect containerBounds = useResolvedBounds + ? new Rect(resolvedBounds) + : newParentConfiguration.windowConfiguration.getBounds(); + final Rect containerAppBounds = useResolvedBounds + ? new Rect(resolvedConfig.windowConfiguration.getAppBounds()) + : mActivityRecord.mResolveConfigHint.mParentAppBoundsOverride; + + final int requestedOrientation = mActivityRecord.getRequestedConfigurationOrientation(); + final boolean orientationRequested = requestedOrientation != ORIENTATION_UNDEFINED; + final int parentOrientation = mActivityRecord.mResolveConfigHint.mUseOverrideInsetsForConfig + ? mActivityRecord.mResolveConfigHint.mTmpOverrideConfigOrientation + : newParentConfiguration.orientation; + final int orientation = orientationRequested + ? requestedOrientation + // We should use the original orientation of the activity when possible to avoid + // forcing the activity in the opposite orientation. + : appCompatDisplayInsets.mOriginalRequestedOrientation != ORIENTATION_UNDEFINED + ? appCompatDisplayInsets.mOriginalRequestedOrientation + : parentOrientation; + int rotation = newParentConfiguration.windowConfiguration.getRotation(); + final boolean isFixedToUserRotation = mActivityRecord.mDisplayContent == null + || mActivityRecord.mDisplayContent.getDisplayRotation().isFixedToUserRotation(); + if (!isFixedToUserRotation && !appCompatDisplayInsets.mIsFloating) { + // Use parent rotation because the original display can be rotated. + resolvedConfig.windowConfiguration.setRotation(rotation); + } else { + final int overrideRotation = resolvedConfig.windowConfiguration.getRotation(); + if (overrideRotation != ROTATION_UNDEFINED) { + rotation = overrideRotation; + } + } + + // Use compat insets to lock width and height. We should not use the parent width and height + // because apps in compat mode should have a constant width and height. The compat insets + // are locked when the app is first launched and are never changed after that, so we can + // rely on them to contain the original and unchanging width and height of the app. + final Rect containingAppBounds = new Rect(); + final Rect containingBounds = tmpBounds; + appCompatDisplayInsets.getContainerBounds(containingAppBounds, containingBounds, rotation, + orientation, orientationRequested, isFixedToUserRotation); + resolvedBounds.set(containingBounds); + // The size of floating task is fixed (only swap), so the aspect ratio is already correct. + if (!appCompatDisplayInsets.mIsFloating) { + mActivityRecord.mAppCompatController.getAppCompatAspectRatioPolicy() + .applyAspectRatioForLetterbox(resolvedBounds, containingAppBounds, + containingBounds); + } + + // Use resolvedBounds to compute other override configurations such as appBounds. The bounds + // are calculated in compat container space. The actual position on screen will be applied + // later, so the calculation is simpler that doesn't need to involve offset from parent. + mActivityRecord.mResolveConfigHint.mTmpCompatInsets = appCompatDisplayInsets; + mActivityRecord.computeConfigByResolveHint(resolvedConfig, newParentConfiguration); + // Use current screen layout as source because the size of app is independent to parent. + resolvedConfig.screenLayout = ActivityRecord.computeScreenLayout( + mActivityRecord.getConfiguration().screenLayout, resolvedConfig.screenWidthDp, + resolvedConfig.screenHeightDp); + + // Use parent orientation if it cannot be decided by bounds, so the activity can fit inside + // the parent bounds appropriately. + if (resolvedConfig.screenWidthDp == resolvedConfig.screenHeightDp) { + resolvedConfig.orientation = parentOrientation; + } + + // Below figure is an example that puts an activity which was launched in a larger container + // into a smaller container. + // The outermost rectangle is the real display bounds. + // "@" is the container app bounds (parent bounds or fixed orientation bounds) + // "#" is the {@code resolvedBounds} that applies to application. + // "*" is the {@code mSizeCompatBounds} that used to show on screen if scaled. + // ------------------------------ + // | | + // | @@@@*********@@@@### | + // | @ * * @ # | + // | @ * * @ # | + // | @ * * @ # | + // | @@@@*********@@@@ # | + // ---------#--------------#----- + // # # + // ################ + // The application is still layouted in "#" since it was launched, and it will be visually + // scaled and positioned to "*". + + final Rect resolvedAppBounds = resolvedConfig.windowConfiguration.getAppBounds(); + // Calculates the scale the size compatibility bounds into the region which is available + // to application. + final float lastSizeCompatScale = mSizeCompatScale; + updateSizeCompatScale(resolvedAppBounds, containerAppBounds); + + final int containerTopInset = containerAppBounds.top - containerBounds.top; + final boolean topNotAligned = + containerTopInset != resolvedAppBounds.top - resolvedBounds.top; + if (mSizeCompatScale != 1f || topNotAligned) { + if (mSizeCompatBounds == null) { + mSizeCompatBounds = new Rect(); + } + mSizeCompatBounds.set(resolvedAppBounds); + mSizeCompatBounds.offsetTo(0, 0); + mSizeCompatBounds.scale(mSizeCompatScale); + // The insets are included in height, e.g. the area of real cutout shouldn't be scaled. + mSizeCompatBounds.bottom += containerTopInset; + } else { + mSizeCompatBounds = null; + } + if (mSizeCompatScale != lastSizeCompatScale) { + mActivityRecord.forAllWindows(WindowState::updateGlobalScale, + false /* traverseTopToBottom */); + } + + // The position will be later adjusted in updateResolvedBoundsPosition. + // Above coordinates are in "@" space, now place "*" and "#" to screen space. + final boolean fillContainer = resolvedBounds.equals(containingBounds); + final int screenPosX = fillContainer ? containerBounds.left : containerAppBounds.left; + final int screenPosY = fillContainer ? containerBounds.top : containerAppBounds.top; + + if (screenPosX != 0 || screenPosY != 0) { + if (hasSizeCompatBounds()) { + mSizeCompatBounds.offset(screenPosX, screenPosY); + } + // Add the global coordinates and remove the local coordinates. + final int dx = screenPosX - resolvedBounds.left; + final int dy = screenPosY - resolvedBounds.top; + AppCompatUtils.offsetBounds(resolvedConfig, dx, dy); + } + + mInSizeCompatModeForBounds = isInSizeCompatModeForBounds(resolvedAppBounds, + containerAppBounds); + } + + // TODO(b/36505427): Consider moving this method and similar ones to ConfigurationContainer. + void updateAppCompatDisplayInsets() { + if (getAppCompatDisplayInsets() != null + || !mActivityRecord.shouldCreateAppCompatDisplayInsets()) { + // The override configuration is set only once in size compatibility mode. + return; + } + + Configuration overrideConfig = mActivityRecord.getRequestedOverrideConfiguration(); + final Configuration fullConfig = mActivityRecord.getConfiguration(); + + // Ensure the screen related fields are set. It is used to prevent activity relaunch + // when moving between displays. For screenWidthDp and screenWidthDp, because they + // are relative to bounds and density, they will be calculated in + // {@link Task#computeConfigResourceOverrides} and the result will also be + // relatively fixed. + overrideConfig.colorMode = fullConfig.colorMode; + overrideConfig.densityDpi = fullConfig.densityDpi; + // The smallest screen width is the short side of screen bounds. Because the bounds + // and density won't be changed, smallestScreenWidthDp is also fixed. + overrideConfig.smallestScreenWidthDp = fullConfig.smallestScreenWidthDp; + if (ActivityInfo.isFixedOrientation(mActivityRecord.getOverrideOrientation())) { + // lock rotation too. When in size-compat, onConfigurationChanged will watch for and + // apply runtime rotation changes. + overrideConfig.windowConfiguration.setRotation( + fullConfig.windowConfiguration.getRotation()); + } + + final Rect letterboxedContainerBounds = mActivityRecord.mAppCompatController + .getAppCompatAspectRatioPolicy().getLetterboxedContainerBounds(); + + // The role of AppCompatDisplayInsets is like the override bounds. + mAppCompatDisplayInsets = + new AppCompatDisplayInsets(mActivityRecord.mDisplayContent, mActivityRecord, + letterboxedContainerBounds, mActivityRecord.mResolveConfigHint + .mUseOverrideInsetsForConfig); + } + + + private boolean isInSizeCompatModeForBounds(final @NonNull Rect appBounds, + final @NonNull Rect containerBounds) { + if (mActivityRecord.mAppCompatController.getTransparentPolicy().isRunning()) { + // To avoid wrong app behaviour, we decided to disable SCM when a translucent activity + // is letterboxed. + return false; + } + final int appWidth = appBounds.width(); + final int appHeight = appBounds.height(); + final int containerAppWidth = containerBounds.width(); + final int containerAppHeight = containerBounds.height(); + + if (containerAppWidth == appWidth && containerAppHeight == appHeight) { + // Matched the container bounds. + return false; + } + if (containerAppWidth > appWidth && containerAppHeight > appHeight) { + // Both sides are smaller than the container. + return true; + } + if (containerAppWidth < appWidth || containerAppHeight < appHeight) { + // One side is larger than the container. + return true; + } + + // The rest of the condition is that only one side is smaller than the container, but it + // still needs to exclude the cases where the size is limited by the fixed aspect ratio. + final float maxAspectRatio = mActivityRecord.getMaxAspectRatio(); + if (maxAspectRatio > 0) { + final float aspectRatio = (0.5f + Math.max(appWidth, appHeight)) + / Math.min(appWidth, appHeight); + if (aspectRatio >= maxAspectRatio) { + // The current size has reached the max aspect ratio. + return false; + } + } + final float minAspectRatio = mActivityRecord.getMinAspectRatio(); + if (minAspectRatio > 0) { + // The activity should have at least the min aspect ratio, so this checks if the + // container still has available space to provide larger aspect ratio. + final float containerAspectRatio = + (0.5f + Math.max(containerAppWidth, containerAppHeight)) + / Math.min(containerAppWidth, containerAppHeight); + if (containerAspectRatio <= minAspectRatio) { + // The long side has reached the parent. + return false; + } + } + return true; + } + + private float calculateSizeCompatScale(@NonNull Rect resolvedAppBounds, + @NonNull Rect containerAppBounds) { + final int contentW = resolvedAppBounds.width(); + final int contentH = resolvedAppBounds.height(); + final int viewportW = containerAppBounds.width(); + final int viewportH = containerAppBounds.height(); + // Allow an application to be up-scaled if its window is smaller than its + // original container or if it's a freeform window in desktop mode. + boolean shouldAllowUpscaling = !(contentW <= viewportW && contentH <= viewportH) + || (canEnterDesktopMode(mActivityRecord.mAtmService.mContext) + && mActivityRecord.getWindowingMode() == WINDOWING_MODE_FREEFORM); + return shouldAllowUpscaling ? Math.min( + (float) viewportW / contentW, (float) viewportH / contentH) : 1f; + } +} diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java index e3ff85171c0e..69421d0d5c96 100644 --- a/services/core/java/com/android/server/wm/AppCompatUtils.java +++ b/services/core/java/com/android/server/wm/AppCompatUtils.java @@ -251,6 +251,11 @@ final class AppCompatUtils { } } + static void offsetBounds(@NonNull Configuration inOutConfig, int offsetX, int offsetY) { + inOutConfig.windowConfiguration.getBounds().offset(offsetX, offsetY); + inOutConfig.windowConfiguration.getAppBounds().offset(offsetX, offsetY); + } + private static void clearAppCompatTaskInfo(@NonNull AppCompatTaskInfo info) { info.topActivityLetterboxVerticalPosition = TaskInfo.PROPERTY_VALUE_UNSET; info.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET; diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java index bc7e84ae9f86..90d33fbef748 100644 --- a/services/core/java/com/android/server/wm/AppTransition.java +++ b/services/core/java/com/android/server/wm/AppTransition.java @@ -134,8 +134,8 @@ import android.view.animation.TranslateAnimation; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.TransitionAnimation; -import com.android.internal.protolog.common.LogLevel; import com.android.internal.protolog.ProtoLog; +import com.android.internal.protolog.common.LogLevel; import com.android.internal.util.DumpUtils.Dump; import com.android.internal.util.function.pooled.PooledLambda; import com.android.internal.util.function.pooled.PooledPredicate; @@ -402,8 +402,7 @@ public class AppTransition implements Dump { mRemoteAnimationController.goodToGo(transit); } else if ((isTaskOpenTransitOld(transit) || transit == TRANSIT_OLD_WALLPAPER_CLOSE) && topOpeningAnim != null) { - if (mDisplayContent.getDisplayPolicy().shouldAttachNavBarToAppDuringTransition() - && mService.getRecentsAnimationController() == null) { + if (mDisplayContent.getDisplayPolicy().shouldAttachNavBarToAppDuringTransition()) { final NavBarFadeAnimationController controller = new NavBarFadeAnimationController(mDisplayContent); // For remote animation case, the nav bar fades out and in is controlled by the @@ -471,11 +470,9 @@ public class AppTransition implements Dump { } private boolean needsBoosting() { - final boolean recentsAnimRunning = mService.getRecentsAnimationController() != null; return !mNextAppTransitionRequests.isEmpty() || mAppTransitionState == APP_STATE_READY - || mAppTransitionState == APP_STATE_RUNNING - || recentsAnimRunning; + || mAppTransitionState == APP_STATE_RUNNING; } void registerListenerLocked(AppTransitionListener listener) { diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index 5a0cbf3e5f83..197bd5a308cd 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -68,7 +68,6 @@ import static com.android.server.wm.AppTransition.isNormalTransit; import static com.android.server.wm.NonAppWindowAnimationAdapter.shouldAttachNavBarToApp; import static com.android.server.wm.NonAppWindowAnimationAdapter.shouldStartNonAppWindowAnimationsForKeyguardExit; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS; import static com.android.server.wm.WallpaperAnimationAdapter.shouldStartWallpaperAnimation; import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; @@ -173,41 +172,6 @@ public class AppTransitionController { || !transitionGoodToGoForTaskFragments()) { return; } - final boolean isRecentsInOpening = mDisplayContent.mOpeningApps.stream().anyMatch( - ConfigurationContainer::isActivityTypeRecents); - // In order to avoid visual clutter caused by a conflict between app transition - // animation and recents animation, app transition is delayed until recents finishes. - // One exceptional case. When 3P launcher is used and a user taps a task screenshot in - // task switcher (isRecentsInOpening=true), app transition must start even though - // recents is running. Otherwise app transition is blocked until timeout (b/232984498). - // When 1P launcher is used, this animation is controlled by the launcher outside of - // the app transition, so delaying app transition doesn't cause visible delay. After - // recents finishes, app transition is handled just to commit visibility on apps. - if (!isRecentsInOpening) { - final ArraySet<WindowContainer> participants = new ArraySet<>(); - participants.addAll(mDisplayContent.mOpeningApps); - participants.addAll(mDisplayContent.mChangingContainers); - boolean deferForRecents = false; - for (int i = 0; i < participants.size(); i++) { - WindowContainer wc = participants.valueAt(i); - final ActivityRecord activity = getAppFromContainer(wc); - if (activity == null) { - continue; - } - // Don't defer recents animation if one of activity isn't running for it, that one - // might be started from quickstep. - if (!activity.isAnimating(PARENTS, ANIMATION_TYPE_RECENTS)) { - deferForRecents = false; - break; - } - deferForRecents = true; - } - if (deferForRecents) { - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, - "Delaying app transition for recents animation to finish"); - return; - } - } Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "AppTransitionReady"); @@ -1032,12 +996,8 @@ public class AppTransitionController { private void applyAnimations(ArraySet<ActivityRecord> openingApps, ArraySet<ActivityRecord> closingApps, @TransitionOldType int transit, LayoutParams animLp, boolean voiceInteraction) { - final RecentsAnimationController rac = mService.getRecentsAnimationController(); if (transit == WindowManager.TRANSIT_OLD_UNSET || (openingApps.isEmpty() && closingApps.isEmpty())) { - if (rac != null) { - rac.sendTasksAppeared(); - } return; } @@ -1075,9 +1035,6 @@ public class AppTransitionController { voiceInteraction); applyAnimations(closingWcs, closingApps, transit, false /* visible */, animLp, voiceInteraction); - if (rac != null) { - rac.sendTasksAppeared(); - } for (int i = 0; i < openingApps.size(); ++i) { openingApps.valueAtUnchecked(i).mOverrideTaskTransition = false; diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java index eb85c1a73212..f0a6e9ec1d4f 100644 --- a/services/core/java/com/android/server/wm/AsyncRotationController.java +++ b/services/core/java/com/android/server/wm/AsyncRotationController.java @@ -174,11 +174,6 @@ class AsyncRotationController extends FadeAnimationController implements Consume mNavBarToken = w.mToken; // Do not animate movable navigation bar (e.g. 3-buttons mode). if (navigationBarCanMove) return; - // Or when the navigation bar is currently controlled by recents animation. - final RecentsAnimationController recents = mService.getRecentsAnimationController(); - if (recents != null && recents.isNavigationBarAttachedToApp()) { - return; - } } else if (navigationBarCanMove || mTransitionOp == OP_CHANGE_MAY_SEAMLESS || mDisplayContent.mTransitionController.mNavigationBarAttachedToApp) { action = Operation.ACTION_SEAMLESS; diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 87867f6ab7d2..2cbd7f22fcba 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -1820,8 +1820,10 @@ class BackNavigationController { mNavigationMonitor.cancelBackNavigating("cancelAnimation"); mBackAnimationAdapter.getRunner().onAnimationCancelled(); } else { - mBackAnimationAdapter.getRunner().onAnimationStart( - targets, null, null, callback); + mBackAnimationAdapter.getRunner().onAnimationStart(targets, + mOpenAnimAdaptor.mPreparedOpenTransition != null + ? mOpenAnimAdaptor.mPreparedOpenTransition.getToken() + : null, callback); } } catch (RemoteException e) { e.printStackTrace(); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 9bf255529768..0597ed7a1c41 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1936,7 +1936,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return false; } if (mLastWallpaperVisible && r.windowsCanBeWallpaperTarget() - && mFixedRotationTransitionListener.mAnimatingRecents == null && !mTransitionController.isTransientLaunch(r)) { // Use normal rotation animation for orientation change of visible wallpaper if recents // animation is not running (it may be swiping to home). @@ -1962,9 +1961,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** Returns {@code true} if the top activity is transformed with the new rotation of display. */ boolean hasTopFixedRotationLaunchingApp() { - return mFixedRotationLaunchingApp != null - // Ignore animating recents because it hasn't really become the top. - && mFixedRotationLaunchingApp != mFixedRotationTransitionListener.mAnimatingRecents; + return mFixedRotationLaunchingApp != null; } /** It usually means whether the recents activity is launching with a different rotation. */ @@ -1991,8 +1988,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mWmService.mDisplayNotificationController.dispatchFixedRotationStarted(this, rotation); // Delay the hide animation to avoid blinking by clicking navigation bar that may // toggle fixed rotation in a short time. - final boolean shouldDebounce = r == mFixedRotationTransitionListener.mAnimatingRecents - || mTransitionController.isTransientLaunch(r); + final boolean shouldDebounce = mTransitionController.isTransientLaunch(r); startAsyncRotation(shouldDebounce); } else if (mFixedRotationLaunchingApp != null && r == null) { mWmService.mDisplayNotificationController.dispatchFixedRotationFinished(this); @@ -2024,12 +2020,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // the heavy operations. This also benefits that the states of multiple activities // are handled together. r.linkFixedRotationTransform(prevRotatedLaunchingApp); - if (r != mFixedRotationTransitionListener.mAnimatingRecents) { - // Only update the record for normal activity so the display orientation can be - // updated when the transition is done if it becomes the top. And the case of - // recents can be handled when the recents animation is finished. - setFixedRotationLaunchingAppUnchecked(r, rotation); - } + // Only update the record for normal activity so the display orientation can be + // updated when the transition is done if it becomes the top. + setFixedRotationLaunchingAppUnchecked(r, rotation); return; } @@ -5899,18 +5892,13 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp final Region local = Region.obtain(); final int[] remainingLeftRight = {mSystemGestureExclusionLimit, mSystemGestureExclusionLimit}; - final RecentsAnimationController recentsAnimationController = - mWmService.getRecentsAnimationController(); // Traverse all windows top down to assemble the gesture exclusion rects. // For each window, we only take the rects that fall within its touchable region. forAllWindows(w -> { - final boolean ignoreRecentsAnimationTarget = recentsAnimationController != null - && recentsAnimationController.shouldApplyInputConsumer(w.getActivityRecord()); if (!w.canReceiveTouchInput() || !w.isVisible() || (w.getAttrs().flags & FLAG_NOT_TOUCHABLE) != 0 - || unhandled.isEmpty() - || ignoreRecentsAnimationTarget) { + || unhandled.isEmpty()) { return; } @@ -6122,16 +6110,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp void getKeepClearAreas(Set<Rect> outRestricted, Set<Rect> outUnrestricted) { final Matrix tmpMatrix = new Matrix(); final float[] tmpFloat9 = new float[9]; - final RecentsAnimationController recentsAnimationController = - mWmService.getRecentsAnimationController(); forAllWindows(w -> { - // Skip the window if it is part of Recents animation - final boolean ignoreRecentsAnimationTarget = recentsAnimationController != null - && recentsAnimationController.shouldApplyInputConsumer(w.getActivityRecord()); - if (ignoreRecentsAnimationTarget) { - return false; // continue traversal - } - if (w.isVisible() && !w.inPinnedWindowingMode()) { w.getKeepClearAreas(outRestricted, outUnrestricted, tmpMatrix, tmpFloat9); @@ -6306,14 +6285,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } boolean updateDisplayOverrideConfigurationLocked() { - // Preemptively cancel the running recents animation -- SysUI can't currently handle this - // case properly since the signals it receives all happen post-change - final RecentsAnimationController recentsAnimationController = - mWmService.getRecentsAnimationController(); - if (recentsAnimationController != null) { - recentsAnimationController.cancelAnimationForDisplayChange(); - } - Configuration values = new Configuration(); computeScreenConfiguration(values); @@ -6914,79 +6885,11 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** The entry for proceeding to handle {@link #mFixedRotationLaunchingApp}. */ class FixedRotationTransitionListener extends WindowManagerInternal.AppTransitionListener { - /** - * The animating activity which shows the recents task list. It is set between - * {@link RecentsAnimationController#initialize} and - * {@link RecentsAnimationController#cleanupAnimation}. - */ - private ActivityRecord mAnimatingRecents; - - /** Whether {@link #mAnimatingRecents} is going to be the top activity. */ - private boolean mRecentsWillBeTop; - FixedRotationTransitionListener(int displayId) { super(displayId); } /** - * If the recents activity has a fixed orientation which is different from the current top - * activity, it will be rotated before being shown so we avoid a screen rotation animation - * when showing the Recents view. - */ - void onStartRecentsAnimation(@NonNull ActivityRecord r) { - mAnimatingRecents = r; - if (r.isVisible() && mFocusedApp != null && !mFocusedApp.occludesParent()) { - // The recents activity has shown with the orientation determined by the top - // activity, keep its current orientation to avoid flicking by the configuration - // change of visible activity. - return; - } - rotateInDifferentOrientationIfNeeded(r); - if (r.hasFixedRotationTransform()) { - // Set the record so we can recognize it to continue to update display orientation - // if the recents activity becomes the top later. - setFixedRotationLaunchingApp(r, r.getWindowConfiguration().getRotation()); - } - } - - /** - * If {@link #mAnimatingRecents} still has fixed rotation, it should be moved to top so we - * don't clear {@link #mFixedRotationLaunchingApp} that will be handled by transition. - */ - void onFinishRecentsAnimation() { - final ActivityRecord animatingRecents = mAnimatingRecents; - final boolean recentsWillBeTop = mRecentsWillBeTop; - mAnimatingRecents = null; - mRecentsWillBeTop = false; - if (recentsWillBeTop) { - // The recents activity will be the top, such as staying at recents list or - // returning to home (if home and recents are the same activity). - return; - } - - if (animatingRecents != null && animatingRecents == mFixedRotationLaunchingApp - && animatingRecents.isVisible() && animatingRecents != topRunningActivity()) { - // The recents activity should be going to be invisible (switch to another app or - // return to original top). Only clear the top launching record without finishing - // the transform immediately because it won't affect display orientation. And before - // the visibility is committed, the recents activity may perform relayout which may - // cause unexpected configuration change if the rotated configuration is restored. - // The transform will be finished when the transition is done. - setFixedRotationLaunchingAppUnchecked(null); - } else { - // If there is already a launching activity that is not the recents, before its - // transition is completed, the recents animation may be started. So if the recents - // activity won't be the top, the display orientation should be updated according - // to the current top activity. - continueUpdateOrientationForDiffOrienLaunchingApp(); - } - } - - void notifyRecentsWillBeTop() { - mRecentsWillBeTop = true; - } - - /** * Returns {@code true} if the transient launch (e.g. recents animation) requested a fixed * orientation, then the rotation change should be deferred. */ @@ -6996,8 +6899,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (hasFixedRotationTransientLaunch()) { source = mFixedRotationLaunchingApp; } - } else if (mAnimatingRecents != null && !hasTopFixedRotationLaunchingApp()) { - source = mAnimatingRecents; } if (source == null || source.getRequestedConfigurationOrientation( true /* forDisplay */) == ORIENTATION_UNDEFINED) { @@ -7010,19 +6911,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp @Override public void onAppTransitionFinishedLocked(IBinder token) { final ActivityRecord r = ActivityRecord.forTokenLocked(token); - // Ignore the animating recents so the fixed rotation transform won't be switched twice - // by finishing the recents animation and moving it to top. That also avoids flickering - // due to wait for previous activity to be paused if it supports PiP that ignores the - // effect of resume-while-pausing. - if (r == null || r == mAnimatingRecents) { - return; - } - if (mAnimatingRecents != null && mRecentsWillBeTop) { - // The activity is not the recents and it should be moved to back later, so it is - // better to keep its current appearance for the next transition. Otherwise the - // display orientation may be updated too early and the layout procedures at the - // end of finishing recents animation is skipped. That causes flickering because - // the surface of closing app hasn't updated to invisible. + if (r == null) { return; } if (mFixedRotationLaunchingApp == null) { diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index a5da5e7cc0de..5200e820fc02 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -611,16 +611,6 @@ public class DisplayRotation { mDisplayRotationCoordinator.onDefaultDisplayRotationChanged(rotation); } - // Preemptively cancel the running recents animation -- SysUI can't currently handle this - // case properly since the signals it receives all happen post-change. We do this earlier - // in the rotation flow, since DisplayContent.updateDisplayOverrideConfigurationLocked seems - // to happen too late. - final RecentsAnimationController recentsAnimationController = - mService.getRecentsAnimationController(); - if (recentsAnimationController != null) { - recentsAnimationController.cancelAnimationForDisplayChange(); - } - ProtoLog.v(WM_DEBUG_ORIENTATION, "Display id=%d rotation changed to %d from %d, lastOrientation=%d", displayId, rotation, oldRotation, lastOrientation); diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java index b8869f159169..ddbfd70ea4c4 100644 --- a/services/core/java/com/android/server/wm/InputMonitor.java +++ b/services/core/java/com/android/server/wm/InputMonitor.java @@ -49,7 +49,6 @@ import static com.android.server.wm.WindowManagerService.LOGTAG_INPUT_FOCUS; import static java.lang.Integer.MAX_VALUE; import android.annotation.Nullable; -import android.graphics.Rect; import android.graphics.Region; import android.os.Handler; import android.os.IBinder; @@ -111,7 +110,7 @@ final class InputMonitor { * draw the live-tile above the recents activity, we also need to provide that activity as a * z-layering reference so that we can place the recents input consumer above it. */ - private WeakReference<ActivityRecord> mActiveRecentsActivity = null; + private WeakReference<Task> mActiveRecentsTask = null; private WeakReference<Task> mActiveRecentsLayerRef = null; private class UpdateInputWindows implements Runnable { @@ -388,13 +387,13 @@ final class InputMonitor { /** * Inform InputMonitor when recents is active so it can enable the recents input consumer. - * @param activity The active recents activity. {@code null} means recents is not active. + * @param task The active recents task. {@code null} means recents is not active. * @param layer A task whose Z-layer is used as a reference for how to sort the consumer. */ - void setActiveRecents(@Nullable ActivityRecord activity, @Nullable Task layer) { - final boolean clear = activity == null; - final boolean wasActive = mActiveRecentsActivity != null && mActiveRecentsLayerRef != null; - mActiveRecentsActivity = clear ? null : new WeakReference<>(activity); + void setActiveRecents(@Nullable Task task, @Nullable Task layer) { + final boolean clear = task == null; + final boolean wasActive = mActiveRecentsTask != null && mActiveRecentsLayerRef != null; + mActiveRecentsTask = clear ? null : new WeakReference<>(task); mActiveRecentsLayerRef = clear ? null : new WeakReference<>(layer); if (clear && wasActive) { setUpdateInputWindowsNeededLw(); @@ -413,17 +412,12 @@ final class InputMonitor { // Request focus for the recents animation input consumer if an input consumer should // be applied for the window. if (recentsAnimationInputConsumer != null && focus != null) { - final RecentsAnimationController recentsAnimationController = - mService.getRecentsAnimationController(); // Apply recents input consumer when the focusing window is in recents animation. - final boolean shouldApplyRecentsInputConsumer = (recentsAnimationController != null - && recentsAnimationController.shouldApplyInputConsumer(focus.mActivityRecord)) - // Shell transitions doesn't use RecentsAnimationController but we still - // have carryover legacy logic that relies on the consumer. - || (getWeak(mActiveRecentsActivity) != null && focus.inTransition() + final boolean shouldApplyRecentsInputConsumer = + getWeak(mActiveRecentsTask) != null && focus.inTransition() // only take focus from the recents activity to avoid intercepting // events before the gesture officially starts. - && focus.isActivityTypeHomeOrRecents()); + && focus.isActivityTypeHomeOrRecents(); if (shouldApplyRecentsInputConsumer) { if (mInputFocus != recentsAnimationInputConsumer.mWindowHandle.token) { requestFocus(recentsAnimationInputConsumer.mWindowHandle.token, @@ -569,7 +563,6 @@ final class InputMonitor { private boolean mAddRecentsAnimationInputConsumerHandle; private boolean mInDrag; - private final Rect mTmpRect = new Rect(); private void updateInputWindows(boolean inDrag) { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "updateInputWindows"); @@ -586,18 +579,15 @@ final class InputMonitor { resetInputConsumers(mInputTransaction); // Update recents input consumer layer if active - final ActivityRecord activeRecents = getWeak(mActiveRecentsActivity); + final Task activeRecents = getWeak(mActiveRecentsTask); if (mAddRecentsAnimationInputConsumerHandle && activeRecents != null && activeRecents.getSurfaceControl() != null) { WindowContainer layer = getWeak(mActiveRecentsLayerRef); layer = layer != null ? layer : activeRecents; // Handle edge-case for SUW where windows don't exist yet if (layer.getSurfaceControl() != null) { - final WindowState targetAppMainWindow = activeRecents.findMainWindow(); - if (targetAppMainWindow != null) { - targetAppMainWindow.getBounds(mTmpRect); - mRecentsAnimationInputConsumer.mWindowHandle.touchableRegion.set(mTmpRect); - } + mRecentsAnimationInputConsumer.mWindowHandle.touchableRegion.set( + activeRecents.getBounds()); mRecentsAnimationInputConsumer.show(mInputTransaction, layer); mAddRecentsAnimationInputConsumerHandle = false; } @@ -629,24 +619,6 @@ final class InputMonitor { return; } - // This only works for legacy transitions. - final RecentsAnimationController recentsAnimationController = - mService.getRecentsAnimationController(); - final boolean shouldApplyRecentsInputConsumer = recentsAnimationController != null - && recentsAnimationController.shouldApplyInputConsumer(w.mActivityRecord); - if (mAddRecentsAnimationInputConsumerHandle && shouldApplyRecentsInputConsumer) { - if (recentsAnimationController.updateInputConsumerForApp( - mRecentsAnimationInputConsumer.mWindowHandle)) { - final DisplayArea targetDA = - recentsAnimationController.getTargetAppDisplayArea(); - if (targetDA != null) { - mRecentsAnimationInputConsumer.reparent(mInputTransaction, targetDA); - mRecentsAnimationInputConsumer.show(mInputTransaction, MAX_VALUE - 2); - mAddRecentsAnimationInputConsumerHandle = false; - } - } - } - if (w.inPinnedWindowingMode()) { if (mAddPipInputConsumerHandle) { final Task rootTask = w.getTask().getRootTask(); diff --git a/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java b/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java index 1a895ea22f36..403d3bd5c008 100644 --- a/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java +++ b/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java @@ -91,7 +91,6 @@ class NonAppWindowAnimationAdapter implements AnimationAdapter { return (transit == TRANSIT_OLD_TASK_OPEN || transit == TRANSIT_OLD_TASK_TO_FRONT || transit == TRANSIT_OLD_WALLPAPER_CLOSE) && displayContent.getDisplayPolicy().shouldAttachNavBarToAppDuringTransition() - && service.getRecentsAnimationController() == null && displayContent.getAsyncRotationController() == null; } diff --git a/services/core/java/com/android/server/wm/RecentsAnimation.java b/services/core/java/com/android/server/wm/RecentsAnimation.java index c592caf488ad..c06efc775403 100644 --- a/services/core/java/com/android/server/wm/RecentsAnimation.java +++ b/services/core/java/com/android/server/wm/RecentsAnimation.java @@ -17,82 +17,54 @@ package com.android.server.wm; import static android.app.ActivityManager.PROCESS_STATE_CACHED_ACTIVITY; -import static android.app.ActivityManager.START_TASK_TO_FRONT; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION; -import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RECENTS_ANIMATIONS; import static com.android.server.wm.ActivityRecord.State.STOPPED; import static com.android.server.wm.ActivityRecord.State.STOPPING; -import static com.android.server.wm.RecentsAnimationController.REORDER_KEEP_IN_PLACE; -import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_ORIGINAL_POSITION; -import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_TOP; -import static com.android.server.wm.TaskDisplayArea.getRootTaskAbove; import android.annotation.Nullable; import android.app.ActivityOptions; import android.content.ComponentName; import android.content.Intent; -import android.os.RemoteException; -import android.os.Trace; import android.util.Slog; -import android.view.IRecentsAnimationRunner; import com.android.internal.protolog.ProtoLog; import com.android.internal.util.function.pooled.PooledLambda; import com.android.internal.util.function.pooled.PooledPredicate; -import com.android.server.wm.ActivityMetricsLogger.LaunchingState; -import com.android.server.wm.RecentsAnimationController.RecentsAnimationCallbacks; -import com.android.server.wm.TaskDisplayArea.OnRootTaskOrderChangedListener; /** * Manages the recents animation, including the reordering of the root tasks for the transition and * cleanup. See {@link com.android.server.wm.RecentsAnimationController}. */ -class RecentsAnimation implements RecentsAnimationCallbacks, OnRootTaskOrderChangedListener { +class RecentsAnimation { private static final String TAG = RecentsAnimation.class.getSimpleName(); - private final ActivityTaskManagerService mService; private final ActivityTaskSupervisor mTaskSupervisor; private final ActivityStartController mActivityStartController; - private final WindowManagerService mWindowManager; private final TaskDisplayArea mDefaultTaskDisplayArea; private final Intent mTargetIntent; private final ComponentName mRecentsComponent; private final @Nullable String mRecentsFeatureId; private final int mRecentsUid; - private final @Nullable WindowProcessController mCaller; private final int mUserId; private final int mTargetActivityType; - /** - * The activity which has been launched behind. We need to remember the activity because the - * target root task may have other activities, then we are able to restore the launch-behind - * state for the exact activity. - */ - private ActivityRecord mLaunchedTargetActivity; - - // The root task to restore the target root task behind when the animation is finished - private Task mRestoreTargetBehindRootTask; - RecentsAnimation(ActivityTaskManagerService atm, ActivityTaskSupervisor taskSupervisor, - ActivityStartController activityStartController, WindowManagerService wm, + ActivityStartController activityStartController, Intent targetIntent, ComponentName recentsComponent, @Nullable String recentsFeatureId, - int recentsUid, @Nullable WindowProcessController caller) { - mService = atm; + int recentsUid) { mTaskSupervisor = taskSupervisor; - mDefaultTaskDisplayArea = mService.mRootWindowContainer.getDefaultTaskDisplayArea(); + mDefaultTaskDisplayArea = atm.mRootWindowContainer.getDefaultTaskDisplayArea(); mActivityStartController = activityStartController; - mWindowManager = wm; mTargetIntent = targetIntent; mRecentsComponent = recentsComponent; mRecentsFeatureId = recentsFeatureId; mRecentsUid = recentsUid; - mCaller = caller; mUserId = atm.getCurrentUserId(); mTargetActivityType = targetIntent.getComponent() != null && recentsComponent.equals(targetIntent.getComponent()) @@ -171,310 +143,6 @@ class RecentsAnimation implements RecentsAnimationCallbacks, OnRootTaskOrderChan } } - void startRecentsActivity(IRecentsAnimationRunner recentsAnimationRunner, long eventTime) { - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "startRecentsActivity(): intent=%s", mTargetIntent); - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "RecentsAnimation#startRecentsActivity"); - - // Cancel any existing recents animation running synchronously (do not hold the - // WM lock) before starting the newly requested recents animation as they can not coexist - if (mWindowManager.getRecentsAnimationController() != null) { - mWindowManager.getRecentsAnimationController().forceCancelAnimation( - REORDER_MOVE_TO_ORIGINAL_POSITION, "startRecentsActivity"); - } - - // If the activity is associated with the root recents task, then try and get that first - Task targetRootTask = mDefaultTaskDisplayArea.getRootTask(WINDOWING_MODE_UNDEFINED, - mTargetActivityType); - ActivityRecord targetActivity = getTargetActivity(targetRootTask); - final boolean hasExistingActivity = targetActivity != null; - if (hasExistingActivity) { - mRestoreTargetBehindRootTask = getRootTaskAbove(targetRootTask); - if (mRestoreTargetBehindRootTask == null - && targetRootTask.getTopMostTask() == targetActivity.getTask()) { - notifyAnimationCancelBeforeStart(recentsAnimationRunner); - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, - "No root task above target root task=%s", targetRootTask); - return; - } - } - - // Send launch hint if we are actually launching the target. If it's already visible - // (shouldn't happen in general) we don't need to send it. - if (targetActivity == null || !targetActivity.isVisibleRequested()) { - mService.mRootWindowContainer.startPowerModeLaunchIfNeeded( - true /* forceSend */, targetActivity); - } - - final LaunchingState launchingState = - mTaskSupervisor.getActivityMetricsLogger().notifyActivityLaunching(mTargetIntent); - - setProcessAnimating(true); - - mService.deferWindowLayout(); - try { - if (hasExistingActivity) { - // Move the recents activity into place for the animation if it is not top most - mDefaultTaskDisplayArea.moveRootTaskBehindBottomMostVisibleRootTask(targetRootTask); - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "Moved rootTask=%s behind rootTask=%s", - targetRootTask, getRootTaskAbove(targetRootTask)); - - // If there are multiple tasks in the target root task (ie. the root home task, - // with 3p and default launchers coexisting), then move the task to the top as a - // part of moving the root task to the front - final Task task = targetActivity.getTask(); - if (targetRootTask.getTopMostTask() != task) { - targetRootTask.positionChildAtTop(task); - } - } else { - // No recents activity, create the new recents activity bottom most - startRecentsActivityInBackground("startRecentsActivity_noTargetActivity"); - - // Move the recents activity into place for the animation - targetRootTask = mDefaultTaskDisplayArea.getRootTask(WINDOWING_MODE_UNDEFINED, - mTargetActivityType); - targetActivity = getTargetActivity(targetRootTask); - mDefaultTaskDisplayArea.moveRootTaskBehindBottomMostVisibleRootTask(targetRootTask); - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "Moved rootTask=%s behind rootTask=%s", - targetRootTask, getRootTaskAbove(targetRootTask)); - - mWindowManager.prepareAppTransitionNone(); - mWindowManager.executeAppTransition(); - - // TODO: Maybe wait for app to draw in this particular case? - - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "Started intent=%s", mTargetIntent); - } - - // Mark the target activity as launch-behind to bump its visibility for the - // duration of the gesture that is driven by the recents component - targetActivity.mLaunchTaskBehind = true; - mLaunchedTargetActivity = targetActivity; - // TODO(b/156772625): Evaluate to send new intents vs. replacing the intent extras. - targetActivity.intent.replaceExtras(mTargetIntent); - - // Fetch all the surface controls and pass them to the client to get the animation - // started - mWindowManager.initializeRecentsAnimation(mTargetActivityType, recentsAnimationRunner, - this, mDefaultTaskDisplayArea.getDisplayId(), - mTaskSupervisor.mRecentTasks.getRecentTaskIds(), targetActivity); - - // If we updated the launch-behind state, update the visibility of the activities after - // we fetch the visible tasks to be controlled by the animation - mService.mRootWindowContainer.ensureActivitiesVisible(); - - ActivityOptions options = null; - if (eventTime > 0) { - options = ActivityOptions.makeBasic(); - options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime); - } - mTaskSupervisor.getActivityMetricsLogger().notifyActivityLaunched(launchingState, - START_TASK_TO_FRONT, !hasExistingActivity, targetActivity, options); - - // Register for root task order changes - mDefaultTaskDisplayArea.registerRootTaskOrderChangedListener(this); - } catch (Exception e) { - Slog.e(TAG, "Failed to start recents activity", e); - throw e; - } finally { - mService.continueWindowLayout(); - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - } - } - - private void finishAnimation(@RecentsAnimationController.ReorderMode int reorderMode, - boolean sendUserLeaveHint) { - synchronized (mService.mGlobalLock) { - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, - "onAnimationFinished(): controller=%s reorderMode=%d", - mWindowManager.getRecentsAnimationController(), reorderMode); - - // Unregister for root task order changes - mDefaultTaskDisplayArea.unregisterRootTaskOrderChangedListener(this); - - final RecentsAnimationController controller = - mWindowManager.getRecentsAnimationController(); - if (controller == null) return; - - // Just to be sure end the launch hint in case the target activity was never launched. - // However, if we're keeping the activity and making it visible, we can leave it on. - if (reorderMode != REORDER_KEEP_IN_PLACE) { - mService.endPowerMode(ActivityTaskManagerService.POWER_MODE_REASON_START_ACTIVITY); - } - - // Once the target is shown, prevent spurious background app switches - if (reorderMode == REORDER_MOVE_TO_TOP) { - mService.stopAppSwitches(); - } - - inSurfaceTransaction(() -> { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, - "RecentsAnimation#onAnimationFinished_inSurfaceTransaction"); - mService.deferWindowLayout(); - try { - mWindowManager.cleanupRecentsAnimation(reorderMode); - - final Task targetRootTask = mDefaultTaskDisplayArea.getRootTask( - WINDOWING_MODE_UNDEFINED, mTargetActivityType); - // Prefer to use the original target activity instead of top activity because - // we may have moved another task to top (starting 3p launcher). - final ActivityRecord targetActivity = targetRootTask != null - ? targetRootTask.isInTask(mLaunchedTargetActivity) - : null; - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, - "onAnimationFinished(): targetRootTask=%s targetActivity=%s " - + "mRestoreTargetBehindRootTask=%s", - targetRootTask, targetActivity, mRestoreTargetBehindRootTask); - if (targetActivity == null) { - return; - } - - // Restore the launched-behind state - targetActivity.mLaunchTaskBehind = false; - - if (reorderMode == REORDER_MOVE_TO_TOP) { - // Bring the target root task to the front - mTaskSupervisor.mNoAnimActivities.add(targetActivity); - - if (sendUserLeaveHint) { - // Setting this allows the previous app to PiP. - mTaskSupervisor.mUserLeaving = true; - targetRootTask.moveTaskToFront(targetActivity.getTask(), - true /* noAnimation */, null /* activityOptions */, - targetActivity.appTimeTracker, - "RecentsAnimation.onAnimationFinished()"); - } else { - targetRootTask.moveToFront("RecentsAnimation.onAnimationFinished()"); - } - - if (WM_DEBUG_RECENTS_ANIMATIONS.isLogToAny()) { - final Task topRootTask = getTopNonAlwaysOnTopRootTask(); - if (topRootTask != targetRootTask) { - ProtoLog.w(WM_DEBUG_RECENTS_ANIMATIONS, - "Expected target rootTask=%s" - + " to be top most but found rootTask=%s", - targetRootTask, topRootTask); - } - } - } else if (reorderMode == REORDER_MOVE_TO_ORIGINAL_POSITION){ - // Restore the target root task to its previous position - final TaskDisplayArea taskDisplayArea = targetActivity.getDisplayArea(); - taskDisplayArea.moveRootTaskBehindRootTask(targetRootTask, - mRestoreTargetBehindRootTask); - if (WM_DEBUG_RECENTS_ANIMATIONS.isLogToAny()) { - final Task aboveTargetRootTask = getRootTaskAbove(targetRootTask); - if (mRestoreTargetBehindRootTask != null - && aboveTargetRootTask != mRestoreTargetBehindRootTask) { - ProtoLog.w(WM_DEBUG_RECENTS_ANIMATIONS, - "Expected target rootTask=%s to restored behind " - + "rootTask=%s but it is behind rootTask=%s", - targetRootTask, mRestoreTargetBehindRootTask, - aboveTargetRootTask); - } - } - } else { - // If there is no recents screenshot animation, we can update the visibility - // of target root task immediately because it is visually invisible and the - // launch-behind state is restored. That also prevents the next transition - // type being disturbed if the visibility is updated after setting the next - // transition (the target activity will be one of closing apps). - if (!controller.shouldDeferCancelWithScreenshot() - && !targetRootTask.isFocusedRootTaskOnDisplay()) { - targetRootTask.ensureActivitiesVisible(null /* starting */); - } - // Keep target root task in place, nothing changes, so ignore the transition - // logic below - return; - } - - mWindowManager.prepareAppTransitionNone(); - mService.mRootWindowContainer.ensureActivitiesVisible(); - mService.mRootWindowContainer.resumeFocusedTasksTopActivities(); - - // No reason to wait for the pausing activity in this case, as the hiding of - // surfaces needs to be done immediately. - mWindowManager.executeAppTransition(); - - final Task rootTask = targetRootTask.getRootTask(); - // Client state may have changed during the recents animation, so force - // send task info so the client can synchronize its state. - rootTask.dispatchTaskInfoChangedIfNeeded(true /* force */); - } catch (Exception e) { - Slog.e(TAG, "Failed to clean up recents activity", e); - throw e; - } finally { - mTaskSupervisor.mUserLeaving = false; - mService.continueWindowLayout(); - // Make sure the surfaces are updated with the latest state. Sometimes the - // surface placement may be skipped if display configuration is changed (i.e. - // {@link DisplayContent#mWaitingForConfig} is true). - if (mWindowManager.mRoot.isLayoutNeeded()) { - mWindowManager.mRoot.performSurfacePlacement(); - } - setProcessAnimating(false); - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - } - }); - } - } - - // No-op wrapper to keep legacy code. - private static void inSurfaceTransaction(Runnable exec) { - exec.run(); - } - - /** Gives the owner of recents animation higher priority. */ - private void setProcessAnimating(boolean animating) { - if (mCaller == null) return; - // Apply the top-app scheduling group to who runs the animation. - mCaller.setRunningRecentsAnimation(animating); - int demoteReasons = mService.mDemoteTopAppReasons; - if (animating) { - demoteReasons |= ActivityTaskManagerService.DEMOTE_TOP_REASON_ANIMATING_RECENTS; - } else { - demoteReasons &= ~ActivityTaskManagerService.DEMOTE_TOP_REASON_ANIMATING_RECENTS; - } - mService.mDemoteTopAppReasons = demoteReasons; - // Make the demotion of the real top app take effect. No need to restore top app state for - // finishing recents because addToStopping -> scheduleIdle -> activityIdleInternal -> - // trimApplications will have a full update. - if (animating && mService.mTopApp != null) { - mService.mTopApp.scheduleUpdateOomAdj(); - } - } - - @Override - public void onAnimationFinished(@RecentsAnimationController.ReorderMode int reorderMode, - boolean sendUserLeaveHint) { - finishAnimation(reorderMode, sendUserLeaveHint); - } - - @Override - public void onRootTaskOrderChanged(Task rootTask) { - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "onRootTaskOrderChanged(): rootTask=%s", rootTask); - if (mDefaultTaskDisplayArea.getRootTask(t -> t == rootTask) == null - || !rootTask.shouldBeVisible(null)) { - // The root task is not visible, so ignore this change - return; - } - final RecentsAnimationController controller = - mWindowManager.getRecentsAnimationController(); - if (controller == null) { - return; - } - - // We defer canceling the recents animation until the next app transition in the following - // cases: - // 1) The next launching task is not being animated by the recents animation - // 2) The next task is home activity. (i.e. pressing home key to back home in recents). - if ((!controller.isAnimatingTask(rootTask.getTopMostTask()) - || controller.isTargetApp(rootTask.getTopNonFinishingActivity())) - && controller.shouldDeferCancelUntilNextTransition()) { - // Always prepare an app transition since we rely on the transition callbacks to cleanup - mWindowManager.prepareAppTransitionNone(); - controller.setCancelOnNextTransitionStart(); - } - } - private void startRecentsActivityInBackground(String reason) { final ActivityOptions options = ActivityOptions.makeBasic(); options.setLaunchActivityType(mTargetActivityType); @@ -492,26 +160,6 @@ class RecentsAnimation implements RecentsAnimationCallbacks, OnRootTaskOrderChan } /** - * Called only when the animation should be canceled prior to starting. - */ - static void notifyAnimationCancelBeforeStart(IRecentsAnimationRunner recentsAnimationRunner) { - try { - recentsAnimationRunner.onAnimationCanceled(null /* taskIds */, - null /* taskSnapshots */); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to cancel recents animation before start", e); - } - } - - /** - * @return The top root task that is not always-on-top. - */ - private Task getTopNonAlwaysOnTopRootTask() { - return mDefaultTaskDisplayArea.getRootTask(task -> - !task.getWindowConfiguration().isAlwaysOnTop()); - } - - /** * @return the top activity in the {@param targetRootTask} matching the {@param component}, * or just the top activity of the top task if no task matches the component. */ diff --git a/services/core/java/com/android/server/wm/RecentsAnimationController.java b/services/core/java/com/android/server/wm/RecentsAnimationController.java deleted file mode 100644 index 6f947135b789..000000000000 --- a/services/core/java/com/android/server/wm/RecentsAnimationController.java +++ /dev/null @@ -1,1382 +0,0 @@ -/* - * 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.server.wm; - -import static android.app.ActivityTaskManager.INVALID_TASK_ID; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; -import static android.view.RemoteAnimationTarget.MODE_CLOSING; -import static android.view.RemoteAnimationTarget.MODE_OPENING; -import static android.view.WindowManager.INPUT_CONSUMER_RECENTS_ANIMATION; -import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; - -import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RECENTS_ANIMATIONS; -import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; -import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_RECENTS_ANIM; -import static com.android.server.wm.AnimationAdapterProto.REMOTE; -import static com.android.server.wm.RemoteAnimationAdapterWrapperProto.TARGET; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS; -import static com.android.server.wm.WindowManagerInternal.AppTransitionListener; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.app.WindowConfiguration; -import android.graphics.GraphicBuffer; -import android.graphics.Point; -import android.graphics.Rect; -import android.hardware.HardwareBuffer; -import android.os.Binder; -import android.os.Bundle; -import android.os.IBinder.DeathRecipient; -import android.os.RemoteException; -import android.os.SystemClock; -import android.util.ArrayMap; -import android.util.ArraySet; -import android.util.IntArray; -import android.util.Slog; -import android.util.SparseBooleanArray; -import android.util.proto.ProtoOutputStream; -import android.view.IRecentsAnimationController; -import android.view.IRecentsAnimationRunner; -import android.view.InputWindowHandle; -import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; -import android.view.SurfaceControl.Transaction; -import android.view.SurfaceSession; -import android.view.WindowInsets.Type; -import android.window.PictureInPictureSurfaceTransaction; -import android.window.TaskSnapshot; -import android.window.WindowAnimationState; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.os.IResultReceiver; -import com.android.internal.protolog.ProtoLog; -import com.android.server.LocalServices; -import com.android.server.inputmethod.InputMethodManagerInternal; -import com.android.server.statusbar.StatusBarManagerInternal; -import com.android.server.wm.SurfaceAnimator.AnimationType; -import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback; -import com.android.server.wm.utils.InsetUtils; - -import com.google.android.collect.Sets; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.stream.Collectors; - -/** - * Controls a single instance of the remote driven recents animation. In particular, this allows - * the calling SystemUI to animate the visible task windows as a part of the transition. The remote - * runner is provided an animation controller which allows it to take screenshots and to notify - * window manager when the animation is completed. In addition, window manager may also notify the - * app if it requires the animation to be canceled at any time (ie. due to timeout, etc.) - */ -public class RecentsAnimationController implements DeathRecipient { - private static final String TAG = RecentsAnimationController.class.getSimpleName(); - private static final long FAILSAFE_DELAY = 1000; - - // Constant for a yet-to-be-calculated {@link RemoteAnimationTarget#Mode} state - private static final int MODE_UNKNOWN = -1; - - public static final int REORDER_KEEP_IN_PLACE = 0; - public static final int REORDER_MOVE_TO_TOP = 1; - public static final int REORDER_MOVE_TO_ORIGINAL_POSITION = 2; - - @IntDef(prefix = { "REORDER_MODE_" }, value = { - REORDER_KEEP_IN_PLACE, - REORDER_MOVE_TO_TOP, - REORDER_MOVE_TO_ORIGINAL_POSITION - }) - public @interface ReorderMode {} - - private final WindowManagerService mService; - @VisibleForTesting - final StatusBarManagerInternal mStatusBar; - private IRecentsAnimationRunner mRunner; - private final RecentsAnimationCallbacks mCallbacks; - private final ArrayList<TaskAnimationAdapter> mPendingAnimations = new ArrayList<>(); - private final IntArray mPendingNewTaskTargets = new IntArray(0); - - private final ArrayList<WallpaperAnimationAdapter> mPendingWallpaperAnimations = - new ArrayList<>(); - private final int mDisplayId; - private boolean mWillFinishToHome = false; - private final Runnable mFailsafeRunnable = this::onFailsafe; - - // The recents component app token that is shown behind the visible tasks - private ActivityRecord mTargetActivityRecord; - private DisplayContent mDisplayContent; - private int mTargetActivityType; - - // We start the RecentsAnimationController in a pending-start state since we need to wait for - // the wallpaper/activity to draw before we can give control to the handler to start animating - // the visible task surfaces - private boolean mPendingStart = true; - - // Set when the animation has been canceled - private boolean mCanceled; - - // Whether or not the input consumer is enabled. The input consumer must be both registered and - // enabled for it to start intercepting touch events. - private boolean mInputConsumerEnabled; - - private final Rect mTmpRect = new Rect(); - - private boolean mLinkedToDeathOfRunner; - - // Whether to try to defer canceling from a root task order change until the next transition - private boolean mRequestDeferCancelUntilNextTransition; - // Whether to actually defer canceling until the next transition - private boolean mCancelOnNextTransitionStart; - // Whether to take a screenshot when handling a deferred cancel - private boolean mCancelDeferredWithScreenshot; - // The reorder mode to apply after the cleanupScreenshot() callback - private int mPendingCancelWithScreenshotReorderMode = REORDER_MOVE_TO_ORIGINAL_POSITION; - - @VisibleForTesting - boolean mIsAddingTaskToTargets; - private boolean mNavigationBarAttachedToApp; - private ActivityRecord mNavBarAttachedApp; - - private final ArrayList<RemoteAnimationTarget> mPendingTaskAppears = new ArrayList<>(); - - /** - * An app transition listener to cancel the recents animation only after the app transition - * starts or is canceled. - */ - final AppTransitionListener mAppTransitionListener = new AppTransitionListener() { - @Override - public int onAppTransitionStartingLocked(long statusBarAnimationStartTime, - long statusBarAnimationDuration) { - continueDeferredCancel(); - return 0; - } - - @Override - public void onAppTransitionCancelledLocked(boolean keyguardGoingAwayCancelled) { - continueDeferredCancel(); - } - - private void continueDeferredCancel() { - mDisplayContent.mAppTransition.unregisterListener(this); - if (mCanceled) { - return; - } - - if (mCancelOnNextTransitionStart) { - mCancelOnNextTransitionStart = false; - cancelAnimationWithScreenshot(mCancelDeferredWithScreenshot); - } - } - }; - - public interface RecentsAnimationCallbacks { - /** Callback when recents animation is finished. */ - void onAnimationFinished(@ReorderMode int reorderMode, boolean sendUserLeaveHint); - } - - private final IRecentsAnimationController mController = - new IRecentsAnimationController.Stub() { - - @Override - public TaskSnapshot screenshotTask(int taskId) { - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, - "screenshotTask(%d): mCanceled=%b", taskId, mCanceled); - final long token = Binder.clearCallingIdentity(); - try { - synchronized (mService.getWindowManagerLock()) { - if (mCanceled) { - return null; - } - for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { - final TaskAnimationAdapter adapter = mPendingAnimations.get(i); - final Task task = adapter.mTask; - if (task.mTaskId == taskId) { - final TaskSnapshotController snapshotController = - mService.mTaskSnapshotController; - final ArraySet<Task> tasks = Sets.newArraySet(task); - snapshotController.snapshotTasks(tasks); - snapshotController.addSkipClosingAppSnapshotTasks(tasks); - return snapshotController.getSnapshot(taskId, task.mUserId, - false /* restoreFromDisk */, false /* isLowResolution */); - } - } - return null; - } - } finally { - Binder.restoreCallingIdentity(token); - } - } - - @Override - public void setFinishTaskTransaction(int taskId, - PictureInPictureSurfaceTransaction finishTransaction, - SurfaceControl overlay) { - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, - "setFinishTaskTransaction(%d): transaction=%s", taskId, finishTransaction); - final long token = Binder.clearCallingIdentity(); - try { - synchronized (mService.getWindowManagerLock()) { - for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { - final TaskAnimationAdapter taskAdapter = mPendingAnimations.get(i); - if (taskAdapter.mTask.mTaskId == taskId) { - taskAdapter.mFinishTransaction = finishTransaction; - taskAdapter.mFinishOverlay = overlay; - break; - } - } - } - } finally { - Binder.restoreCallingIdentity(token); - } - } - - @Override - public void finish(boolean moveHomeToTop, boolean sendUserLeaveHint, - IResultReceiver finishCb) { - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, - "finish(%b): mCanceled=%b", moveHomeToTop, mCanceled); - final long token = Binder.clearCallingIdentity(); - try { - // Note, the callback will handle its own synchronization, do not lock on WM lock - // prior to calling the callback - mCallbacks.onAnimationFinished(moveHomeToTop - ? REORDER_MOVE_TO_TOP - : REORDER_MOVE_TO_ORIGINAL_POSITION, sendUserLeaveHint); - } finally { - Binder.restoreCallingIdentity(token); - } - if (finishCb != null) { - try { - finishCb.send(0, new Bundle()); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to report animation finished", e); - } - } - } - - @Override - public void setAnimationTargetsBehindSystemBars(boolean behindSystemBars) - throws RemoteException { - final long token = Binder.clearCallingIdentity(); - try { - synchronized (mService.getWindowManagerLock()) { - for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { - final Task task = mPendingAnimations.get(i).mTask; - if (task.getActivityType() != mTargetActivityType) { - task.setCanAffectSystemUiFlags(behindSystemBars); - } - } - InputMethodManagerInternal.get().maybeFinishStylusHandwriting(); - mService.mWindowPlacerLocked.requestTraversal(); - } - } finally { - Binder.restoreCallingIdentity(token); - } - } - - @Override - public void setInputConsumerEnabled(boolean enabled) { - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, - "setInputConsumerEnabled(%s): mCanceled=%b", enabled, mCanceled); - final long token = Binder.clearCallingIdentity(); - try { - synchronized (mService.getWindowManagerLock()) { - if (mCanceled) { - return; - } - mInputConsumerEnabled = enabled; - final InputMonitor inputMonitor = mDisplayContent.getInputMonitor(); - inputMonitor.updateInputWindowsLw(true /*force*/); - mService.scheduleAnimationLocked(); - } - } finally { - Binder.restoreCallingIdentity(token); - } - } - - @Override - public void setDeferCancelUntilNextTransition(boolean defer, boolean screenshot) { - synchronized (mService.mGlobalLock) { - setDeferredCancel(defer, screenshot); - } - } - - @Override - public void cleanupScreenshot() { - final long token = Binder.clearCallingIdentity(); - try { - // Note, the callback will handle its own synchronization, do not lock on WM lock - // prior to calling the callback - continueDeferredCancelAnimation(); - } finally { - Binder.restoreCallingIdentity(token); - } - } - - @Override - public void setWillFinishToHome(boolean willFinishToHome) { - synchronized (mService.getWindowManagerLock()) { - RecentsAnimationController.this.setWillFinishToHome(willFinishToHome); - } - } - - @Override - public boolean removeTask(int taskId) { - final long token = Binder.clearCallingIdentity(); - try { - synchronized (mService.getWindowManagerLock()) { - return removeTaskInternal(taskId); - } - } finally { - Binder.restoreCallingIdentity(token); - } - } - - @Override - public void detachNavigationBarFromApp(boolean moveHomeToTop) { - final long token = Binder.clearCallingIdentity(); - try { - synchronized (mService.getWindowManagerLock()) { - restoreNavigationBarFromApp( - moveHomeToTop || mIsAddingTaskToTargets /* animate */); - mService.mWindowPlacerLocked.requestTraversal(); - } - } finally { - Binder.restoreCallingIdentity(token); - } - } - - @Override - public void animateNavigationBarToApp(long duration) { - final long token = Binder.clearCallingIdentity(); - try { - synchronized (mService.getWindowManagerLock()) { - animateNavigationBarForAppLaunch(duration); - } - } finally { - Binder.restoreCallingIdentity(token); - } - } - - @Override - public void handOffAnimation( - RemoteAnimationTarget[] targets, WindowAnimationState[] states) { - // unused legacy implementation - } - }; - - /** - * @param remoteAnimationRunner The remote runner which should be notified when the animation is - * ready to start or has been canceled - * @param callbacks Callbacks to be made when the animation finishes - */ - RecentsAnimationController(WindowManagerService service, - IRecentsAnimationRunner remoteAnimationRunner, RecentsAnimationCallbacks callbacks, - int displayId) { - mService = service; - mRunner = remoteAnimationRunner; - mCallbacks = callbacks; - mDisplayId = displayId; - mStatusBar = LocalServices.getService(StatusBarManagerInternal.class); - mDisplayContent = service.mRoot.getDisplayContent(displayId); - } - - /** - * Initializes the recents animation controller. This is a separate call from the constructor - * because it may call cancelAnimation() which needs to properly clean up the controller - * in the window manager. - */ - public void initialize(int targetActivityType, SparseBooleanArray recentTaskIds, - ActivityRecord targetActivity) { - mTargetActivityType = targetActivityType; - mDisplayContent.mAppTransition.registerListenerLocked(mAppTransitionListener); - - // Make leashes for each of the visible/target tasks and add it to the recents animation to - // be started - // TODO(b/153090560): Support Recents on multiple task display areas - final ArrayList<Task> visibleTasks = mDisplayContent.getDefaultTaskDisplayArea() - .getVisibleTasks(); - final Task targetRootTask = mDisplayContent.getDefaultTaskDisplayArea() - .getRootTask(WINDOWING_MODE_UNDEFINED, targetActivityType); - if (targetRootTask != null) { - targetRootTask.forAllLeafTasks(t -> { - if (!visibleTasks.contains(t)) { - visibleTasks.add(t); - } - }, true /* traverseTopToBottom */); - } - - final int taskCount = visibleTasks.size(); - for (int i = taskCount - 1; i >= 0; i--) { - final Task task = visibleTasks.get(i); - if (skipAnimation(task)) { - continue; - } - addAnimation(task, !recentTaskIds.get(task.mTaskId), false /* hidden */, - (type, anim) -> task.forAllWindows(win -> { - win.onAnimationFinished(type, anim); - }, true /* traverseTopToBottom */)); - } - - // Skip the animation if there is nothing to animate - if (mPendingAnimations.isEmpty()) { - cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "initialize-noVisibleTasks"); - return; - } - - try { - linkToDeathOfRunner(); - } catch (RemoteException e) { - cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "initialize-failedToLinkToDeath"); - return; - } - - attachNavigationBarToApp(); - - // Adjust the wallpaper visibility for the showing target activity - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, - "setHomeApp(%s)", targetActivity.getName()); - mTargetActivityRecord = targetActivity; - if (targetActivity.windowsCanBeWallpaperTarget()) { - mDisplayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER; - mDisplayContent.setLayoutNeeded(); - } - - mService.mWindowPlacerLocked.performSurfacePlacement(); - - mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(targetActivity); - - // Notify that the animation has started - if (mStatusBar != null) { - mStatusBar.onRecentsAnimationStateChanged(true /* running */); - } - } - - /** - * Return whether the given window should still be considered interesting for the all-drawn - * state. This is only interesting for the target app, which may have child windows that are - * not actually visible and should not be considered interesting and waited upon. - */ - protected boolean isInterestingForAllDrawn(WindowState window) { - if (isTargetApp(window.getActivityRecord())) { - if (window.getWindowType() != TYPE_BASE_APPLICATION - && window.getAttrs().alpha == 0f) { - // If there is a cihld window that is alpha 0, then ignore that window - return false; - } - } - // By default all windows are still interesting for all drawn purposes - return true; - } - - /** - * Whether a task should be filtered from the recents animation. This can be true for tasks - * being displayed outside of recents. - */ - private boolean skipAnimation(Task task) { - final WindowConfiguration config = task.getWindowConfiguration(); - return task.isAlwaysOnTop() || config.tasksAreFloating(); - } - - @VisibleForTesting - TaskAnimationAdapter addAnimation(Task task, boolean isRecentTaskInvisible) { - return addAnimation(task, isRecentTaskInvisible, false /* hidden */, - null /* finishedCallback */); - } - - @VisibleForTesting - TaskAnimationAdapter addAnimation(Task task, boolean isRecentTaskInvisible, boolean hidden, - OnAnimationFinishedCallback finishedCallback) { - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "addAnimation(%s)", task.getName()); - final TaskAnimationAdapter taskAdapter = new TaskAnimationAdapter(task, - isRecentTaskInvisible); - task.startAnimation(task.getPendingTransaction(), taskAdapter, hidden, - ANIMATION_TYPE_RECENTS, finishedCallback); - task.commitPendingTransaction(); - mPendingAnimations.add(taskAdapter); - return taskAdapter; - } - - @VisibleForTesting - void removeAnimation(TaskAnimationAdapter taskAdapter) { - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, - "removeAnimation(%d)", taskAdapter.mTask.mTaskId); - taskAdapter.onRemove(); - mPendingAnimations.remove(taskAdapter); - } - - @VisibleForTesting - void removeWallpaperAnimation(WallpaperAnimationAdapter wallpaperAdapter) { - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "removeWallpaperAnimation()"); - wallpaperAdapter.getLeashFinishedCallback().onAnimationFinished( - wallpaperAdapter.getLastAnimationType(), wallpaperAdapter); - mPendingWallpaperAnimations.remove(wallpaperAdapter); - } - - void startAnimation() { - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, - "startAnimation(): mPendingStart=%b mCanceled=%b", mPendingStart, mCanceled); - if (!mPendingStart || mCanceled) { - // Skip starting if we've already started or canceled the animation - return; - } - try { - // Create the app targets - final RemoteAnimationTarget[] appTargets = createAppAnimations(); - - // Skip the animation if there is nothing to animate - if (appTargets.length == 0) { - cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "startAnimation-noAppWindows"); - return; - } - - // Create the wallpaper targets - final RemoteAnimationTarget[] wallpaperTargets = createWallpaperAnimations(); - - mPendingStart = false; - - final Rect contentInsets; - final WindowState targetAppMainWindow = getTargetAppMainWindow(); - if (targetAppMainWindow != null) { - contentInsets = targetAppMainWindow - .getInsetsStateWithVisibilityOverride() - .calculateInsets(mTargetActivityRecord.getBounds(), Type.systemBars(), - false /* ignoreVisibility */).toRect(); - } else { - // If the window for the activity had not yet been created, use the display insets. - mService.getStableInsets(mDisplayId, mTmpRect); - contentInsets = mTmpRect; - } - mRunner.onAnimationStart(mController, appTargets, wallpaperTargets, contentInsets, - null, new Bundle()); - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, - "startAnimation(): Notify animation start: %s", - mPendingAnimations.stream() - .map(anim->anim.mTask.mTaskId).collect(Collectors.toList())); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to start recents animation", e); - } - - if (mTargetActivityRecord != null) { - final ArrayMap<WindowContainer, Integer> reasons = new ArrayMap<>(1); - reasons.put(mTargetActivityRecord, APP_TRANSITION_RECENTS_ANIM); - mService.mAtmService.mTaskSupervisor.getActivityMetricsLogger() - .notifyTransitionStarting(reasons); - } - } - - boolean isNavigationBarAttachedToApp() { - return mNavigationBarAttachedToApp; - } - - @VisibleForTesting - WindowState getNavigationBarWindow() { - return mDisplayContent.getDisplayPolicy().getNavigationBar(); - } - - private void attachNavigationBarToApp() { - if (!mDisplayContent.getDisplayPolicy().shouldAttachNavBarToAppDuringTransition() - // Skip the case where the nav bar is controlled by fade rotation. - || mDisplayContent.getAsyncRotationController() != null) { - return; - } - for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { - final TaskAnimationAdapter adapter = mPendingAnimations.get(i); - final Task task = adapter.mTask; - if (task.isActivityTypeHomeOrRecents()) { - continue; - } - mNavBarAttachedApp = task.getTopVisibleActivity(); - break; - } - - final WindowState navWindow = getNavigationBarWindow(); - if (mNavBarAttachedApp == null || navWindow == null || navWindow.mToken == null) { - return; - } - mNavigationBarAttachedToApp = true; - navWindow.mToken.cancelAnimation(); - final SurfaceControl.Transaction t = navWindow.mToken.getPendingTransaction(); - final SurfaceControl navSurfaceControl = navWindow.mToken.getSurfaceControl(); - navWindow.setSurfaceTranslationY(-mNavBarAttachedApp.getBounds().top); - t.reparent(navSurfaceControl, mNavBarAttachedApp.getSurfaceControl()); - t.show(navSurfaceControl); - - final WindowContainer imeContainer = mDisplayContent.getImeContainer(); - if (imeContainer.isVisible()) { - t.setRelativeLayer(navSurfaceControl, imeContainer.getSurfaceControl(), 1); - } else { - // Place the nav bar on top of anything else in the top activity. - t.setLayer(navSurfaceControl, Integer.MAX_VALUE); - } - if (mStatusBar != null) { - mStatusBar.setNavigationBarLumaSamplingEnabled(mDisplayId, false); - } - } - - @VisibleForTesting - void restoreNavigationBarFromApp(boolean animate) { - if (!mNavigationBarAttachedToApp) { - return; - } - mNavigationBarAttachedToApp = false; - - if (mStatusBar != null) { - mStatusBar.setNavigationBarLumaSamplingEnabled(mDisplayId, true); - } - - final WindowState navWindow = getNavigationBarWindow(); - if (navWindow == null) { - return; - } - navWindow.setSurfaceTranslationY(0); - - final WindowToken navToken = navWindow.mToken; - if (navToken == null) { - return; - } - final SurfaceControl.Transaction t = mDisplayContent.getPendingTransaction(); - final WindowContainer parent = navToken.getParent(); - t.setLayer(navToken.getSurfaceControl(), navToken.getLastLayer()); - - if (animate) { - final NavBarFadeAnimationController controller = - new NavBarFadeAnimationController(mDisplayContent); - controller.fadeWindowToken(true); - } else { - // Reparent the SurfaceControl of nav bar token back. - t.reparent(navToken.getSurfaceControl(), parent.getSurfaceControl()); - } - } - - void animateNavigationBarForAppLaunch(long duration) { - if (!mDisplayContent.getDisplayPolicy().shouldAttachNavBarToAppDuringTransition() - // Skip the case where the nav bar is controlled by fade rotation. - || mDisplayContent.getAsyncRotationController() != null - || mNavigationBarAttachedToApp - || mNavBarAttachedApp == null) { - return; - } - - final NavBarFadeAnimationController controller = - new NavBarFadeAnimationController(mDisplayContent); - controller.fadeOutAndInSequentially(duration, null /* fadeOutParent */, - mNavBarAttachedApp.getSurfaceControl()); - } - - void addTaskToTargets(Task task, OnAnimationFinishedCallback finishedCallback) { - if (mRunner != null) { - mIsAddingTaskToTargets = task != null; - mNavBarAttachedApp = task == null ? null : task.getTopVisibleActivity(); - // No need to send task appeared when the task target already exists, or when the - // task is being managed as a multi-window mode outside of recents (e.g. bubbles). - if (isAnimatingTask(task) || skipAnimation(task)) { - return; - } - collectTaskRemoteAnimations(task, MODE_OPENING, finishedCallback); - } - } - - void sendTasksAppeared() { - if (mPendingTaskAppears.isEmpty() || mRunner == null) return; - try { - final RemoteAnimationTarget[] targets = mPendingTaskAppears.toArray( - new RemoteAnimationTarget[0]); - mRunner.onTasksAppeared(targets); - mPendingTaskAppears.clear(); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to report task appeared", e); - } - } - - private void collectTaskRemoteAnimations(Task task, int mode, - OnAnimationFinishedCallback finishedCallback) { - final SparseBooleanArray recentTaskIds = - mService.mAtmService.getRecentTasks().getRecentTaskIds(); - - // The target must be built off the root task (the leaf task surface would be cropped - // within the root surface). However, recents only tracks leaf task ids, so we'll traverse - // and create animation target for all visible leaf tasks. - task.forAllLeafTasks(leafTask -> { - if (!leafTask.shouldBeVisible(null /* starting */)) { - return; - } - final int taskId = leafTask.mTaskId; - TaskAnimationAdapter adapter = addAnimation(leafTask, - !recentTaskIds.get(taskId), true /* hidden */, finishedCallback); - mPendingNewTaskTargets.add(taskId); - final RemoteAnimationTarget target = - adapter.createRemoteAnimationTarget(taskId, mode); - if (target != null) { - mPendingTaskAppears.add(target); - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, - "collectTaskRemoteAnimations, target: %s", target); - } - }, false /* traverseTopToBottom */); - } - - private boolean removeTaskInternal(int taskId) { - boolean result = false; - for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { - // Only allows when task target has became visible to user, to prevent - // the flickering during remove animation and task visible. - final TaskAnimationAdapter target = mPendingAnimations.get(i); - if (target.mTask.mTaskId == taskId && target.mTask.isOnTop()) { - removeAnimation(target); - final int taskIndex = mPendingNewTaskTargets.indexOf(taskId); - if (taskIndex != -1) { - mPendingNewTaskTargets.remove(taskIndex); - } - result = true; - break; - } - } - return result; - } - - private RemoteAnimationTarget[] createAppAnimations() { - final ArrayList<RemoteAnimationTarget> targets = new ArrayList<>(); - for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { - final TaskAnimationAdapter taskAdapter = mPendingAnimations.get(i); - final RemoteAnimationTarget target = - taskAdapter.createRemoteAnimationTarget(INVALID_TASK_ID, MODE_UNKNOWN); - if (target != null) { - targets.add(target); - } else { - removeAnimation(taskAdapter); - } - } - return targets.toArray(new RemoteAnimationTarget[targets.size()]); - } - - private RemoteAnimationTarget[] createWallpaperAnimations() { - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "createWallpaperAnimations()"); - return WallpaperAnimationAdapter.startWallpaperAnimations(mDisplayContent, 0L, 0L, - adapter -> { - synchronized (mService.mGlobalLock) { - // If the wallpaper animation is canceled, continue with the recents - // animation - mPendingWallpaperAnimations.remove(adapter); - } - }, mPendingWallpaperAnimations); - } - - void forceCancelAnimation(@ReorderMode int reorderMode, String reason) { - if (!mCanceled) { - cancelAnimation(reorderMode, reason); - } else { - continueDeferredCancelAnimation(); - } - } - - void cancelAnimation(@ReorderMode int reorderMode, String reason) { - cancelAnimation(reorderMode, false /*screenshot */, reason); - } - - void cancelAnimationWithScreenshot(boolean screenshot) { - cancelAnimation(REORDER_KEEP_IN_PLACE, screenshot, "rootTaskOrderChanged"); - } - - /** - * Cancels the running animation when starting home, providing a snapshot for the runner to - * properly handle the cancellation. This call uses the provided hint to determine how to - * finish the animation. - */ - public void cancelAnimationForHomeStart() { - final int reorderMode = mTargetActivityType == ACTIVITY_TYPE_HOME && mWillFinishToHome - ? REORDER_MOVE_TO_TOP - : REORDER_KEEP_IN_PLACE; - cancelAnimation(reorderMode, true /* screenshot */, "cancelAnimationForHomeStart"); - } - - /** - * Cancels the running animation when there is a display change, providing a snapshot for the - * runner to properly handle the cancellation. This call uses the provided hint to determine - * how to finish the animation. - */ - public void cancelAnimationForDisplayChange() { - cancelAnimation(mWillFinishToHome ? REORDER_MOVE_TO_TOP : REORDER_MOVE_TO_ORIGINAL_POSITION, - true /* screenshot */, "cancelAnimationForDisplayChange"); - } - - private void cancelAnimation(@ReorderMode int reorderMode, boolean screenshot, String reason) { - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "cancelAnimation(): reason=%s", reason); - synchronized (mService.getWindowManagerLock()) { - if (mCanceled) { - // We've already canceled the animation - return; - } - mService.mH.removeCallbacks(mFailsafeRunnable); - mCanceled = true; - - if (screenshot && !mPendingAnimations.isEmpty()) { - final ArrayMap<Task, TaskSnapshot> snapshotMap = screenshotRecentTasks(); - mPendingCancelWithScreenshotReorderMode = reorderMode; - - if (!snapshotMap.isEmpty()) { - try { - int[] taskIds = new int[snapshotMap.size()]; - TaskSnapshot[] snapshots = new TaskSnapshot[snapshotMap.size()]; - for (int i = snapshotMap.size() - 1; i >= 0; i--) { - taskIds[i] = snapshotMap.keyAt(i).mTaskId; - snapshots[i] = snapshotMap.valueAt(i); - } - mRunner.onAnimationCanceled(taskIds, snapshots); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to cancel recents animation", e); - } - // Schedule a new failsafe for if the runner doesn't clean up the screenshot - scheduleFailsafe(); - return; - } - // Fallback to a normal cancel since we couldn't screenshot - } - - // Notify the runner and clean up the animation immediately - // Note: In the fallback case, this can trigger multiple onAnimationCancel() calls - // to the runner if we this actually triggers cancel twice on the caller - try { - mRunner.onAnimationCanceled(null /* taskIds */, null /* taskSnapshots */); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to cancel recents animation", e); - } - mCallbacks.onAnimationFinished(reorderMode, false /* sendUserLeaveHint */); - } - } - - @VisibleForTesting - void continueDeferredCancelAnimation() { - mCallbacks.onAnimationFinished(mPendingCancelWithScreenshotReorderMode, - false /* sendUserLeaveHint */); - } - - @VisibleForTesting - void setWillFinishToHome(boolean willFinishToHome) { - mWillFinishToHome = willFinishToHome; - } - - /** - * Cancel recents animation when the next app transition starts. - * <p> - * When we cancel the recents animation due to a root task order change, we can't just cancel it - * immediately as it would lead to a flicker in Launcher if we just remove the task from the - * leash. Instead we screenshot the previous task and replace the child of the leash with the - * screenshot, so that Launcher can still control the leash lifecycle & make the next app - * transition animate smoothly without flickering. - */ - void setCancelOnNextTransitionStart() { - mCancelOnNextTransitionStart = true; - } - - /** - * Requests that we attempt to defer the cancel until the next app transition if we are - * canceling from a root task order change. If {@param screenshot} is specified, then the - * system will replace the contents of the leash with a screenshot, which must be cleaned up - * when the runner calls cleanUpScreenshot(). - */ - void setDeferredCancel(boolean defer, boolean screenshot) { - mRequestDeferCancelUntilNextTransition = defer; - mCancelDeferredWithScreenshot = screenshot; - } - - /** - * @return Whether we should defer the cancel from a root task order change until the next app - * transition. - */ - boolean shouldDeferCancelUntilNextTransition() { - return mRequestDeferCancelUntilNextTransition; - } - - /** - * @return Whether we should both defer the cancel from a root task order change until the next - * app transition, and also that the deferred cancel should replace the contents of the leash - * with a screenshot. - */ - boolean shouldDeferCancelWithScreenshot() { - return mRequestDeferCancelUntilNextTransition && mCancelDeferredWithScreenshot; - } - - private ArrayMap<Task, TaskSnapshot> screenshotRecentTasks() { - final TaskSnapshotController snapshotController = mService.mTaskSnapshotController; - final ArrayMap<Task, TaskSnapshot> snapshotMap = new ArrayMap<>(); - for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { - final TaskAnimationAdapter adapter = mPendingAnimations.get(i); - final Task task = adapter.mTask; - if (task.isActivityTypeHome()) continue; - snapshotController.recordSnapshot(task); - final TaskSnapshot snapshot = snapshotController.getSnapshot(task.mTaskId, task.mUserId, - false /* restoreFromDisk */, false /* isLowResolution */); - if (snapshot != null) { - snapshotMap.put(task, snapshot); - // Defer until the runner calls back to cleanupScreenshot() - adapter.setSnapshotOverlay(snapshot); - } - } - snapshotController.addSkipClosingAppSnapshotTasks(snapshotMap.keySet()); - return snapshotMap; - } - - void cleanupAnimation(@ReorderMode int reorderMode) { - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, - "cleanupAnimation(): Notify animation finished mPendingAnimations=%d " - + "reorderMode=%d", - mPendingAnimations.size(), reorderMode); - if (reorderMode != REORDER_MOVE_TO_ORIGINAL_POSITION - && mTargetActivityRecord != mDisplayContent.topRunningActivity()) { - // Notify the state at the beginning because the removeAnimation may notify the - // transition is finished. This is a signal that there will be a next transition. - mDisplayContent.mFixedRotationTransitionListener.notifyRecentsWillBeTop(); - } - for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { - final TaskAnimationAdapter taskAdapter = mPendingAnimations.get(i); - if (reorderMode == REORDER_MOVE_TO_TOP || reorderMode == REORDER_KEEP_IN_PLACE) { - taskAdapter.mTask.dontAnimateDimExit(); - } - removeAnimation(taskAdapter); - taskAdapter.onCleanup(); - } - // Should already be empty, but clean-up pending task-appears in-case they weren't sent. - mPendingNewTaskTargets.clear(); - mPendingTaskAppears.clear(); - - for (int i = mPendingWallpaperAnimations.size() - 1; i >= 0; i--) { - final WallpaperAnimationAdapter wallpaperAdapter = mPendingWallpaperAnimations.get(i); - removeWallpaperAnimation(wallpaperAdapter); - } - - restoreNavigationBarFromApp( - reorderMode == REORDER_MOVE_TO_TOP || mIsAddingTaskToTargets /* animate */); - - // Clear any pending failsafe runnables - mService.mH.removeCallbacks(mFailsafeRunnable); - mDisplayContent.mAppTransition.unregisterListener(mAppTransitionListener); - - // Clear references to the runner - unlinkToDeathOfRunner(); - mRunner = null; - mCanceled = true; - - // Restore IME icon only when moving the original app task to front from recents, in case - // IME icon may missing if the moving task has already been the current focused task. - if (reorderMode == REORDER_MOVE_TO_ORIGINAL_POSITION && !mIsAddingTaskToTargets) { - InputMethodManagerInternal.get().updateImeWindowStatus( - false /* disableImeIcon */, mDisplayId); - } - - // Update the input windows after the animation is complete - final InputMonitor inputMonitor = mDisplayContent.getInputMonitor(); - inputMonitor.updateInputWindowsLw(true /*force*/); - - // We have deferred all notifications to the target app as a part of the recents animation, - // so if we are actually transitioning there, notify again here - if (mTargetActivityRecord != null) { - if (reorderMode == REORDER_MOVE_TO_TOP || reorderMode == REORDER_KEEP_IN_PLACE) { - mDisplayContent.mAppTransition.notifyAppTransitionFinishedLocked( - mTargetActivityRecord.token); - } - } - mDisplayContent.mFixedRotationTransitionListener.onFinishRecentsAnimation(); - - // Notify that the animation has ended - if (mStatusBar != null) { - mStatusBar.onRecentsAnimationStateChanged(false /* running */); - } - } - - void scheduleFailsafe() { - mService.mH.postDelayed(mFailsafeRunnable, FAILSAFE_DELAY); - } - - void onFailsafe() { - forceCancelAnimation( - mWillFinishToHome ? REORDER_MOVE_TO_TOP : REORDER_MOVE_TO_ORIGINAL_POSITION, - "onFailsafe"); - } - - private void linkToDeathOfRunner() throws RemoteException { - if (!mLinkedToDeathOfRunner) { - mRunner.asBinder().linkToDeath(this, 0); - mLinkedToDeathOfRunner = true; - } - } - - private void unlinkToDeathOfRunner() { - if (mLinkedToDeathOfRunner) { - mRunner.asBinder().unlinkToDeath(this, 0); - mLinkedToDeathOfRunner = false; - } - } - - @Override - public void binderDied() { - forceCancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "binderDied"); - - synchronized (mService.getWindowManagerLock()) { - // Clear associated input consumers on runner death - final InputMonitor inputMonitor = mDisplayContent.getInputMonitor(); - final InputConsumerImpl consumer = inputMonitor.getInputConsumer( - INPUT_CONSUMER_RECENTS_ANIMATION); - if (consumer != null) { - inputMonitor.destroyInputConsumer(consumer.mToken); - } - } - } - - void checkAnimationReady(WallpaperController wallpaperController) { - if (mPendingStart) { - final boolean wallpaperReady = !isTargetOverWallpaper() - || (wallpaperController.getWallpaperTarget() != null - && wallpaperController.wallpaperTransitionReady()); - if (wallpaperReady) { - mService.getRecentsAnimationController().startAnimation(); - } - } - } - - boolean isWallpaperVisible(WindowState w) { - return w != null && w.mAttrs.type == TYPE_BASE_APPLICATION && - ((w.mActivityRecord != null && mTargetActivityRecord == w.mActivityRecord) - || isAnimatingTask(w.getTask())) - && isTargetOverWallpaper() && w.isOnScreen(); - } - - /** - * @return Whether to use the input consumer to override app input to route home/recents. - */ - boolean shouldApplyInputConsumer(ActivityRecord activity) { - // Only apply the input consumer if it is enabled, it is not the target (home/recents) - // being revealed with the transition, and we are actively animating the app as a part of - // the animation - return mInputConsumerEnabled && activity != null - && !isTargetApp(activity) && isAnimatingApp(activity); - } - - boolean updateInputConsumerForApp(InputWindowHandle inputWindowHandle) { - // Update the input consumer touchable region to match the target app main window - final WindowState targetAppMainWindow = getTargetAppMainWindow(); - if (targetAppMainWindow != null) { - targetAppMainWindow.getBounds(mTmpRect); - inputWindowHandle.touchableRegion.set(mTmpRect); - return true; - } - return false; - } - - boolean isTargetApp(ActivityRecord activity) { - return mTargetActivityRecord != null && activity == mTargetActivityRecord; - } - - private boolean isTargetOverWallpaper() { - if (mTargetActivityRecord == null) { - return false; - } - return mTargetActivityRecord.windowsCanBeWallpaperTarget(); - } - - WindowState getTargetAppMainWindow() { - if (mTargetActivityRecord == null) { - return null; - } - return mTargetActivityRecord.findMainWindow(); - } - - DisplayArea getTargetAppDisplayArea() { - if (mTargetActivityRecord == null) { - return null; - } - return mTargetActivityRecord.getDisplayArea(); - } - - boolean isAnimatingTask(Task task) { - for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { - if (task == mPendingAnimations.get(i).mTask) { - return true; - } - } - return false; - } - - boolean isAnimatingWallpaper(WallpaperWindowToken token) { - for (int i = mPendingWallpaperAnimations.size() - 1; i >= 0; i--) { - if (token == mPendingWallpaperAnimations.get(i).getToken()) { - return true; - } - } - return false; - } - - private boolean isAnimatingApp(ActivityRecord activity) { - for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { - if (activity.isDescendantOf(mPendingAnimations.get(i).mTask)) { - return true; - } - } - return false; - } - - boolean shouldIgnoreForAccessibility(WindowState windowState) { - final Task task = windowState.getTask(); - return task != null && isAnimatingTask(task) && !isTargetApp(windowState.mActivityRecord); - } - - /** - * If the animation target ActivityRecord has a fixed rotation ({@link - * WindowToken#hasFixedRotationTransform()}, the provided wallpaper will be rotated accordingly. - * - * This avoids any screen rotation animation when animating to the Recents view. - */ - void linkFixedRotationTransformIfNeeded(@NonNull WindowToken wallpaper) { - if (mTargetActivityRecord == null) { - return; - } - wallpaper.linkFixedRotationTransform(mTargetActivityRecord); - } - - @VisibleForTesting - class TaskAnimationAdapter implements AnimationAdapter { - - private final Task mTask; - private SurfaceControl mCapturedLeash; - private OnAnimationFinishedCallback mCapturedFinishCallback; - private @AnimationType int mLastAnimationType; - private final boolean mIsRecentTaskInvisible; - private RemoteAnimationTarget mTarget; - private final Rect mBounds = new Rect(); - // The bounds of the target relative to its parent. - private final Rect mLocalBounds = new Rect(); - // The final surface transaction when animation is finished. - private PictureInPictureSurfaceTransaction mFinishTransaction; - // An overlay used to mask the content as an app goes into PIP - private SurfaceControl mFinishOverlay; - // An overlay used for canceling the animation with a screenshot - private SurfaceControl mSnapshotOverlay; - - TaskAnimationAdapter(Task task, boolean isRecentTaskInvisible) { - mTask = task; - mIsRecentTaskInvisible = isRecentTaskInvisible; - mBounds.set(mTask.getBounds()); - - mLocalBounds.set(mBounds); - Point tmpPos = new Point(); - mTask.getRelativePosition(tmpPos); - mLocalBounds.offsetTo(tmpPos.x, tmpPos.y); - } - - /** - * @param overrideTaskId overrides the target's taskId. It may differ from mTaskId and thus - * can differ from taskInfo. This mismatch is needed, however, in - * some cases where we are animating root tasks but need need leaf - * ids for identification. If this is INVALID (-1), then mTaskId - * will be used. - * @param overrideMode overrides the target's mode. If this is -1, the mode will be - * calculated relative to going to the target activity (ie. OPENING if - * this is the target task, CLOSING otherwise). - */ - RemoteAnimationTarget createRemoteAnimationTarget(int overrideTaskId, int overrideMode) { - ActivityRecord topApp = mTask.getTopRealVisibleActivity(); - if (topApp == null) { - topApp = mTask.getTopVisibleActivity(); - } - final WindowState mainWindow = topApp != null - ? topApp.findMainWindow() - : null; - if (mainWindow == null) { - return null; - } - final Rect insets = mainWindow.getInsetsStateWithVisibilityOverride().calculateInsets( - mBounds, Type.systemBars(), false /* ignoreVisibility */).toRect(); - InsetUtils.addInsets(insets, mainWindow.mActivityRecord.getLetterboxInsets()); - final int mode = overrideMode != MODE_UNKNOWN - ? overrideMode - : topApp.getActivityType() == mTargetActivityType - ? MODE_OPENING - : MODE_CLOSING; - if (overrideTaskId < 0) { - overrideTaskId = mTask.mTaskId; - } - mTarget = new RemoteAnimationTarget(overrideTaskId, mode, mCapturedLeash, - !topApp.fillsParent(), new Rect(), - insets, mTask.getPrefixOrderIndex(), new Point(mBounds.left, mBounds.top), - mLocalBounds, mBounds, mTask.getWindowConfiguration(), - mIsRecentTaskInvisible, null, null, mTask.getTaskInfo(), - topApp.checkEnterPictureInPictureAppOpsState()); - - final ActivityRecord topActivity = mTask.getTopNonFinishingActivity(); - if (topActivity != null && topActivity.mStartingData != null - && topActivity.mStartingData.hasImeSurface()) { - mTarget.setWillShowImeOnTarget(true); - } - return mTarget; - } - - void setSnapshotOverlay(TaskSnapshot snapshot) { - // Create a surface control for the snapshot and reparent it to the leash - final HardwareBuffer buffer = snapshot.getHardwareBuffer(); - if (buffer == null) { - return; - } - - final SurfaceSession session = new SurfaceSession(); - mSnapshotOverlay = mService.mSurfaceControlFactory.apply(session) - .setName("RecentTaskScreenshotSurface") - .setCallsite("TaskAnimationAdapter.setSnapshotOverlay") - .setFormat(buffer.getFormat()) - .setParent(mCapturedLeash) - .setBLASTLayer() - .build(); - - final float scale = 1.0f * mTask.getBounds().width() / buffer.getWidth(); - mTask.getPendingTransaction() - .setBuffer(mSnapshotOverlay, GraphicBuffer.createFromHardwareBuffer(buffer)) - .setColorSpace(mSnapshotOverlay, snapshot.getColorSpace()) - .setLayer(mSnapshotOverlay, Integer.MAX_VALUE) - .setMatrix(mSnapshotOverlay, scale, 0, 0, scale) - .show(mSnapshotOverlay) - .apply(); - } - - void onRemove() { - if (mSnapshotOverlay != null) { - // Clean up the snapshot overlay if necessary - mTask.getPendingTransaction() - .remove(mSnapshotOverlay) - .apply(); - mSnapshotOverlay = null; - } - mTask.setCanAffectSystemUiFlags(true); - mCapturedFinishCallback.onAnimationFinished(mLastAnimationType, this); - } - - void onCleanup() { - final Transaction pendingTransaction = mTask.getPendingTransaction(); - if (mFinishTransaction != null) { - // Reparent the overlay - if (mFinishOverlay != null) { - pendingTransaction.reparent(mFinishOverlay, mTask.mSurfaceControl); - } - - // Transfer the transform from the leash to the task - PictureInPictureSurfaceTransaction.apply(mFinishTransaction, - mTask.mSurfaceControl, pendingTransaction); - mTask.setLastRecentsAnimationTransaction(mFinishTransaction, mFinishOverlay); - if (mDisplayContent.isFixedRotationLaunchingApp(mTargetActivityRecord)) { - // The transaction is needed for position when rotating the display. - mDisplayContent.mPinnedTaskController.setEnterPipTransaction( - mFinishTransaction); - } - // In the case where we are transferring the transform to the task in preparation - // for entering PIP, we disable the task being able to affect sysui flags otherwise - // it may cause a flash - if (mTask.getActivityType() != mTargetActivityType - && mFinishTransaction.getShouldDisableCanAffectSystemUiFlags()) { - mTask.setCanAffectSystemUiFlags(false); - } - mFinishTransaction = null; - mFinishOverlay = null; - pendingTransaction.apply(); - } else if (!mTask.isAttached()) { - // Apply the task's pending transaction in case it is detached and its transaction - // is not reachable. - pendingTransaction.apply(); - } - } - - @VisibleForTesting - public SurfaceControl getSnapshotOverlay() { - return mSnapshotOverlay; - } - - @Override - public boolean getShowWallpaper() { - return false; - } - - @Override - public void startAnimation(SurfaceControl animationLeash, Transaction t, - @AnimationType int type, @NonNull OnAnimationFinishedCallback finishCallback) { - // Restore position and root task crop until client has a chance to modify it. - t.setPosition(animationLeash, mLocalBounds.left, mLocalBounds.top); - mTmpRect.set(mLocalBounds); - mTmpRect.offsetTo(0, 0); - t.setWindowCrop(animationLeash, mTmpRect); - mCapturedLeash = animationLeash; - mCapturedFinishCallback = finishCallback; - mLastAnimationType = type; - } - - @Override - public void onAnimationCancelled(SurfaceControl animationLeash) { - // Cancel the animation immediately if any single task animator is canceled - cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "taskAnimationAdapterCanceled"); - } - - @Override - public long getDurationHint() { - return 0; - } - - @Override - public long getStatusBarTransitionsStartTime() { - return SystemClock.uptimeMillis(); - } - - @Override - public void dump(PrintWriter pw, String prefix) { - pw.print(prefix); pw.println("task=" + mTask); - if (mTarget != null) { - pw.print(prefix); pw.println("Target:"); - mTarget.dump(pw, prefix + " "); - } else { - pw.print(prefix); pw.println("Target: null"); - } - pw.println("mIsRecentTaskInvisible=" + mIsRecentTaskInvisible); - pw.println("mLocalBounds=" + mLocalBounds); - pw.println("mFinishTransaction=" + mFinishTransaction); - pw.println("mBounds=" + mBounds); - pw.println("mIsRecentTaskInvisible=" + mIsRecentTaskInvisible); - } - - @Override - public void dumpDebug(ProtoOutputStream proto) { - final long token = proto.start(REMOTE); - if (mTarget != null) { - mTarget.dumpDebug(proto, TARGET); - } - proto.end(token); - } - } - - public void dump(PrintWriter pw, String prefix) { - final String innerPrefix = prefix + " "; - pw.print(prefix); pw.println(RecentsAnimationController.class.getSimpleName() + ":"); - pw.print(innerPrefix); pw.println("mPendingStart=" + mPendingStart); - pw.print(innerPrefix); pw.println("mPendingAnimations=" + mPendingAnimations.size()); - pw.print(innerPrefix); pw.println("mCanceled=" + mCanceled); - pw.print(innerPrefix); pw.println("mInputConsumerEnabled=" + mInputConsumerEnabled); - pw.print(innerPrefix); pw.println("mTargetActivityRecord=" + mTargetActivityRecord); - pw.print(innerPrefix); pw.println("isTargetOverWallpaper=" + isTargetOverWallpaper()); - pw.print(innerPrefix); pw.println("mRequestDeferCancelUntilNextTransition=" - + mRequestDeferCancelUntilNextTransition); - pw.print(innerPrefix); pw.println("mCancelOnNextTransitionStart=" - + mCancelOnNextTransitionStart); - pw.print(innerPrefix); pw.println("mCancelDeferredWithScreenshot=" - + mCancelDeferredWithScreenshot); - pw.print(innerPrefix); pw.println("mPendingCancelWithScreenshotReorderMode=" - + mPendingCancelWithScreenshotReorderMode); - } -} diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 862f84dbd73d..a6d965955272 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -804,12 +804,6 @@ class RootWindowContainer extends WindowContainer<DisplayContent> checkAppTransitionReady(surfacePlacer); - // Defer starting the recents animation until the wallpaper has drawn - final RecentsAnimationController recentsAnimationController = - mWmService.getRecentsAnimationController(); - if (recentsAnimationController != null) { - recentsAnimationController.checkAnimationReady(defaultDisplay.mWallpaperController); - } mWmService.mAtmService.mBackNavigationController .checkAnimationReady(defaultDisplay.mWallpaperController); @@ -1471,9 +1465,6 @@ class RootWindowContainer extends WindowContainer<DisplayContent> // Updates the extra information of the intent. if (fromHomeKey) { homeIntent.putExtra(WindowManagerPolicy.EXTRA_FROM_HOME_KEY, true); - if (mWindowManager.getRecentsAnimationController() != null) { - mWindowManager.getRecentsAnimationController().cancelAnimationForHomeStart(); - } } homeIntent.putExtra(WindowManagerPolicy.EXTRA_START_REASON, reason); diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java index 9cfd39688c12..57f9be097ee6 100644 --- a/services/core/java/com/android/server/wm/SurfaceAnimator.java +++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java @@ -32,8 +32,8 @@ import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.common.LogLevel; import com.android.internal.protolog.ProtoLog; +import com.android.internal.protolog.common.LogLevel; import java.io.PrintWriter; import java.io.StringWriter; @@ -585,7 +585,6 @@ public class SurfaceAnimator { ANIMATION_TYPE_APP_TRANSITION, ANIMATION_TYPE_SCREEN_ROTATION, ANIMATION_TYPE_DIMMER, - ANIMATION_TYPE_RECENTS, ANIMATION_TYPE_WINDOW_ANIMATION, ANIMATION_TYPE_INSETS_CONTROL, ANIMATION_TYPE_TOKEN_TRANSFORM, @@ -604,7 +603,6 @@ public class SurfaceAnimator { case ANIMATION_TYPE_APP_TRANSITION: return "app_transition"; case ANIMATION_TYPE_SCREEN_ROTATION: return "screen_rotation"; case ANIMATION_TYPE_DIMMER: return "dimmer"; - case ANIMATION_TYPE_RECENTS: return "recents_animation"; case ANIMATION_TYPE_WINDOW_ANIMATION: return "window_animation"; case ANIMATION_TYPE_INSETS_CONTROL: return "insets_animation"; case ANIMATION_TYPE_TOKEN_TRANSFORM: return "token_transform"; diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 13d6ed5e69c4..21be0fc2ac68 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -61,7 +61,6 @@ import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_LOCKTASK; -import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RECENTS_ANIMATIONS; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS; import static com.android.server.wm.ActivityRecord.State.PAUSED; @@ -95,7 +94,6 @@ import static com.android.server.wm.LockTaskController.LOCK_TASK_AUTH_DONT_LOCK; import static com.android.server.wm.LockTaskController.LOCK_TASK_AUTH_LAUNCHABLE; import static com.android.server.wm.LockTaskController.LOCK_TASK_AUTH_LAUNCHABLE_PRIV; import static com.android.server.wm.LockTaskController.LOCK_TASK_AUTH_PINNABLE; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS; import static com.android.server.wm.TaskProto.AFFINITY; import static com.android.server.wm.TaskProto.BOUNDS; import static com.android.server.wm.TaskProto.CREATED_BY_ORGANIZER; @@ -168,7 +166,6 @@ import android.view.InsetsState; import android.view.RemoteAnimationAdapter; import android.view.SurfaceControl; import android.view.WindowManager; -import android.view.WindowManager.TransitionOldType; import android.window.ITaskOrganizer; import android.window.PictureInPictureSurfaceTransaction; import android.window.StartingWindowInfo; @@ -2957,14 +2954,6 @@ class Task extends TaskFragment { if (isOrganized()) { return false; } - // Don't animate while the task runs recents animation but only if we are in the mode - // where we cancel with deferred screenshot, which means that the controller has - // transformed the task. - final RecentsAnimationController controller = mWmService.getRecentsAnimationController(); - if (controller != null && controller.isAnimatingTask(this) - && controller.shouldDeferCancelUntilNextTransition()) { - return false; - } return true; } @@ -2976,8 +2965,7 @@ class Task extends TaskFragment { /** Checking if self or its child tasks are animated by recents animation. */ boolean isAnimatingByRecents() { - return isAnimating(CHILDREN, ANIMATION_TYPE_RECENTS) - || mTransitionController.isTransientHide(this); + return mTransitionController.isTransientHide(this); } WindowState getTopVisibleAppMainWindow() { @@ -3282,30 +3270,6 @@ class Task extends TaskFragment { } @Override - protected void applyAnimationUnchecked(WindowManager.LayoutParams lp, boolean enter, - @TransitionOldType int transit, boolean isVoiceInteraction, - @Nullable ArrayList<WindowContainer> sources) { - final RecentsAnimationController control = mWmService.getRecentsAnimationController(); - if (control != null) { - // We let the transition to be controlled by RecentsAnimation, and callback task's - // RemoteAnimationTarget for remote runner to animate. - if (enter && !isActivityTypeHomeOrRecents()) { - ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, - "applyAnimationUnchecked, control: %s, task: %s, transit: %s", - control, asTask(), AppTransition.appTransitionOldToString(transit)); - final int size = sources != null ? sources.size() : 0; - control.addTaskToTargets(this, (type, anim) -> { - for (int i = 0; i < size; ++i) { - sources.get(i).onAnimationFinished(type, anim); - } - }); - } - } else { - super.applyAnimationUnchecked(lp, enter, transit, isVoiceInteraction, sources); - } - } - - @Override void dump(PrintWriter pw, String prefix, boolean dumpAll) { super.dump(pw, prefix, dumpAll); mAnimatingActivityRegistry.dump(pw, "AnimatingApps:", prefix); @@ -6230,26 +6194,6 @@ class Task extends TaskFragment { ActivityOptions.abort(options); } - boolean shouldSleepActivities() { - final DisplayContent display = mDisplayContent; - final boolean isKeyguardGoingAway = (mDisplayContent != null) - ? mDisplayContent.isKeyguardGoingAway() - : mRootWindowContainer.getDefaultDisplay().isKeyguardGoingAway(); - - // Do not sleep activities in this root task if we're marked as focused and the keyguard - // is in the process of going away. - if (isKeyguardGoingAway && isFocusedRootTaskOnDisplay() - // Avoid resuming activities on secondary displays since we don't want bubble - // activities to be resumed while bubble is still collapsed. - // TODO(b/113840485): Having keyguard going away state for secondary displays. - && display != null - && display.isDefaultDisplay) { - return false; - } - - return display != null ? display.isSleeping() : mAtmService.isSleepingLocked(); - } - private Rect getRawBounds() { return super.getBounds(); } diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index 01fea479e6da..638e92f112c7 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -143,13 +143,6 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { * current focused root task. */ Task mLastFocusedRootTask; - /** - * All of the root tasks on this display. Order matters, topmost root task is in front of all - * other root tasks, bottommost behind. Accessed directly by ActivityManager package classes. - * Any calls changing the list should also call {@link #onRootTaskOrderChanged(Task)}. - */ - private ArrayList<OnRootTaskOrderChangedListener> mRootTaskOrderChangedCallbacks = - new ArrayList<>(); /** * The task display area is removed from the system and we are just waiting for all activities @@ -332,7 +325,6 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { mAtmService.mTaskSupervisor.updateTopResumedActivityIfNeeded("addChildTask"); mAtmService.updateSleepIfNeededLocked(); - onRootTaskOrderChanged(task); } @Override @@ -424,10 +416,6 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { // Update the top resumed activity because the preferred top focusable task may be changed. mAtmService.mTaskSupervisor.updateTopResumedActivityIfNeeded("positionChildTaskAt"); - - if (mChildren.indexOf(child) != oldPosition) { - onRootTaskOrderChanged(child); - } } void onLeafTaskRemoved(int taskId) { @@ -844,7 +832,6 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { mLaunchAdjacentFlagRootTask = null; } mDisplayContent.releaseSelfIfNeeded(); - onRootTaskOrderChanged(rootTask); } /** @@ -1743,35 +1730,6 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { return mRemoved; } - /** - * Adds a listener to be notified whenever the root task order in the display changes. Currently - * only used by the {@link RecentsAnimation} to determine whether to interrupt and cancel the - * current animation when the system state changes. - */ - void registerRootTaskOrderChangedListener(OnRootTaskOrderChangedListener listener) { - if (!mRootTaskOrderChangedCallbacks.contains(listener)) { - mRootTaskOrderChangedCallbacks.add(listener); - } - } - - /** - * Removes a previously registered root task order change listener. - */ - void unregisterRootTaskOrderChangedListener(OnRootTaskOrderChangedListener listener) { - mRootTaskOrderChangedCallbacks.remove(listener); - } - - /** - * Notifies of a root task order change - * - * @param rootTask The root task which triggered the order change - */ - void onRootTaskOrderChanged(Task rootTask) { - for (int i = mRootTaskOrderChangedCallbacks.size() - 1; i >= 0; i--) { - mRootTaskOrderChangedCallbacks.get(i).onRootTaskOrderChanged(rootTask); - } - } - @Override boolean canCreateRemoteAnimationTarget() { // In the legacy transition system, promoting animation target from TaskFragment to @@ -1786,13 +1744,6 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { return mDisplayContent.isHomeSupported() && mCanHostHomeTask; } - /** - * Callback for when the order of the root tasks in the display changes. - */ - interface OnRootTaskOrderChangedListener { - void onRootTaskOrderChanged(Task rootTask); - } - void ensureActivitiesVisible(ActivityRecord starting, boolean notifyClients) { mAtmService.mTaskSupervisor.beginActivityVisibilityUpdate(); try { diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index c8139faab4c4..f58b322cab36 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -2145,8 +2145,22 @@ class TaskFragment extends WindowContainer<WindowContainer> { } boolean shouldSleepActivities() { - final Task task = getRootTask(); - return task != null && task.shouldSleepActivities(); + final DisplayContent dc = mDisplayContent; + if (dc == null) { + return mAtmService.isSleepingLocked(); + } + if (!dc.isSleeping()) { + return false; + } + // In case the unlocking order is keyguard-going-away -> screen-turning-on (display is + // sleeping by screen-off-token which may be notified to release from power manager's + // thread), keep the activities resume-able to avoid extra activity lifecycle when + // performing keyguard-going-away. This only applies to default display because currently + // the per-display keyguard-going-away state is assigned from a global signal. + if (!dc.isDefaultDisplay || !dc.isKeyguardGoingAway()) { + return true; + } + return !shouldBeVisible(null /* starting */); } @Override diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index e76e94d58800..82ede7efda5a 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -181,7 +181,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { final @TransitionType int mType; private int mSyncId = -1; private @TransitionFlags int mFlags; - private final TransitionController mController; + final TransitionController mController; private final BLASTSyncEngine mSyncEngine; private final Token mToken; @@ -329,6 +329,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { */ ArrayList<ActivityRecord> mConfigAtEndActivities = null; + /** The current head of the chain of actions related to this transition. */ + ActionChain mChainHead = null; + @VisibleForTesting Transition(@TransitionType int type, @TransitionFlags int flags, TransitionController controller, BLASTSyncEngine syncEngine) { @@ -1207,10 +1210,14 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { * The transition has finished animating and is ready to finalize WM state. This should not * be called directly; use {@link TransitionController#finishTransition} instead. */ - void finishTransition() { + void finishTransition(@NonNull ActionChain chain) { if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER) && mIsPlayerEnabled) { asyncTraceEnd(System.identityHashCode(this)); } + if (!chain.isFinishing()) { + throw new IllegalStateException("Can't finish on a non-finishing transition " + + chain.mTransition); + } mLogger.mFinishTimeNs = SystemClock.elapsedRealtimeNanos(); mController.mLoggerHandler.post(mLogger::logOnFinish); mController.mTransitionTracer.logFinishedTransition(this); @@ -1453,7 +1460,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // Clean up input monitors (for recents) final DisplayContent dc = mController.mAtm.mRootWindowContainer.getDisplayContent(mRecentsDisplayId); - dc.getInputMonitor().setActiveRecents(null /* activity */, null /* layer */); + dc.getInputMonitor().setActiveRecents(null /* task */, null /* layer */); dc.getInputMonitor().updateInputWindowsLw(false /* force */); } if (mTransientLaunches != null) { @@ -2163,7 +2170,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (mFinishTransaction != null) { mFinishTransaction.apply(); } - mController.finishTransition(this); + mController.finishTransition(mController.mAtm.mChainTracker.startFinish("clean-up", this)); } private void cleanUpInternal() { @@ -2249,7 +2256,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // Recents has an input-consumer to grab input from the "live tile" app. Set that up here final InputConsumerImpl recentsAnimationInputConsumer = dc.getInputMonitor().getInputConsumer(INPUT_CONSUMER_RECENTS_ANIMATION); - ActivityRecord recentsActivity = null; + Task recentsTask = null; if (recentsAnimationInputConsumer != null) { // Find the top-most going-away task and the recents activity. The top-most // is used as layer reference while the recents is used for registering the consumer @@ -2264,20 +2271,20 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { final int activityType = taskInfo.topActivityType; final boolean isRecents = activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS; - if (isRecents && recentsActivity == null) { - recentsActivity = task.getTopVisibleActivity(); + if (isRecents && recentsTask == null) { + recentsTask = task; } else if (!isRecents && topNonRecentsTask == null) { topNonRecentsTask = task; } } - if (recentsActivity != null && topNonRecentsTask != null) { + if (recentsTask != null && topNonRecentsTask != null) { recentsAnimationInputConsumer.mWindowHandle.touchableRegion.set( topNonRecentsTask.getBounds()); - dc.getInputMonitor().setActiveRecents(recentsActivity, topNonRecentsTask); + dc.getInputMonitor().setActiveRecents(recentsTask, topNonRecentsTask); } } - if (recentsActivity == null) { + if (recentsTask == null) { // No recents activity on `dc`, its probably on a different display. return; } @@ -3379,6 +3386,11 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return false; } + void recordChain(@NonNull ActionChain chain) { + chain.mPrevious = mChainHead; + mChainHead = chain; + } + @VisibleForTesting static class ChangeInfo { private static final int FLAG_NONE = 0; diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 1d2b693995c8..56a24dd4ca49 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -921,7 +921,12 @@ class TransitionController { } /** @see Transition#finishTransition */ - void finishTransition(Transition record) { + void finishTransition(@NonNull ActionChain chain) { + if (!chain.isFinishing()) { + throw new IllegalStateException("Can't finish on a non-finishing transition " + + chain.mTransition); + } + final Transition record = chain.mTransition; // It is usually a no-op but make sure that the metric consumer is removed. mTransitionMetricsReporter.reportAnimationStart(record.getToken(), 0 /* startTime */); // It is a no-op if the transition did not change the display. @@ -937,7 +942,7 @@ class TransitionController { mTrackCount = 0; } updateRunningRemoteAnimation(record, false /* isPlaying */); - record.finishTransition(); + record.finishTransition(chain); for (int i = mAnimatingExitWindows.size() - 1; i >= 0; i--) { final WindowState w = mAnimatingExitWindows.get(i); if (w.mAnimatingExit && w.mHasSurface && !w.inTransition()) { diff --git a/services/core/java/com/android/server/wm/TransparentPolicy.java b/services/core/java/com/android/server/wm/TransparentPolicy.java index 016b65ed06d8..f1941afe8f58 100644 --- a/services/core/java/com/android/server/wm/TransparentPolicy.java +++ b/services/core/java/com/android/server/wm/TransparentPolicy.java @@ -201,8 +201,10 @@ class TransparentPolicy { // never has letterbox. return true; } + final AppCompatSizeCompatModePolicy scmPolicy = mActivityRecord.mAppCompatController + .getAppCompatSizeCompatModePolicy(); if (mActivityRecord.getTask() == null || mActivityRecord.fillsParent() - || mActivityRecord.hasAppCompatDisplayInsetsWithoutInheritance()) { + || scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance()) { return true; } return false; diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 4536f244215c..06010bb1642e 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -172,8 +172,8 @@ class WallpaperController { && animatingContainer.getAnimation() != null && animatingContainer.getAnimation().getShowWallpaper(); final boolean hasWallpaper = w.hasWallpaper() || animationWallpaper; - if (isRecentsTransitionTarget(w) || isBackNavigationTarget(w)) { - if (DEBUG_WALLPAPER) Slog.v(TAG, "Found recents animation wallpaper target: " + w); + if (isBackNavigationTarget(w)) { + if (DEBUG_WALLPAPER) Slog.v(TAG, "Found back animation wallpaper target: " + w); mFindResults.setWallpaperTarget(w); return true; } else if (hasWallpaper @@ -199,15 +199,6 @@ class WallpaperController { return false; }; - private boolean isRecentsTransitionTarget(WindowState w) { - if (w.mTransitionController.isShellTransitionsEnabled()) { - return false; - } - // The window is either the recents activity or is in the task animating by the recents. - final RecentsAnimationController controller = mService.getRecentsAnimationController(); - return controller != null && controller.isWallpaperVisible(w); - } - private boolean isBackNavigationTarget(WindowState w) { // The window is in animating by back navigation and set to show wallpaper. return mService.mAtmService.mBackNavigationController.isWallpaperVisible(w); @@ -928,12 +919,6 @@ class WallpaperController { Slog.v(TAG, "*** WALLPAPER DRAW TIMEOUT"); } - // If there was a pending recents animation, start the animation anyways (it's better - // to not see the wallpaper than for the animation to not start) - if (mService.getRecentsAnimationController() != null) { - mService.getRecentsAnimationController().startAnimation(); - } - // If there was a pending back navigation animation that would show wallpaper, start // the animation due to it was skipped in previous surface placement. mService.mAtmService.mBackNavigationController.startAnimation(); diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java index 384d111eb058..89ad5640ef83 100644 --- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java +++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java @@ -161,15 +161,7 @@ class WallpaperWindowToken extends WindowToken { mDisplayContent.mWallpaperController.getWallpaperTarget(); if (visible && wallpaperTarget != null) { - final RecentsAnimationController recentsAnimationController = - mWmService.getRecentsAnimationController(); - if (recentsAnimationController != null - && recentsAnimationController.isAnimatingTask(wallpaperTarget.getTask())) { - // If the Recents animation is running, and the wallpaper target is the animating - // task we want the wallpaper to be rotated in the same orientation as the - // RecentsAnimation's target (e.g the launcher) - recentsAnimationController.linkFixedRotationTransformIfNeeded(this); - } else if ((wallpaperTarget.mActivityRecord == null + if ((wallpaperTarget.mActivityRecord == null // Ignore invisible activity because it may be moving to background. || wallpaperTarget.mActivityRecord.isVisibleRequested()) && wallpaperTarget.mToken.hasFixedRotationTransform()) { diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java index 03342d316d1c..13334a5f29b1 100644 --- a/services/core/java/com/android/server/wm/WindowAnimator.java +++ b/services/core/java/com/android/server/wm/WindowAnimator.java @@ -19,7 +19,6 @@ package com.android.server.wm; import static com.android.internal.protolog.ProtoLogGroup.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_RECENTS; 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; @@ -218,8 +217,8 @@ public class WindowAnimator { private void updateRunningExpensiveAnimationsLegacy() { final boolean runningExpensiveAnimations = mService.mRoot.isAnimating(TRANSITION | CHILDREN /* flags */, - ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_SCREEN_ROTATION - | ANIMATION_TYPE_RECENTS /* typesToCheck */); + ANIMATION_TYPE_APP_TRANSITION + | ANIMATION_TYPE_SCREEN_ROTATION /* typesToCheck */); if (runningExpensiveAnimations && !mRunningExpensiveAnimations) { mService.mSnapshotController.setPause(true); mTransaction.setEarlyWakeupStart(); diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index a980b7794547..6995027aac78 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -51,7 +51,6 @@ import static com.android.server.wm.IdentifierProto.TITLE; import static com.android.server.wm.IdentifierProto.USER_ID; 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_RECENTS; import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN; import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS; import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION; @@ -1242,8 +1241,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< */ boolean inTransitionSelfOrParent() { if (!mTransitionController.isShellTransitionsEnabled()) { - return isAnimating(PARENTS | TRANSITION, - ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS); + return isAnimating(PARENTS | TRANSITION, ANIMATION_TYPE_APP_TRANSITION); } return inTransition(); } diff --git a/services/core/java/com/android/server/wm/WindowContainerThumbnail.java b/services/core/java/com/android/server/wm/WindowContainerThumbnail.java index 57fc4c7a6860..80f3c44267d7 100644 --- a/services/core/java/com/android/server/wm/WindowContainerThumbnail.java +++ b/services/core/java/com/android/server/wm/WindowContainerThumbnail.java @@ -20,7 +20,7 @@ import static android.view.SurfaceControl.METADATA_OWNER_UID; import static android.view.SurfaceControl.METADATA_WINDOW_TYPE; import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_TRANSACTIONS; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS; +import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; import static com.android.server.wm.WindowContainerThumbnailProto.HEIGHT; import static com.android.server.wm.WindowContainerThumbnailProto.SURFACE_ANIMATOR; import static com.android.server.wm.WindowContainerThumbnailProto.WIDTH; @@ -118,14 +118,7 @@ class WindowContainerThumbnail implements Animatable { mWindowContainer.getDisplayContent().mAppTransition.canSkipFirstFrame(), mWindowContainer.getDisplayContent().getWindowCornerRadius()), mWindowContainer.mWmService.mSurfaceAnimationRunner), false /* hidden */, - ANIMATION_TYPE_RECENTS); - } - - /** - * Start animation with existing adapter. - */ - void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden) { - mSurfaceAnimator.startAnimation(t, anim, hidden, ANIMATION_TYPE_RECENTS); + ANIMATION_TYPE_APP_TRANSITION); } private void onAnimationFinished(@AnimationType int type, AnimationAdapter anim) { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index d8df645bf076..74931781e752 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -129,7 +129,6 @@ import static com.android.server.wm.RootWindowContainer.MATCH_ATTACHED_TASK_OR_R import static com.android.server.wm.SensitiveContentPackages.PackageInfo; 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_RECENTS; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION; import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN; import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS; @@ -246,7 +245,6 @@ import android.util.MergedConfiguration; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; -import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.util.TimeUtils; import android.util.TypedValue; @@ -266,7 +264,6 @@ import android.view.IDisplayWindowListener; import android.view.IInputFilter; import android.view.IOnKeyguardExitResult; import android.view.IPinnedTaskListener; -import android.view.IRecentsAnimationRunner; import android.view.IRotationWatcher; import android.view.IScrollCaptureResponseListener; import android.view.ISystemGestureExclusionListener; @@ -681,7 +678,6 @@ public class WindowManagerService extends IWindowManager.Stub private final SparseIntArray mOrientationMapping = new SparseIntArray(); final AccessibilityController mAccessibilityController; - private RecentsAnimationController mRecentsAnimationController; Watermark mWatermark; StrictModeFlash mStrictModeFlash; @@ -1159,17 +1155,12 @@ public class WindowManagerService extends IWindowManager.Stub return; } - // While running a recents animation, this will get called early because we show the - // recents animation target activity immediately when the animation starts. Defer the - // mLaunchTaskBehind updates until recents animation finishes. - if (atoken.mLaunchTaskBehind && !isRecentsAnimationTarget(atoken)) { + if (atoken.mLaunchTaskBehind) { mAtmService.mTaskSupervisor.scheduleLaunchTaskBehindComplete(atoken.token); atoken.mLaunchTaskBehind = false; } else { atoken.updateReportedVisibilityLocked(); - // We should also defer sending the finished callback until the recents animation - // successfully finishes. - if (atoken.mEnteringAnimation && !isRecentsAnimationTarget(atoken)) { + if (atoken.mEnteringAnimation) { atoken.mEnteringAnimation = false; if (atoken.attachedToProcess()) { try { @@ -2733,8 +2724,7 @@ public class WindowManagerService extends IWindowManager.Stub win.mTransitionController.mAnimatingExitWindows.add(win); reason = "inTransition"; } - } else if (win.isAnimating(PARENTS | TRANSITION, - ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS)) { + } else if (win.isAnimating(PARENTS | TRANSITION, ANIMATION_TYPE_APP_TRANSITION)) { // Already animating as part of a legacy app-transition. reason = "inLegacyTransition"; } @@ -3168,7 +3158,7 @@ public class WindowManagerService extends IWindowManager.Stub } // TODO(multi-display): remove when no default display use case. - // (i.e. KeyguardController / RecentsAnimation) + // (i.e. KeyguardController) public void executeAppTransition() { if (!checkCallingPermission(MANAGE_APP_TOKENS, "executeAppTransition()")) { throw new SecurityException("Requires MANAGE_APP_TOKENS permission"); @@ -3176,57 +3166,6 @@ public class WindowManagerService extends IWindowManager.Stub getDefaultDisplayContentLocked().executeAppTransition(); } - void initializeRecentsAnimation(int targetActivityType, - IRecentsAnimationRunner recentsAnimationRunner, - RecentsAnimationController.RecentsAnimationCallbacks callbacks, int displayId, - SparseBooleanArray recentTaskIds, ActivityRecord targetActivity) { - mRecentsAnimationController = new RecentsAnimationController(this, recentsAnimationRunner, - callbacks, displayId); - mRoot.getDisplayContent(displayId).mAppTransition.updateBooster(); - mRecentsAnimationController.initialize(targetActivityType, recentTaskIds, targetActivity); - } - - @VisibleForTesting - void setRecentsAnimationController(RecentsAnimationController controller) { - mRecentsAnimationController = controller; - } - - RecentsAnimationController getRecentsAnimationController() { - return mRecentsAnimationController; - } - - void cancelRecentsAnimation( - @RecentsAnimationController.ReorderMode int reorderMode, String reason) { - if (mRecentsAnimationController != null) { - // This call will call through to cleanupAnimation() below after the animation is - // canceled - mRecentsAnimationController.cancelAnimation(reorderMode, reason); - } - } - - - void cleanupRecentsAnimation(@RecentsAnimationController.ReorderMode int reorderMode) { - if (mRecentsAnimationController != null) { - final RecentsAnimationController controller = mRecentsAnimationController; - mRecentsAnimationController = null; - controller.cleanupAnimation(reorderMode); - // TODO(multi-display): currently only default display support recents animation. - final DisplayContent dc = getDefaultDisplayContentLocked(); - if (dc.mAppTransition.isTransitionSet()) { - dc.mSkipAppTransitionAnimation = true; - } - dc.forAllWindowContainers((wc) -> { - if (wc.isAnimating(TRANSITION, ANIMATION_TYPE_APP_TRANSITION)) { - wc.cancelAnimation(); - } - }); - } - } - - boolean isRecentsAnimationTarget(ActivityRecord r) { - return mRecentsAnimationController != null && mRecentsAnimationController.isTargetApp(r); - } - boolean isValidPictureInPictureAspectRatio(DisplayContent displayContent, float aspectRatio) { return displayContent.getPinnedTaskController().isValidPictureInPictureAspectRatio( aspectRatio); @@ -3258,11 +3197,6 @@ public class WindowManagerService extends IWindowManager.Stub } @Override - public void triggerAnimationFailsafe() { - mH.sendEmptyMessage(H.ANIMATION_FAILSAFE); - } - - @Override public void onKeyguardShowingAndNotOccludedChanged() { mH.sendEmptyMessage(H.RECOMPUTE_FOCUS); dispatchKeyguardLockedState(); @@ -5652,7 +5586,6 @@ public class WindowManagerService extends IWindowManager.Stub public static final int UPDATE_ANIMATION_SCALE = 51; public static final int WINDOW_HIDE_TIMEOUT = 52; public static final int SET_HAS_OVERLAY_UI = 58; - public static final int ANIMATION_FAILSAFE = 60; public static final int RECOMPUTE_FOCUS = 61; public static final int ON_POINTER_DOWN_OUTSIDE_FOCUS = 62; public static final int WINDOW_STATE_BLAST_SYNC_TIMEOUT = 64; @@ -5887,14 +5820,6 @@ public class WindowManagerService extends IWindowManager.Stub mAmInternal.setHasOverlayUi(msg.arg1, msg.arg2 == 1); break; } - case ANIMATION_FAILSAFE: { - synchronized (mGlobalLock) { - if (mRecentsAnimationController != null) { - mRecentsAnimationController.scheduleFailsafe(); - } - } - break; - } case RECOMPUTE_FOCUS: { synchronized (mGlobalLock) { updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL, @@ -7036,10 +6961,6 @@ public class WindowManagerService extends IWindowManager.Stub pw.print(" window="); pw.print(mWindowAnimationScaleSetting); pw.print(" transition="); pw.print(mTransitionAnimationScaleSetting); pw.print(" animator="); pw.println(mAnimatorDurationScaleSetting); - if (mRecentsAnimationController != null) { - pw.print(" mRecentsAnimationController="); pw.println(mRecentsAnimationController); - mRecentsAnimationController.dump(pw, " "); - } } } @@ -9024,6 +8945,22 @@ public class WindowManagerService extends IWindowManager.Stub // display it's on to the top since that window won't be able to get focus anyway. return; } + + final ActivityRecord touchedApp = t.getActivityRecord(); + if (touchedApp != null && touchedApp.getTask() != null) { + final ActivityRecord top = touchedApp.getTask().topRunningActivity(); + if (top != null && top != touchedApp && top.getTaskFragment().getBounds().contains( + touchedApp.getTaskFragment().getBounds())) { + // This is a special case where the pointer-down-outside focus on an Activity that's + // entirely occluded by the task top running activity, this is possible if the + // pointer-down-outside-focus event is delayed (after new activity started on top). + // In that case, drop the event to prevent changing focus to a background activity. + Slog.w(TAG, "onPointerDownOutsideFocusLocked, drop event because " + touchedApp + + " is occluded and should not be focused."); + return; + } + } + clearPointerDownOutsideFocusRunnable(); if (shouldDelayTouchOutside(t)) { @@ -9075,17 +9012,6 @@ public class WindowManagerService extends IWindowManager.Stub } clearPointerDownOutsideFocusRunnable(); - if (mRecentsAnimationController != null - && mRecentsAnimationController.getTargetAppMainWindow() == t) { - // If there is an active recents animation and touched window is the target, - // then ignore the touch. The target already handles touches using its own - // input monitor and we don't want to trigger any lifecycle changes from - // focusing another window. - // TODO(b/186770026): We should remove this once we support multiple resumed - // activities while in overview - return; - } - final WindowState w = t.getWindowState(); if (w != null) { final Task task = w.getTask(); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index e1e64ee10245..60ccdc72ee09 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -223,7 +223,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final long ident = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { - applyTransaction(t, -1 /*syncId*/, null /*transition*/, caller); + final ActionChain chain = mService.mChainTracker.startLegacy("applyTransactLegacy"); + applyTransaction(t, -1 /*syncId*/, chain, caller); } } finally { Binder.restoreCallingIdentity(ident); @@ -242,7 +243,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub try { synchronized (mGlobalLock) { if (callback == null) { - applyTransaction(t, -1 /* syncId*/, null /*transition*/, caller); + final ActionChain chain = mService.mChainTracker.startLegacy("applySyncLegacy"); + applyTransaction(t, -1 /* syncId*/, chain, caller); return -1; } @@ -262,13 +264,15 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final int syncId = syncGroup.mSyncId; if (mTransitionController.isShellTransitionsEnabled()) { mTransitionController.startLegacySyncOrQueue(syncGroup, (deferred) -> { - applyTransaction(t, syncId, null /* transition */, caller, deferred); + applyTransaction(t, syncId, mService.mChainTracker.startLegacy( + "applySyncLegacy"), caller, deferred); setSyncReady(syncId); }); } else { if (!mService.mWindowManager.mSyncEngine.hasActiveSync()) { mService.mWindowManager.mSyncEngine.startSyncSet(syncGroup); - applyTransaction(t, syncId, null /*transition*/, caller); + applyTransaction(t, syncId, mService.mChainTracker.startLegacy( + "applySyncLegacy"), caller); setSyncReady(syncId); } else { // Because the BLAST engine only supports one sync at a time, queue the @@ -276,7 +280,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub mService.mWindowManager.mSyncEngine.queueSyncSet( () -> mService.mWindowManager.mSyncEngine.startSyncSet(syncGroup), () -> { - applyTransaction(t, syncId, null /*transition*/, caller); + applyTransaction(t, syncId, mService.mChainTracker.startLegacy( + "applySyncLegacy"), caller); setSyncReady(syncId); }); } @@ -313,7 +318,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub throw new IllegalArgumentException("Can't use legacy transitions in" + " compatibility mode with no WCT."); } - applyTransaction(t, -1 /* syncId */, null, caller); + applyTransaction(t, -1 /* syncId */, + mService.mChainTracker.startLegacy("wrongLegacyTransit"), caller); return null; } final WindowContainerTransaction wct = @@ -334,10 +340,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub nextTransition.calcParallelCollectType(wct); mTransitionController.startCollectOrQueue(nextTransition, (deferred) -> { + final ActionChain chain = mService.mChainTracker.start( + "startNewTransit", nextTransition); nextTransition.start(); nextTransition.mLogger.mStartWCT = wct; - applyTransaction(wct, -1 /* syncId */, nextTransition, caller, - deferred); + applyTransaction(wct, -1 /* syncId */, chain, caller, deferred); wctApplied.meet(); if (needsSetReady) { setAllReadyIfNeeded(nextTransition, wct); @@ -351,7 +358,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub Slog.e(TAG, "Trying to start a transition that isn't collecting. This probably" + " means Shell took too long to respond to a request. WM State may be" + " incorrect now, please file a bug"); - applyTransaction(wct, -1 /*syncId*/, null /*transition*/, caller); + final ActionChain chain = mService.mChainTracker.startFailsafe("startTransit"); + chain.mTransition = null; + applyTransaction(wct, -1 /*syncId*/, chain, caller); return transition.getToken(); } // Currently, application of wct can span multiple looper loops (ie. @@ -367,16 +376,20 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (transition.shouldApplyOnDisplayThread()) { mService.mH.post(() -> { synchronized (mService.mGlobalLock) { + final ActionChain chain = mService.mChainTracker.start( + "startTransit", transition); transition.start(); - applyTransaction(wct, -1 /* syncId */, transition, caller); + applyTransaction(wct, -1 /* syncId */, chain, caller); if (wctApplied != null) { wctApplied.meet(); } } }); } else { + final ActionChain chain = mService.mChainTracker.start("startTransit", + transition); transition.start(); - applyTransaction(wct, -1 /* syncId */, transition, caller); + applyTransaction(wct, -1 /* syncId */, chain, caller); if (wctApplied != null) { wctApplied.meet(); } @@ -475,7 +488,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub dc.mAppTransition.overridePendingAppTransitionRemote(adapter, true /* sync */, false /* isActivityEmbedding */); syncId = startSyncWithOrganizer(callback); - applyTransaction(t, syncId, null /* transition */, caller); + applyTransaction(t, syncId, mService.mChainTracker.startLegacy("legacyTransit"), + caller); setSyncReady(syncId); } } finally { @@ -493,6 +507,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub try { synchronized (mGlobalLock) { final Transition transition = Transition.fromBinder(transitionToken); + final ActionChain chain = + mService.mChainTracker.startFinish("finishTransit", transition); // apply the incoming transaction before finish in case it alters the visibility // of the participants. if (t != null) { @@ -500,9 +516,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub // changes of the transition participants will only set visible-requested // and still let finishTransition handle the participants. mTransitionController.mFinishingTransition = transition; - applyTransaction(t, -1 /* syncId */, null /*transition*/, caller, transition); + applyTransaction(t, -1 /* syncId */, chain, caller); } - mTransitionController.finishTransition(transition); + mTransitionController.finishTransition(chain); mTransitionController.mFinishingTransition = null; } } finally { @@ -537,9 +553,10 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final CallerInfo caller = new CallerInfo(); final long ident = Binder.clearCallingIdentity(); try { - if (mTransitionController.getTransitionPlayer() == null) { + if (!mTransitionController.isShellTransitionsEnabled()) { // No need to worry about transition when Shell transition is not enabled. - applyTransaction(wct, -1 /* syncId */, null /* transition */, caller); + applyTransaction(wct, -1 /* syncId */, + mService.mChainTracker.startLegacy("legacyTFTransact"), caller); return; } @@ -548,8 +565,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub // Although there is an active sync, we want to apply the transaction now. // TODO(b/232042367) Redesign the organizer update on activity callback so that we // we will know about the transition explicitly. - final Transition transition = mTransitionController.getCollectingTransition(); - if (transition == null) { + final ActionChain chain = mService.mChainTracker.startDefault("tfTransact"); + if (chain.mTransition == null) { // This should rarely happen, and we should try to avoid using // {@link #applySyncTransaction} with Shell transition. // We still want to apply and merge the transaction to the active sync @@ -559,7 +576,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub + " because there is an ongoing sync for" + " applySyncTransaction()."); } - applyTransaction(wct, -1 /* syncId */, transition, caller); + applyTransaction(wct, -1 /* syncId */, chain, caller); return; } @@ -570,8 +587,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub transition.abort(); return; } - if (applyTransaction(wct, -1 /* syncId */, transition, caller, deferred) - == TRANSACT_EFFECTS_NONE && transition.mParticipants.isEmpty()) { + final ActionChain chain = mService.mChainTracker.start("tfTransact", transition); + final int effects = applyTransaction(wct, -1 /* syncId */, chain, caller, deferred); + if (effects == TRANSACT_EFFECTS_NONE && transition.mParticipants.isEmpty()) { transition.abort(); return; } @@ -586,15 +604,10 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } private int applyTransaction(@NonNull WindowContainerTransaction t, int syncId, - @Nullable Transition transition, @NonNull CallerInfo caller) { - return applyTransaction(t, syncId, transition, caller, null /* finishTransition */); - } - - private int applyTransaction(@NonNull WindowContainerTransaction t, int syncId, - @Nullable Transition transition, @NonNull CallerInfo caller, boolean deferred) { + @NonNull ActionChain chain, @NonNull CallerInfo caller, boolean deferred) { if (deferred) { try { - return applyTransaction(t, syncId, transition, caller); + return applyTransaction(t, syncId, chain, caller); } catch (RuntimeException e) { // If the transaction is deferred, the caller could be from TransitionController // #tryStartCollectFromQueue that executes on system's worker thread rather than @@ -604,19 +617,17 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } return TRANSACT_EFFECTS_NONE; } - return applyTransaction(t, syncId, transition, caller); + return applyTransaction(t, syncId, chain, caller); } /** * @param syncId If non-null, this will be a sync-transaction. - * @param transition A transition to collect changes into. + * @param chain A lifecycle-chain to acculumate changes into. * @param caller Info about the calling process. - * @param finishTransition The transition that is currently being finished. * @return The effects of the window container transaction. */ private int applyTransaction(@NonNull WindowContainerTransaction t, int syncId, - @Nullable Transition transition, @NonNull CallerInfo caller, - @Nullable Transition finishTransition) { + @NonNull ActionChain chain, @NonNull CallerInfo caller) { int effects = TRANSACT_EFFECTS_NONE; ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Apply window transaction, syncId=%d", syncId); mService.deferWindowLayout(); @@ -624,20 +635,21 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub boolean deferResume = true; mService.mTaskSupervisor.setDeferRootVisibilityUpdate(true /* deferUpdate */); boolean deferTransitionReady = false; - if (transition != null && !t.isEmpty()) { - if (transition.isCollecting()) { + if (chain.mTransition != null && !t.isEmpty() && !chain.isFinishing()) { + if (chain.mTransition.isCollecting()) { deferTransitionReady = true; - transition.deferTransitionReady(); + chain.mTransition.deferTransitionReady(); } else { Slog.w(TAG, "Transition is not collecting when applyTransaction." - + " transition=" + transition + " state=" + transition.getState()); - transition = null; + + " transition=" + chain.mTransition + " state=" + + chain.mTransition.getState()); + chain.mTransition = null; } } try { final ArraySet<WindowContainer<?>> haveConfigChanges = new ArraySet<>(); - if (transition != null) { - transition.applyDisplayChangeIfNeeded(haveConfigChanges); + if (chain.mTransition != null) { + chain.mTransition.applyDisplayChangeIfNeeded(haveConfigChanges); if (!haveConfigChanges.isEmpty()) { effects |= TRANSACT_EFFECTS_CLIENT_CONFIG; } @@ -645,7 +657,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final List<WindowContainerTransaction.HierarchyOp> hops = t.getHierarchyOps(); final int hopSize = hops.size(); Iterator<Map.Entry<IBinder, WindowContainerTransaction.Change>> entries; - if (transition != null) { + if (chain.mTransition != null) { // Mark any config-at-end containers before applying config changes so that // the config changes don't dispatch to client. entries = t.getChanges().entrySet().iterator(); @@ -655,7 +667,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (!entry.getValue().getConfigAtTransitionEnd()) continue; final WindowContainer wc = WindowContainer.fromBinder(entry.getKey()); if (wc == null || !wc.isAttached()) continue; - transition.setConfigAtEnd(wc); + chain.mTransition.setConfigAtEnd(wc); } } entries = t.getChanges().entrySet().iterator(); @@ -672,15 +684,13 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (syncId >= 0) { addToSyncSet(syncId, wc); } - if (transition != null) transition.collect(wc); + chain.collect(wc); if ((entry.getValue().getChangeMask() & WindowContainerTransaction.Change.CHANGE_FORCE_NO_PIP) != 0) { // Disable entering pip (eg. when recents pretends to finish itself) - if (finishTransition != null) { - finishTransition.setCanPipOnFinish(false /* canPipOnFinish */); - } else if (transition != null) { - transition.setCanPipOnFinish(false /* canPipOnFinish */); + if (chain.mTransition != null) { + chain.mTransition.setCanPipOnFinish(false /* canPipOnFinish */); } } // A bit hacky, but we need to detect "remove PiP" so that we can "wrap" the @@ -728,9 +738,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (hopSize > 0) { final boolean isInLockTaskMode = mService.isInLockTaskMode(); for (int i = 0; i < hopSize; ++i) { - effects |= applyHierarchyOp(hops.get(i), effects, syncId, transition, + effects |= applyHierarchyOp(hops.get(i), effects, syncId, chain, isInLockTaskMode, caller, t.getErrorCallbackToken(), - t.getTaskFragmentOrganizer(), finishTransition); + t.getTaskFragmentOrganizer()); } } // Queue-up bounds-change transactions for tasks which are now organized. Do @@ -789,7 +799,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } } finally { if (deferTransitionReady) { - transition.continueTransitionReady(); + chain.mTransition.continueTransitionReady(); } mService.mTaskSupervisor.setDeferRootVisibilityUpdate(false /* deferUpdate */); if (deferResume) { @@ -1079,9 +1089,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } private int applyHierarchyOp(WindowContainerTransaction.HierarchyOp hop, int effects, - int syncId, @Nullable Transition transition, boolean isInLockTaskMode, + int syncId, @NonNull ActionChain chain, boolean isInLockTaskMode, @NonNull CallerInfo caller, @Nullable IBinder errorCallbackToken, - @Nullable ITaskFragmentOrganizer organizer, @Nullable Transition finishTransition) { + @Nullable ITaskFragmentOrganizer organizer) { final int type = hop.getType(); switch (type) { case HIERARCHY_OP_TYPE_REMOVE_TASK: { @@ -1151,7 +1161,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub break; } case HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT: { - effects |= reparentChildrenTasksHierarchyOp(hop, transition, syncId, + effects |= reparentChildrenTasksHierarchyOp(hop, chain.mTransition, syncId, isInLockTaskMode); break; } @@ -1204,13 +1214,13 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (syncId >= 0) { addToSyncSet(syncId, wc); } - if (transition != null) { - transition.collect(wc); + if (chain.mTransition != null) { + chain.mTransition.collect(wc); if (hop.isReparent()) { if (wc.getParent() != null) { // Collect the current parent. It's visibility may change as // a result of this reparenting. - transition.collect(wc.getParent()); + chain.mTransition.collect(wc.getParent()); } if (hop.getNewParent() != null) { final WindowContainer parentWc = @@ -1219,7 +1229,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub Slog.e(TAG, "Can't resolve parent window from token"); break; } - transition.collect(parentWc); + chain.mTransition.collect(parentWc); } } } @@ -1233,8 +1243,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub break; } case HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION: { - effects |= applyTaskFragmentOperation(hop, transition, isInLockTaskMode, caller, - errorCallbackToken, organizer); + effects |= applyTaskFragmentOperation(hop, chain, isInLockTaskMode, + caller, errorCallbackToken, organizer); break; } case HIERARCHY_OP_TYPE_PENDING_INTENT: { @@ -1348,13 +1358,13 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub break; } case HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER: { - if (finishTransition == null) break; + if (!chain.isFinishing()) break; final WindowContainer container = WindowContainer.fromBinder(hop.getContainer()); if (container == null) break; final Task thisTask = container.asActivityRecord() != null ? container.asActivityRecord().getTask() : container.asTask(); if (thisTask == null) break; - final Task restoreAt = finishTransition.getTransientLaunchRestoreTarget(container); + final Task restoreAt = chain.mTransition.getTransientLaunchRestoreTarget(container); if (restoreAt == null) break; final TaskDisplayArea taskDisplayArea = thisTask.getTaskDisplayArea(); taskDisplayArea.moveRootTaskBehindRootTask(thisTask.getRootTask(), restoreAt); @@ -1444,7 +1454,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub * {@link #TRANSACT_EFFECTS_LIFECYCLE} or {@link #TRANSACT_EFFECTS_CLIENT_CONFIG}. */ private int applyTaskFragmentOperation(@NonNull WindowContainerTransaction.HierarchyOp hop, - @Nullable Transition transition, boolean isInLockTaskMode, @NonNull CallerInfo caller, + @NonNull ActionChain chain, boolean isInLockTaskMode, @NonNull CallerInfo caller, @Nullable IBinder errorCallbackToken, @Nullable ITaskFragmentOrganizer organizer) { if (!validateTaskFragmentOperation(hop, errorCallbackToken, organizer)) { return TRANSACT_EFFECTS_NONE; @@ -1467,7 +1477,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub break; } createTaskFragment(taskFragmentCreationParams, errorCallbackToken, caller, - transition); + chain.mTransition); break; } case OP_TYPE_DELETE_TASK_FRAGMENT: { @@ -1484,7 +1494,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub break; } } - effects |= deleteTaskFragment(taskFragment, transition); + effects |= deleteTaskFragment(taskFragment, chain.mTransition); break; } case OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: { @@ -1533,14 +1543,14 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub opType, exception); break; } - if (transition != null) { - transition.collect(activity); + if (chain.mTransition != null) { + chain.collect(activity); if (activity.getParent() != null) { // Collect the current parent. Its visibility may change as a result of // this reparenting. - transition.collect(activity.getParent()); + chain.collect(activity.getParent()); } - transition.collect(taskFragment); + chain.collect(taskFragment); } activity.reparent(taskFragment, POSITION_TOP); effects |= TRANSACT_EFFECTS_LIFECYCLE; @@ -1696,8 +1706,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub // If any TaskFragment in the Task is collected by the transition, we make the decor // surface visible in sync with the TaskFragment transition. Otherwise, we make the // decor surface visible immediately. - final TaskFragment syncTaskFragment = transition != null - ? task.getTaskFragment(transition.mParticipants::contains) + final TaskFragment syncTaskFragment = chain.mTransition != null + ? task.getTaskFragment(chain.mTransition.mParticipants::contains) : null; if (syncTaskFragment != null) { @@ -1749,7 +1759,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub // The decor surface boost/unboost must be applied after the transition is // completed. Otherwise, the decor surface could be moved before Shell completes // the transition, causing flicker. - runAfterTransition(transition, task::commitDecorSurfaceBoostedState); + runAfterTransition(chain.mTransition, task::commitDecorSurfaceBoostedState); } break; } diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index d2aebdeed99a..d96ebc6655ac 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -284,14 +284,11 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio static final int ANIMATING_REASON_REMOTE_ANIMATION = 1; /** It is set for wakefulness transition. */ static final int ANIMATING_REASON_WAKEFULNESS_CHANGE = 1 << 1; - /** Whether the legacy {@link RecentsAnimation} is running. */ - static final int ANIMATING_REASON_LEGACY_RECENT_ANIMATION = 1 << 2; @Retention(RetentionPolicy.SOURCE) @IntDef({ ANIMATING_REASON_REMOTE_ANIMATION, ANIMATING_REASON_WAKEFULNESS_CHANGE, - ANIMATING_REASON_LEGACY_RECENT_ANIMATION, }) @interface AnimatingReason {} @@ -2017,14 +2014,6 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio return mStoppedState == STOPPED_STATE_FIRST_LAUNCH; } - void setRunningRecentsAnimation(boolean running) { - if (running) { - addAnimatingReason(ANIMATING_REASON_LEGACY_RECENT_ANIMATION); - } else { - removeAnimatingReason(ANIMATING_REASON_LEGACY_RECENT_ANIMATION); - } - } - void setRunningRemoteAnimation(boolean running) { if (running) { addAnimatingReason(ANIMATING_REASON_REMOTE_ANIMATION); @@ -2119,9 +2108,6 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio if ((animatingReasons & ANIMATING_REASON_WAKEFULNESS_CHANGE) != 0) { pw.print("wakefulness|"); } - if ((animatingReasons & ANIMATING_REASON_LEGACY_RECENT_ANIMATION) != 0) { - pw.print("legacy-recents"); - } pw.println(); } if (mUseFifoUiScheduling) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index eed0cf78baf4..e5e153a41dbc 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -128,7 +128,6 @@ import static com.android.server.wm.MoveAnimationSpecProto.FROM; import static com.android.server.wm.MoveAnimationSpecProto.TO; 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_RECENTS; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_STARTING_REVEAL; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION; import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS; @@ -581,7 +580,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP * is guaranteed to be cleared. */ static final int EXIT_ANIMATING_TYPES = ANIMATION_TYPE_APP_TRANSITION - | ANIMATION_TYPE_WINDOW_ANIMATION | ANIMATION_TYPE_RECENTS; + | ANIMATION_TYPE_WINDOW_ANIMATION; /** Currently running an exit animation? */ boolean mAnimatingExit; @@ -1971,13 +1970,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP * it must be drawn before allDrawn can become true. */ boolean isInteresting() { - final RecentsAnimationController recentsAnimationController = - mWmService.getRecentsAnimationController(); return mActivityRecord != null && (!mActivityRecord.isFreezingScreen() || !mAppFreezing) - && mViewVisibility == View.VISIBLE - && (recentsAnimationController == null - || recentsAnimationController.isInterestingForAllDrawn(this)); + && mViewVisibility == View.VISIBLE; } /** @@ -4671,17 +4666,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (!isImeLayeringTarget()) { return false; } - if (!com.android.window.flags.Flags.doNotSkipImeByTargetVisibility()) { - // Note that we don't process IME window if the IME input target is not on the screen. - // In case some unexpected IME visibility cases happen like starting the remote - // animation on the keyguard but seeing the IME window that originally on the app - // which behinds the keyguard. - final WindowState imeInputTarget = getImeInputTarget(); - if (imeInputTarget != null - && !(imeInputTarget.isDrawn() || imeInputTarget.isVisibleRequested())) { - return false; - } - } return mDisplayContent.forAllImeWindows(callback, traverseTopToBottom); } diff --git a/services/core/java/com/android/server/wm/WindowTracingDataSource.java b/services/core/java/com/android/server/wm/WindowTracingDataSource.java index 2c5a4538863e..dc048ef8c8ec 100644 --- a/services/core/java/com/android/server/wm/WindowTracingDataSource.java +++ b/services/core/java/com/android/server/wm/WindowTracingDataSource.java @@ -35,7 +35,6 @@ import android.util.proto.ProtoInputStream; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; public final class WindowTracingDataSource extends DataSource<WindowTracingDataSource.Instance, WindowTracingDataSource.TlsState, Void> { @@ -77,15 +76,11 @@ public final class WindowTracingDataSource extends DataSource<WindowTracingDataS private static final String TAG = "WindowTracingDataSource"; @NonNull - private final WeakReference<Consumer<Config>> mOnStartCallback; - @NonNull - private final WeakReference<Consumer<Config>> mOnStopCallback; + private final WeakReference<WindowTracingPerfetto> mWindowTracing; - public WindowTracingDataSource(@NonNull Consumer<Config> onStart, - @NonNull Consumer<Config> onStop) { + public WindowTracingDataSource(WindowTracingPerfetto windowTracing) { super(DATA_SOURCE_NAME); - mOnStartCallback = new WeakReference(onStart); - mOnStopCallback = new WeakReference(onStop); + mWindowTracing = new WeakReference<>(windowTracing); Producer.init(InitArguments.DEFAULTS); DataSourceParams params = @@ -94,6 +89,7 @@ public final class WindowTracingDataSource extends DataSource<WindowTracingDataS PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT) .build(); register(params); + Log.i(TAG, "Registered with perfetto service"); } @Override @@ -103,17 +99,17 @@ public final class WindowTracingDataSource extends DataSource<WindowTracingDataS return new Instance(this, instanceIndex, config != null ? config : CONFIG_DEFAULT) { @Override protected void onStart(StartCallbackArguments args) { - Consumer<Config> callback = mOnStartCallback.get(); - if (callback != null) { - callback.accept(mConfig); + WindowTracingPerfetto windowTracing = mWindowTracing.get(); + if (windowTracing != null) { + windowTracing.onStart(mConfig); } } @Override protected void onStop(StopCallbackArguments args) { - Consumer<Config> callback = mOnStopCallback.get(); - if (callback != null) { - callback.accept(mConfig); + WindowTracingPerfetto windowTracing = mWindowTracing.get(); + if (windowTracing != null) { + windowTracing.onStop(mConfig); } } }; diff --git a/services/core/java/com/android/server/wm/WindowTracingPerfetto.java b/services/core/java/com/android/server/wm/WindowTracingPerfetto.java index cf948ca76f99..22d6c863fd4f 100644 --- a/services/core/java/com/android/server/wm/WindowTracingPerfetto.java +++ b/services/core/java/com/android/server/wm/WindowTracingPerfetto.java @@ -35,8 +35,7 @@ class WindowTracingPerfetto extends WindowTracing { private final AtomicInteger mCountSessionsOnFrame = new AtomicInteger(); private final AtomicInteger mCountSessionsOnTransaction = new AtomicInteger(); - private final WindowTracingDataSource mDataSource = new WindowTracingDataSource( - this::onStart, this::onStop); + private final WindowTracingDataSource mDataSource = new WindowTracingDataSource(this); WindowTracingPerfetto(WindowManagerService service, Choreographer choreographer) { this(service, choreographer, service.mGlobalLock); @@ -156,7 +155,7 @@ class WindowTracingPerfetto extends WindowTracing { return mCountSessionsOnTransaction.get() > 0; } - private void onStart(WindowTracingDataSource.Config config) { + void onStart(WindowTracingDataSource.Config config) { if (config.mLogFrequency == WindowTracingLogFrequency.FRAME) { mCountSessionsOnFrame.incrementAndGet(); } else if (config.mLogFrequency == WindowTracingLogFrequency.TRANSACTION) { @@ -168,7 +167,7 @@ class WindowTracingPerfetto extends WindowTracing { log(WHERE_START_TRACING); } - private void onStop(WindowTracingDataSource.Config config) { + void onStop(WindowTracingDataSource.Config config) { if (config.mLogFrequency == WindowTracingLogFrequency.FRAME) { mCountSessionsOnFrame.decrementAndGet(); } else if (config.mLogFrequency == WindowTracingLogFrequency.TRANSACTION) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 1290fb7ef91a..a80ee0f66742 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -2726,22 +2726,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return; } - if (Flags.securityLogV2Enabled()) { - boolean auditLoggingEnabled = Boolean.TRUE.equals( - mDevicePolicyEngine.getResolvedPolicy( - PolicyDefinition.AUDIT_LOGGING, UserHandle.USER_ALL)); - boolean securityLoggingEnabled = Boolean.TRUE.equals( - mDevicePolicyEngine.getResolvedPolicy( - PolicyDefinition.SECURITY_LOGGING, UserHandle.USER_ALL)); - setLoggingConfiguration(securityLoggingEnabled, auditLoggingEnabled); - mInjector.runCryptoSelfTest(); - } else { - synchronized (getLockObject()) { - mSecurityLogMonitor.start(getSecurityLoggingEnabledUser()); - mInjector.runCryptoSelfTest(); - maybePauseDeviceWideLoggingLocked(); - } - } + boolean auditLoggingEnabled = Boolean.TRUE.equals( + mDevicePolicyEngine.getResolvedPolicy( + PolicyDefinition.AUDIT_LOGGING, UserHandle.USER_ALL)); + boolean securityLoggingEnabled = Boolean.TRUE.equals( + mDevicePolicyEngine.getResolvedPolicy( + PolicyDefinition.SECURITY_LOGGING, UserHandle.USER_ALL)); + setLoggingConfiguration(securityLoggingEnabled, auditLoggingEnabled); + mInjector.runCryptoSelfTest(); } /** @@ -3399,7 +3391,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @GuardedBy("getLockObject()") private void maybeMigrateSecurityLoggingPolicyLocked() { - if (!Flags.securityLogV2Enabled() || mOwners.isSecurityLoggingMigrated()) { + if (mOwners.isSecurityLoggingMigrated()) { return; } @@ -16304,9 +16296,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public void enforceSecurityLoggingPolicy(boolean enabled) { - if (!Flags.securityLogV2Enabled()) { - return; - } Boolean auditLoggingEnabled = mDevicePolicyEngine.getResolvedPolicy( PolicyDefinition.AUDIT_LOGGING, UserHandle.USER_ALL); enforceLoggingPolicy(enabled, Boolean.TRUE.equals(auditLoggingEnabled)); @@ -16314,9 +16303,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public void enforceAuditLoggingPolicy(boolean enabled) { - if (!Flags.securityLogV2Enabled()) { - return; - } Boolean securityLoggingEnabled = mDevicePolicyEngine.getResolvedPolicy( PolicyDefinition.SECURITY_LOGGING, UserHandle.USER_ALL); enforceLoggingPolicy(Boolean.TRUE.equals(securityLoggingEnabled), enabled); @@ -18252,45 +18238,20 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } final CallerIdentity caller = getCallerIdentity(who, packageName); - if (Flags.securityLogV2Enabled()) { - EnforcingAdmin admin = enforcePermissionAndGetEnforcingAdmin( - who, - MANAGE_DEVICE_POLICY_SECURITY_LOGGING, - caller.getPackageName(), - caller.getUserId()); - if (enabled) { - mDevicePolicyEngine.setGlobalPolicy( - PolicyDefinition.SECURITY_LOGGING, - admin, - new BooleanPolicyValue(true)); - } else { - mDevicePolicyEngine.removeGlobalPolicy( - PolicyDefinition.SECURITY_LOGGING, - admin); - } + EnforcingAdmin admin = enforcePermissionAndGetEnforcingAdmin( + who, + MANAGE_DEVICE_POLICY_SECURITY_LOGGING, + caller.getPackageName(), + caller.getUserId()); + if (enabled) { + mDevicePolicyEngine.setGlobalPolicy( + PolicyDefinition.SECURITY_LOGGING, + admin, + new BooleanPolicyValue(true)); } else { - synchronized (getLockObject()) { - if (who != null) { - Preconditions.checkCallAuthorization( - isProfileOwnerOfOrganizationOwnedDevice(caller) - || isDefaultDeviceOwner(caller)); - } else { - // A delegate app passes a null admin component, which is expected - Preconditions.checkCallAuthorization( - isCallerDelegate(caller, DELEGATION_SECURITY_LOGGING)); - } - - if (enabled == mInjector.securityLogGetLoggingEnabledProperty()) { - return; - } - mInjector.securityLogSetLoggingEnabledProperty(enabled); - if (enabled) { - mSecurityLogMonitor.start(getSecurityLoggingEnabledUser()); - maybePauseDeviceWideLoggingLocked(); - } else { - mSecurityLogMonitor.stop(); - } - } + mDevicePolicyEngine.removeGlobalPolicy( + PolicyDefinition.SECURITY_LOGGING, + admin); } DevicePolicyEventLogger .createEvent(DevicePolicyEnums.SET_SECURITY_LOGGING_ENABLED) @@ -18312,29 +18273,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return mInjector.securityLogGetLoggingEnabledProperty(); } - if (Flags.securityLogV2Enabled()) { - final EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( - admin, - MANAGE_DEVICE_POLICY_SECURITY_LOGGING, - caller.getPackageName(), - caller.getUserId()); - final Boolean policy = mDevicePolicyEngine.getGlobalPolicySetByAdmin( - PolicyDefinition.SECURITY_LOGGING, enforcingAdmin); - return Boolean.TRUE.equals(policy); - } else { - synchronized (getLockObject()) { - if (admin != null) { - Preconditions.checkCallAuthorization( - isProfileOwnerOfOrganizationOwnedDevice(caller) - || isDefaultDeviceOwner(caller)); - } else { - // A delegate app passes a null admin component, which is expected - Preconditions.checkCallAuthorization( - isCallerDelegate(caller, DELEGATION_SECURITY_LOGGING)); - } - return mInjector.securityLogGetLoggingEnabledProperty(); - } - } + final EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( + admin, + MANAGE_DEVICE_POLICY_SECURITY_LOGGING, + caller.getPackageName(), + caller.getUserId()); + final Boolean policy = mDevicePolicyEngine.getGlobalPolicySetByAdmin( + PolicyDefinition.SECURITY_LOGGING, enforcingAdmin); + return Boolean.TRUE.equals(policy); } private void recordSecurityLogRetrievalTime() { @@ -18410,42 +18356,24 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final CallerIdentity caller = getCallerIdentity(admin, packageName); - if (Flags.securityLogV2Enabled()) { - EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( - admin, - MANAGE_DEVICE_POLICY_SECURITY_LOGGING, - caller.getPackageName(), - caller.getUserId()); - - synchronized (getLockObject()) { - Preconditions.checkCallAuthorization(isOrganizationOwnedDeviceWithManagedProfile() - || areAllUsersAffiliatedWithDeviceLocked()); - } - - Boolean policy = mDevicePolicyEngine.getGlobalPolicySetByAdmin( - PolicyDefinition.SECURITY_LOGGING, enforcingAdmin); + EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( + admin, + MANAGE_DEVICE_POLICY_SECURITY_LOGGING, + caller.getPackageName(), + caller.getUserId()); - if (!Boolean.TRUE.equals(policy)) { - Slogf.e(LOG_TAG, "%s hasn't enabled security logging but tries to retrieve logs", - caller.getPackageName()); - return null; - } - } else { - if (admin != null) { - Preconditions.checkCallAuthorization( - isProfileOwnerOfOrganizationOwnedDevice(caller) - || isDefaultDeviceOwner(caller)); - } else { - // A delegate app passes a null admin component, which is expected - Preconditions.checkCallAuthorization( - isCallerDelegate(caller, DELEGATION_SECURITY_LOGGING)); - } + synchronized (getLockObject()) { Preconditions.checkCallAuthorization(isOrganizationOwnedDeviceWithManagedProfile() || areAllUsersAffiliatedWithDeviceLocked()); + } - if (!mInjector.securityLogGetLoggingEnabledProperty()) { - return null; - } + Boolean policy = mDevicePolicyEngine.getGlobalPolicySetByAdmin( + PolicyDefinition.SECURITY_LOGGING, enforcingAdmin); + + if (!Boolean.TRUE.equals(policy)) { + Slogf.e(LOG_TAG, "%s hasn't enabled security logging but tries to retrieve logs", + caller.getPackageName()); + return null; } recordSecurityLogRetrievalTime(); @@ -18465,10 +18393,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } final CallerIdentity caller = getCallerIdentity(callingPackage); - if (!Flags.securityLogV2Enabled()) { - throw new UnsupportedOperationException("Audit log not enabled"); - } - EnforcingAdmin admin = enforcePermissionAndGetEnforcingAdmin( null /* admin */, MANAGE_DEVICE_POLICY_AUDIT_LOGGING, @@ -18493,10 +18417,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return false; } - if (!Flags.securityLogV2Enabled()) { - throw new UnsupportedOperationException("Audit log not enabled"); - } - final CallerIdentity caller = getCallerIdentity(callingPackage); EnforcingAdmin admin = enforcePermissionAndGetEnforcingAdmin( null /* admin */, diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java index 2ea5f168bdd1..52a784559510 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java @@ -410,9 +410,8 @@ class OwnersData { out.startTag(null, TAG_POLICY_ENGINE_MIGRATION); out.attributeBoolean(null, ATTR_MIGRATED_TO_POLICY_ENGINE, mMigratedToPolicyEngine); out.attributeBoolean(null, ATTR_MIGRATED_POST_UPGRADE, mPoliciesMigratedPostUpdate); - if (Flags.securityLogV2Enabled()) { - out.attributeBoolean(null, ATTR_SECURITY_LOG_MIGRATED, mSecurityLoggingMigrated); - } + out.attributeBoolean(null, ATTR_SECURITY_LOG_MIGRATED, mSecurityLoggingMigrated); + if (Flags.unmanagedModeMigration()) { out.attributeBoolean(null, ATTR_REQUIRED_PASSWORD_COMPLEXITY_MIGRATED, mRequiredPasswordComplexityMigrated); @@ -483,8 +482,8 @@ class OwnersData { null, ATTR_MIGRATED_TO_POLICY_ENGINE, false); mPoliciesMigratedPostUpdate = parser.getAttributeBoolean( null, ATTR_MIGRATED_POST_UPGRADE, false); - mSecurityLoggingMigrated = Flags.securityLogV2Enabled() - && parser.getAttributeBoolean(null, ATTR_SECURITY_LOG_MIGRATED, false); + mSecurityLoggingMigrated = + parser.getAttributeBoolean(null, ATTR_SECURITY_LOG_MIGRATED, false); mRequiredPasswordComplexityMigrated = Flags.unmanagedModeMigration() && parser.getAttributeBoolean(null, ATTR_REQUIRED_PASSWORD_COMPLEXITY_MIGRATED, false); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/SecurityLogMonitor.java b/services/devicepolicy/java/com/android/server/devicepolicy/SecurityLogMonitor.java index dd0493032c56..474c48a746c9 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/SecurityLogMonitor.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/SecurityLogMonitor.java @@ -23,7 +23,6 @@ import android.app.admin.DeviceAdminReceiver; import android.app.admin.IAuditLogEventsCallback; import android.app.admin.SecurityLog; import android.app.admin.SecurityLog.SecurityEvent; -import android.app.admin.flags.Flags; import android.os.Handler; import android.os.IBinder; import android.os.Process; @@ -184,28 +183,6 @@ class SecurityLogMonitor implements Runnable { @GuardedBy("mLock") private final ArrayDeque<SecurityEvent> mAuditLogEventBuffer = new ArrayDeque<>(); - /** - * Start security logging. - * - * @param enabledUser which user logging is enabled on, or USER_ALL to enable logging for all - * users on the device. - */ - void start(int enabledUser) { - Slog.i(TAG, "Starting security logging for user " + enabledUser); - mEnabledUser = enabledUser; - mLock.lock(); - try { - if (mMonitorThread == null) { - resetLegacyBufferLocked(); - startMonitorThreadLocked(); - } else { - Slog.i(TAG, "Security log monitor thread is already running"); - } - } finally { - mLock.unlock(); - } - } - void stop() { Slog.i(TAG, "Stopping security logging."); mLock.lock(); @@ -467,11 +444,11 @@ class SecurityLogMonitor implements Runnable { assignLogId(event); } - if (!Flags.securityLogV2Enabled() || mLegacyLogEnabled) { + if (mLegacyLogEnabled) { addToLegacyBufferLocked(dedupedLogs); } - if (Flags.securityLogV2Enabled() && mAuditLogEnabled) { + if (mAuditLogEnabled) { addAuditLogEventsLocked(dedupedLogs); } } @@ -548,7 +525,7 @@ class SecurityLogMonitor implements Runnable { saveLastEvents(newLogs); newLogs.clear(); - if (!Flags.securityLogV2Enabled() || mLegacyLogEnabled) { + if (mLegacyLogEnabled) { notifyDeviceOwnerOrProfileOwnerIfNeeded(force); } } catch (IOException e) { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 09c54cb40373..da3ada8671df 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -106,7 +106,7 @@ import com.android.internal.notification.SystemNotificationChannels; import com.android.internal.os.BinderInternal; import com.android.internal.os.RuntimeInit; import com.android.internal.policy.AttributeCache; -import com.android.internal.protolog.ProtoLogService; +import com.android.internal.protolog.ProtoLogConfigurationService; import com.android.internal.util.ConcurrentUtils; import com.android.internal.util.EmergencyAffordanceManager; import com.android.internal.util.FrameworkStatsLog; @@ -256,6 +256,7 @@ import com.android.server.stats.bootstrap.StatsBootstrapAtomService; import com.android.server.stats.pull.StatsPullAtomService; import com.android.server.statusbar.StatusBarManagerService; import com.android.server.storage.DeviceStorageMonitorService; +import com.android.server.supervision.SupervisionService; import com.android.server.systemcaptions.SystemCaptionsManagerService; import com.android.server.telecom.TelecomLoaderService; import com.android.server.testharness.TestHarnessModeService; @@ -1092,8 +1093,9 @@ public final class SystemServer implements Dumpable { // Orchestrates some ProtoLogging functionality. if (android.tracing.Flags.clientSideProtoLogging()) { - t.traceBegin("StartProtoLogService"); - ServiceManager.addService(Context.PROTOLOG_SERVICE, new ProtoLogService()); + t.traceBegin("StartProtoLogConfigurationService"); + ServiceManager.addService( + Context.PROTOLOG_CONFIGURATION_SERVICE, new ProtoLogConfigurationService()); t.traceEnd(); } @@ -1597,6 +1599,12 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(ROLE_SERVICE_CLASS); t.traceEnd(); + if (android.app.supervision.flags.Flags.supervisionApi()) { + t.traceBegin("StartSupervisionService"); + mSystemServiceManager.startService(SupervisionService.Lifecycle.class); + t.traceEnd(); + } + if (!isTv) { t.traceBegin("StartVibratorManagerService"); mSystemServiceManager.startService(VibratorManagerService.Lifecycle.class); diff --git a/ravenwood/coretest/Android.bp b/services/supervision/Android.bp index a78c5c1e8227..93a0c4af7891 100644 --- a/ravenwood/coretest/Android.bp +++ b/services/supervision/Android.bp @@ -7,17 +7,16 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -android_ravenwood_test { - name: "RavenwoodCoreTest", +filegroup { + name: "services.supervision-sources", + srcs: ["java/**/*.java"], + path: "java", + visibility: ["//frameworks/base/services"], +} - static_libs: [ - "androidx.annotation_annotation", - "androidx.test.ext.junit", - "androidx.test.rules", - ], - srcs: [ - "test/**/*.java", - ], - sdk_version: "test_current", - auto_gen_config: true, +java_library_static { + name: "services.supervision", + defaults: ["platform_service_defaults"], + srcs: [":services.supervision-sources"], + libs: ["services.core"], } diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java new file mode 100644 index 000000000000..a4ef629492e7 --- /dev/null +++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.supervision; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.supervision.ISupervisionManager; +import android.content.Context; + + +import com.android.internal.util.DumpUtils; +import com.android.server.SystemService; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** Service for handling system supervision. */ +public class SupervisionService extends ISupervisionManager.Stub { + private static final String LOG_TAG = "SupervisionService"; + + private final Context mContext; + + public SupervisionService(Context context) { + mContext = context.createAttributionContext("SupervisionService"); + } + + @Override + public boolean isSupervisionEnabled() { + return false; + } + + @Override + protected void dump(@NonNull FileDescriptor fd, + @NonNull PrintWriter fout, @Nullable String[] args) { + if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, fout)) return; + + fout.println("Supervision enabled: " + isSupervisionEnabled()); + } + + public static class Lifecycle extends SystemService { + private final SupervisionService mSupervisionService; + + public Lifecycle(@NonNull Context context) { + super(context); + mSupervisionService = new SupervisionService(context); + } + + @Override + public void onStart() { + publishBinderService(Context.SUPERVISION_SERVICE, mSupervisionService); + } + } +} diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java index 0787058e3c11..2c785049412a 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java @@ -33,11 +33,15 @@ import android.content.Context; import android.content.res.Configuration; import android.graphics.Insets; import android.os.RemoteException; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; import android.util.Log; import android.view.WindowManagerGlobal; import android.view.WindowManagerPolicyConstants; import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.Flags; import android.view.inputmethod.InputMethodManager; import androidx.annotation.NonNull; @@ -56,6 +60,7 @@ import com.android.internal.inputmethod.InputMethodNavButtonFlags; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -89,6 +94,9 @@ public class InputMethodServiceTest { private String mInputMethodId; private boolean mShowImeWithHardKeyboardEnabled; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setUp() throws Exception { mInstrumentation = InstrumentationRegistry.getInstrumentation(); @@ -155,7 +163,13 @@ public class InputMethodServiceTest { () -> assertThat(mUiDevice.pressHome()).isTrue(), true /* expected */, false /* inputViewStarted */); - assertThat(mInputMethodService.isInputViewShown()).isFalse(); + if (Flags.refactorInsetsController()) { + // The IME visibility is only sent at the end of the animation. Therefore, we have to + // wait until the visibility was sent to the server and the IME window hidden. + eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse()); + } else { + assertThat(mInputMethodService.isInputViewShown()).isFalse(); + } } /** @@ -182,8 +196,13 @@ public class InputMethodServiceTest { /** * This checks the result of calling IMS#requestShowSelf and IMS#requestHideSelf. + * + * With the refactor in b/298172246, all calls to IMMS#{show,hide}MySoftInputLocked + * will be just apply the requested visibility (by using the callback). Therefore, we will + * lose flags like HIDE_IMPLICIT_ONLY. */ @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowHideSelf() throws Exception { setShowImeWithHardKeyboard(true /* enabled */); @@ -375,8 +394,13 @@ public class InputMethodServiceTest { /** * This checks that an implicit show request when the IME is not previously shown, * and it should be shown in fullscreen mode, results in the IME not being shown. + * + * With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput + * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like + * SHOW_IMPLICIT. */ @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowSoftInputImplicitly_fullScreenMode() throws Exception { setShowImeWithHardKeyboard(true /* enabled */); @@ -425,8 +449,13 @@ public class InputMethodServiceTest { /** * This checks that an implicit show request when a hard keyboard is connected, * results in the IME not being shown. + * + * With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput + * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like + * SHOW_IMPLICIT. */ @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowSoftInputImplicitly_withHardKeyboard() throws Exception { setShowImeWithHardKeyboard(false /* enabled */); @@ -484,8 +513,13 @@ public class InputMethodServiceTest { * This checks that an implicit show request followed by connecting a hard keyboard * and a configuration change, does not trigger IMS#onFinishInputView, * but results in the IME being hidden. + * + * With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput + * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like + * SHOW_IMPLICIT. */ @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowSoftInputImplicitly_thenConfigurationChanged() throws Exception { setShowImeWithHardKeyboard(false /* enabled */); @@ -567,8 +601,13 @@ public class InputMethodServiceTest { * This checks that a forced show request directly followed by an explicit show request, * and then a hide not always request, still results in the IME being shown * (i.e. the explicit show request retains the forced state). + * + * With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput + * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like + * HIDE_NOT_ALWAYS. */ @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowSoftInputForced_testShowSoftInputExplicitly_thenHideSoftInputNotAlways() throws Exception { setShowImeWithHardKeyboard(true /* enabled */); @@ -734,7 +773,13 @@ public class InputMethodServiceTest { backButtonUiObject.click(); mInstrumentation.waitForIdleSync(); - assertThat(mInputMethodService.isInputViewShown()).isFalse(); + if (Flags.refactorInsetsController()) { + // The IME visibility is only sent at the end of the animation. Therefore, we have to + // wait until the visibility was sent to the server and the IME window hidden. + eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse()); + } else { + assertThat(mInputMethodService.isInputViewShown()).isFalse(); + } } /** @@ -766,7 +811,13 @@ public class InputMethodServiceTest { backButtonUiObject.longClick(); mInstrumentation.waitForIdleSync(); - assertThat(mInputMethodService.isInputViewShown()).isFalse(); + if (Flags.refactorInsetsController()) { + // The IME visibility is only sent at the end of the animation. Therefore, we have to + // wait until the visibility was sent to the server and the IME window hidden. + eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse()); + } else { + assertThat(mInputMethodService.isInputViewShown()).isFalse(); + } } /** @@ -848,7 +899,13 @@ public class InputMethodServiceTest { assertWithMessage("Input Method Switcher Menu is shown") .that(isInputMethodPickerShown(imm)) .isTrue(); - assertThat(mInputMethodService.isInputViewShown()).isTrue(); + if (Flags.refactorInsetsController()) { + // The IME visibility is only sent at the end of the animation. Therefore, we have to + // wait until the visibility was sent to the server and the IME window hidden. + eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse()); + } else { + assertThat(mInputMethodService.isInputViewShown()).isTrue(); + } // Hide the Picker menu before finishing. mUiDevice.pressBack(); diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java index ab0f0c1fe5ff..d91f154c1b87 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java @@ -3556,12 +3556,16 @@ public class DisplayModeDirectorTest { new RefreshRateRange(refreshRate, refreshRate); displayListener.onDisplayChanged(DISPLAY_ID); - Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_LAYOUT_LIMITED_FRAME_RATE); + Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_LAYOUT_LIMITED_REFRESH_RATE); assertVoteForPhysicalRefreshRate(vote, /* refreshRate= */ refreshRate); + vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_LAYOUT_LIMITED_FRAME_RATE); + assertVoteForRenderFrameRateRange(vote, refreshRate, refreshRate); mInjector.mDisplayInfo.layoutLimitedRefreshRate = null; displayListener.onDisplayChanged(DISPLAY_ID); + vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_LAYOUT_LIMITED_REFRESH_RATE); + assertNull(vote); vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_LAYOUT_LIMITED_FRAME_RATE); assertNull(vote); } @@ -3585,6 +3589,8 @@ public class DisplayModeDirectorTest { Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_LAYOUT_LIMITED_FRAME_RATE); assertNull(vote); + vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_LAYOUT_LIMITED_REFRESH_RATE); + assertNull(vote); } private Temperature getSkinTemp(@Temperature.ThrottlingStatus int status) { 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 ee96c2abbf46..3dd2f24aa4e4 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java @@ -22,6 +22,7 @@ import static com.android.server.am.ActivityManagerService.Injector; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -42,6 +43,8 @@ import android.os.Process; import android.platform.test.annotations.Presubmit; import android.text.TextUtils; +import com.android.internal.os.Clock; +import com.android.internal.os.MonotonicClock; import com.android.server.LocalServices; import com.android.server.ServiceThread; import com.android.server.appop.AppOpsService; @@ -121,11 +124,18 @@ public class ApplicationStartInfoTest { LocalServices.removeServiceForTest(PackageManagerInternal.class); LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt); + mAppStartInfoTracker.mMonotonicClock = new MonotonicClock( + Clock.SYSTEM_CLOCK.elapsedRealtime(), Clock.SYSTEM_CLOCK); mAppStartInfoTracker.clearProcessStartInfo(true); mAppStartInfoTracker.mAppStartInfoLoaded.set(true); mAppStartInfoTracker.mAppStartInfoHistoryListSize = mAppStartInfoTracker.APP_START_INFO_HISTORY_LIST_SIZE; doNothing().when(mAppStartInfoTracker).schedulePersistProcessStartInfo(anyBoolean()); + + mAppStartInfoTracker.mProcStartStoreDir = new File(mContext.getFilesDir(), + AppStartInfoTracker.APP_START_STORE_DIR); + mAppStartInfoTracker.mProcStartInfoFile = new File(mAppStartInfoTracker.mProcStartStoreDir, + AppStartInfoTracker.APP_START_INFO_FILE); } @After @@ -135,11 +145,8 @@ public class ApplicationStartInfoTest { @Test public void testApplicationStartInfo() throws Exception { - mAppStartInfoTracker.mProcStartStoreDir = new File(mContext.getFilesDir(), - AppStartInfoTracker.APP_START_STORE_DIR); + // Make sure we can write to the file. assertTrue(FileUtils.createDir(mAppStartInfoTracker.mProcStartStoreDir)); - mAppStartInfoTracker.mProcStartInfoFile = new File(mAppStartInfoTracker.mProcStartStoreDir, - AppStartInfoTracker.APP_START_INFO_FILE); final long appStartTimestampIntentStarted = 1000000; final long appStartTimestampActivityLaunchFinished = 2000000; @@ -482,6 +489,79 @@ public class ApplicationStartInfoTest { verifyInProgressRecordsSize(AppStartInfoTracker.MAX_IN_PROGRESS_RECORDS); } + /** + * Test to make sure that records are returned in correct order, from most recently added at + * index 0 to least recently added at index size - 1. + */ + @Test + public void testHistoricalRecordsOrdering() throws Exception { + // Clear old records + mAppStartInfoTracker.clearProcessStartInfo(false); + + // Add some records with timestamps 0 decreasing as clock increases. + ProcessRecord app = makeProcessRecord( + APP_1_PID_1, // pid + APP_1_UID, // uid + APP_1_UID, // packageUid + null, // definingUid + APP_1_PROCESS_NAME, // processName + APP_1_PACKAGE_NAME); // packageName + + mAppStartInfoTracker.handleProcessBroadcastStart(3, app, buildIntent(COMPONENT), + false /* isAlarm */); + mAppStartInfoTracker.handleProcessBroadcastStart(2, app, buildIntent(COMPONENT), + false /* isAlarm */); + mAppStartInfoTracker.handleProcessBroadcastStart(1, app, buildIntent(COMPONENT), + false /* isAlarm */); + + // Get records + ArrayList<ApplicationStartInfo> list = new ArrayList<ApplicationStartInfo>(); + mAppStartInfoTracker.getStartInfo(null, APP_1_UID, 0, 0, list); + + // Confirm that records are in correct order, with index 0 representing the most recently + // added record and index size - 1 representing the least recently added one. + assertEquals(3, list.size()); + assertEquals(1L, list.get(0).getStartupTimestamps().get(0).longValue()); + assertEquals(2L, list.get(1).getStartupTimestamps().get(0).longValue()); + assertEquals(3L, list.get(2).getStartupTimestamps().get(0).longValue()); + } + + /** + * Test to make sure that persist and restore correctly maintains the state of the monotonic + * clock. + */ + @Test + public void testPersistAndRestoreMonotonicClock() { + // Make sure we can write to the file. + assertTrue(FileUtils.createDir(mAppStartInfoTracker.mProcStartStoreDir)); + + // No need to persist records for this test, clear any that may be there. + mAppStartInfoTracker.clearProcessStartInfo(false); + + // Set clock with an arbitrary 5 minute offset, just needs to be longer than it would take + // for code to run. + mAppStartInfoTracker.mMonotonicClock = new MonotonicClock(5 * 60 * 1000, + Clock.SYSTEM_CLOCK); + + // Record the current time. + long originalMonotonicTime = mAppStartInfoTracker.mMonotonicClock.monotonicTime(); + + // Now persist the process start info. Records were cleared above so this should just + // persist the monotonic time. + mAppStartInfoTracker.persistProcessStartInfo(); + + // Null out the clock to make sure its set on load. + mAppStartInfoTracker.mMonotonicClock = null; + assertNull(mAppStartInfoTracker.mMonotonicClock); + + // Now load from disk. + mAppStartInfoTracker.loadExistingProcessStartInfo(); + + // Confirm clock has been set and that its current time is greater than the previous one. + assertNotNull(mAppStartInfoTracker.mMonotonicClock); + assertTrue(mAppStartInfoTracker.mMonotonicClock.monotonicTime() > originalMonotonicTime); + } + private static <T> void setFieldValue(Class clazz, Object obj, String fieldName, T val) { try { Field field = clazz.getDeclaredField(fieldName); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index 0ba74c62b9fe..100b54897573 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -1176,6 +1176,17 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { verifyPendingRecords(greenQueue, List.of(screenOff, screenOn)); verifyPendingRecords(redQueue, List.of(screenOff)); verifyPendingRecords(blueQueue, List.of(screenOff, screenOn)); + + final BroadcastRecord screenOffRecord = makeBroadcastRecord(screenOff, screenOnOffOptions, + List.of(greenReceiver, redReceiver, blueReceiver), false); + screenOffRecord.setDeliveryState(2, BroadcastRecord.DELIVERY_DEFERRED, + "testDeliveryGroupPolicy_prioritized_diffReceivers"); + mImpl.enqueueBroadcastLocked(screenOffRecord); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions, + List.of(greenReceiver, blueReceiver), false)); + verifyPendingRecords(greenQueue, List.of(screenOff, screenOn)); + verifyPendingRecords(redQueue, List.of(screenOff)); + verifyPendingRecords(blueQueue, List.of(screenOn)); } /** diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java index 0703db2ab648..18811de14c73 100644 --- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java @@ -223,6 +223,11 @@ public class GameManagerServiceTests { mShutDownActionReceiver = receiver; return null; } + + @Override + public int getUserId() { + return 0; + } } @Before @@ -237,7 +242,7 @@ public class GameManagerServiceTests { mPackageCategories = new HashMap<>(); mPackageUids = new HashMap<>(); mPackageName = mMockContext.getPackageName(); - mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_GAME); + mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_GAME, -1); LocalServices.addService(PowerManagerInternal.class, mMockPowerManager); mSetFlagsRule.enableFlags(Flags.FLAG_GAME_DEFAULT_FRAME_RATE); @@ -245,7 +250,12 @@ public class GameManagerServiceTests { } private void mockAppCategory(String packageName, int packageUid, - @ApplicationInfo.Category int category) + @ApplicationInfo.Category int category) throws Exception { + mockAppCategory(packageName, packageUid, category, -1 /*userId*/); + } + + private void mockAppCategory(String packageName, int packageUid, + @ApplicationInfo.Category int category, int userId) throws Exception { reset(mMockPackageManager); mPackageCategories.put(packageName, category); @@ -259,8 +269,15 @@ public class GameManagerServiceTests { ApplicationInfo applicationInfo = new ApplicationInfo(); applicationInfo.packageName = packageName; applicationInfo.category = category; - when(mMockPackageManager.getApplicationInfoAsUser(eq(packageName), anyInt(), anyInt())) - .thenReturn(applicationInfo); + if (userId == -1) { + when(mMockPackageManager.getApplicationInfoAsUser(eq(packageName), anyInt(), + anyInt())) + .thenReturn(applicationInfo); + } else { + when(mMockPackageManager.getApplicationInfoAsUser(eq(packageName), anyInt(), + eq(userId))) + .thenReturn(applicationInfo); + } final PackageInfo pi = new PackageInfo(); pi.packageName = packageName; @@ -2331,10 +2348,12 @@ public class GameManagerServiceTests { @Test public void testGamePowerMode_twoGames() throws Exception { - GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1); + GameManagerService gameManagerService = new GameManagerService(mMockContext, + mTestLooper.getLooper()); String someGamePkg = "some.game"; int somePackageId = DEFAULT_PACKAGE_UID + 1; - mockAppCategory(someGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME); + mockAppCategory(someGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME, + ActivityManager.getCurrentUser()); HashMap<Integer, Boolean> powerState = new HashMap<>(); doAnswer(inv -> powerState.put(inv.getArgument(0), inv.getArgument(1))) .when(mMockPowerManager).setPowerMode(anyInt(), anyBoolean()); @@ -2354,10 +2373,12 @@ public class GameManagerServiceTests { @Test public void testGamePowerMode_twoGamesOverlap() throws Exception { - GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1); + GameManagerService gameManagerService = new GameManagerService(mMockContext, + mTestLooper.getLooper()); String someGamePkg = "some.game"; int somePackageId = DEFAULT_PACKAGE_UID + 1; - mockAppCategory(someGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME); + mockAppCategory(someGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME, + ActivityManager.getCurrentUser()); gameManagerService.mUidObserver.onUidStateChanged( DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0); gameManagerService.mUidObserver.onUidStateChanged( @@ -2372,7 +2393,8 @@ public class GameManagerServiceTests { @Test public void testGamePowerMode_noPackage() throws Exception { - GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1); + GameManagerService gameManagerService = new GameManagerService(mMockContext, + mTestLooper.getLooper()); String[] packages = {}; when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages); gameManagerService.mUidObserver.onUidStateChanged( @@ -2383,23 +2405,24 @@ public class GameManagerServiceTests { @Test public void testGamePowerMode_gameAndNotGameApps_flagOn() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_DISABLE_GAME_MODE_WHEN_APP_TOP); - GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1); - + GameManagerService gameManagerService = new GameManagerService(mMockContext, + mTestLooper.getLooper()); + int userId = ActivityManager.getCurrentUser(); String nonGamePkg1 = "not.game1"; int nonGameUid1 = DEFAULT_PACKAGE_UID + 1; - mockAppCategory(nonGamePkg1, nonGameUid1, ApplicationInfo.CATEGORY_IMAGE); + mockAppCategory(nonGamePkg1, nonGameUid1, ApplicationInfo.CATEGORY_IMAGE, userId); String nonGamePkg2 = "not.game2"; int nonGameUid2 = DEFAULT_PACKAGE_UID + 2; - mockAppCategory(nonGamePkg2, nonGameUid2, ApplicationInfo.CATEGORY_IMAGE); + mockAppCategory(nonGamePkg2, nonGameUid2, ApplicationInfo.CATEGORY_IMAGE, userId); String gamePkg1 = "game1"; int gameUid1 = DEFAULT_PACKAGE_UID + 3; - mockAppCategory(gamePkg1, gameUid1, ApplicationInfo.CATEGORY_GAME); + mockAppCategory(gamePkg1, gameUid1, ApplicationInfo.CATEGORY_GAME, userId); String gamePkg2 = "game2"; int gameUid2 = DEFAULT_PACKAGE_UID + 4; - mockAppCategory(gamePkg2, gameUid2, ApplicationInfo.CATEGORY_GAME); + mockAppCategory(gamePkg2, gameUid2, ApplicationInfo.CATEGORY_GAME, userId); // non-game1 top and background with no-op gameManagerService.mUidObserver.onUidStateChanged( @@ -2470,15 +2493,17 @@ public class GameManagerServiceTests { @Test public void testGamePowerMode_gameAndNotGameApps_flagOff() throws Exception { mSetFlagsRule.disableFlags(Flags.FLAG_DISABLE_GAME_MODE_WHEN_APP_TOP); - GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1); + int userId = ActivityManager.getCurrentUser(); + GameManagerService gameManagerService = new GameManagerService(mMockContext, + mTestLooper.getLooper()); String nonGamePkg1 = "not.game1"; int nonGameUid1 = DEFAULT_PACKAGE_UID + 1; - mockAppCategory(nonGamePkg1, nonGameUid1, ApplicationInfo.CATEGORY_IMAGE); + mockAppCategory(nonGamePkg1, nonGameUid1, ApplicationInfo.CATEGORY_IMAGE, userId); String gamePkg1 = "game1"; int gameUid1 = DEFAULT_PACKAGE_UID + 3; - mockAppCategory(gamePkg1, gameUid1, ApplicationInfo.CATEGORY_GAME); + mockAppCategory(gamePkg1, gameUid1, ApplicationInfo.CATEGORY_GAME, userId); // non-game1 top and background with no-op gameManagerService.mUidObserver.onUidStateChanged( diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java index bbab0eef49cb..7b635d4198c3 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java @@ -38,6 +38,7 @@ import android.hardware.power.stats.PowerEntity; import android.hardware.power.stats.StateResidencyResult; import android.os.Handler; import android.os.Looper; +import android.os.connectivity.WifiActivityEnergyInfo; import android.platform.test.ravenwood.RavenwoodRule; import android.power.PowerStatsInternal; import android.util.IntArray; @@ -88,6 +89,33 @@ public class BatteryExternalStatsWorkerTest { } @Test + public void testUpdateWifiState() { + WifiActivityEnergyInfo firstInfo = new WifiActivityEnergyInfo(1111, + WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 11, 22, 33, 44); + + WifiActivityEnergyInfo delta = mBatteryExternalStatsWorker.extractDeltaLocked(firstInfo); + + assertEquals(1111, delta.getTimeSinceBootMillis()); + assertEquals(WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, delta.getStackState()); + assertEquals(0, delta.getControllerTxDurationMillis()); + assertEquals(0, delta.getControllerRxDurationMillis()); + assertEquals(0, delta.getControllerScanDurationMillis()); + assertEquals(0, delta.getControllerIdleDurationMillis()); + + WifiActivityEnergyInfo secondInfo = new WifiActivityEnergyInfo(91111, + WifiActivityEnergyInfo.STACK_STATE_STATE_IDLE, 811, 722, 633, 544); + + delta = mBatteryExternalStatsWorker.extractDeltaLocked(secondInfo); + + assertEquals(91111, delta.getTimeSinceBootMillis()); + assertEquals(WifiActivityEnergyInfo.STACK_STATE_STATE_IDLE, delta.getStackState()); + assertEquals(800, delta.getControllerTxDurationMillis()); + assertEquals(700, delta.getControllerRxDurationMillis()); + assertEquals(600, delta.getControllerScanDurationMillis()); + assertEquals(500, delta.getControllerIdleDurationMillis()); + } + + @Test public void testTargetedEnergyConsumerQuerying() { final int numCpuClusters = 4; final int numDisplays = 5; diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/GnssPowerStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/GnssPowerStatsTest.java index 127ab8a6e549..f22279a88a50 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/GnssPowerStatsTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/GnssPowerStatsTest.java @@ -74,6 +74,7 @@ public class GnssPowerStatsTest { private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 101; private static final int VOLTAGE_MV = 3500; private static final int ENERGY_CONSUMER_ID = 777; + private static final long START_TIME = 10_000_000_000L; private final PowerStatsUidResolver mUidResolver = new PowerStatsUidResolver(); @Mock @@ -113,11 +114,13 @@ public class GnssPowerStatsTest { }; private MonotonicClock mMonotonicClock; + private final BatteryStats.HistoryItem mHistoryItem = new BatteryStats.HistoryItem(); @Before public void setup() { MockitoAnnotations.initMocks(this); - mMonotonicClock = new MonotonicClock(0, mStatsRule.getMockClock()); + mMonotonicClock = new MonotonicClock(START_TIME, mStatsRule.getMockClock()); + mHistoryItem.clear(); } @Test @@ -129,7 +132,6 @@ public class GnssPowerStatsTest { PowerComponentAggregatedPowerStats stats = createAggregatedPowerStats( () -> new GnssPowerStatsProcessor(mStatsRule.getPowerProfile(), mUidResolver)); - stats.start(0); GnssPowerStatsCollector collector = new GnssPowerStatsCollector(mInjector); collector.addConsumer( @@ -142,9 +144,11 @@ public class GnssPowerStatsTest { stats.noteStateChange(buildHistoryItem(0, true, APP_UID1)); // Turn the screen off after 2.5 seconds - stats.setState(STATE_SCREEN, SCREEN_STATE_OTHER, 2500); - stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_BACKGROUND, 2500); - stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND_SERVICE, 5000); + stats.setState(STATE_SCREEN, SCREEN_STATE_OTHER, START_TIME + 2500); + stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_BACKGROUND, + START_TIME + 2500); + stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND_SERVICE, + START_TIME + 5000); stats.noteStateChange(buildHistoryItem(6000, false, APP_UID1)); @@ -158,7 +162,87 @@ public class GnssPowerStatsTest { mStatsRule.setTime(11_000, 11_000); collector.collectAndDeliverStats(); - stats.finish(11_000); + stats.finish(START_TIME + 11_000); + + PowerStats.Descriptor descriptor = stats.getPowerStatsDescriptor(); + BinaryStatePowerStatsLayout statsLayout = new BinaryStatePowerStatsLayout(); + statsLayout.fromExtras(descriptor.extras); + + // scr-on, GNSS-good: 2500 * 100 = 250000 mA-ms = 0.06944 mAh + // scr-off GNSS=good: 4500 * 100 = 0.12500 mAh + // scr-off GNSS=poor: 3000 * 1000 = 0.83333 mAh + // scr-off GNSS-on: 0.12500 + 0.83333 = 0.95833 mAh + long[] deviceStats = new long[descriptor.statsArrayLength]; + stats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_ON)); + assertThat(statsLayout.getDevicePowerEstimate(deviceStats)) + .isWithin(PRECISION).of(0.06944); + + stats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_OTHER)); + assertThat(statsLayout.getDevicePowerEstimate(deviceStats)) + .isWithin(PRECISION).of(0.12500 + 0.83333); + + // UID1 = + // scr-on FG: 2500 -> 0.06944 mAh + // scr-off BG: 2500/7500 * 0.95833 = 0.31944 mAh + // scr-off FGS: 1000/7500 * 0.95833 = 0.12777 mAh + long[] uidStats = new long[descriptor.uidStatsArrayLength]; + stats.getUidStats(uidStats, APP_UID1, + states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_FOREGROUND)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(0.06944); + + stats.getUidStats(uidStats, APP_UID1, + states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_BACKGROUND)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(0.31944); + + stats.getUidStats(uidStats, APP_UID1, + states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_FOREGROUND_SERVICE)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(0.12777); + + // UID2 = + // scr-off cached: 4000/7500 * 0.95833 = 0.51111 mAh + stats.getUidStats(uidStats, APP_UID2, + states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_CACHED)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(0.51111); + + stats.getUidStats(uidStats, APP_UID2, + states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(0); + } + + @Test + public void initialStateGnssOn() { + // ODPM unsupported + when(mConsumedEnergyRetriever + .getEnergyConsumerIds(eq((int) EnergyConsumerType.GNSS), any())) + .thenReturn(new int[0]); + + PowerComponentAggregatedPowerStats stats = createAggregatedPowerStats( + () -> new GnssPowerStatsProcessor(mStatsRule.getPowerProfile(), mUidResolver)); + + stats.noteStateChange(buildHistoryItemInitialStateGpsOn(0)); + + // Turn the screen off after 2.5 seconds + stats.setState(STATE_SCREEN, SCREEN_STATE_OTHER, START_TIME + 2500); + stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_BACKGROUND, + START_TIME + 2500); + stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND_SERVICE, + START_TIME + 5000); + + stats.noteStateChange(buildHistoryItem(6000, false, APP_UID1)); + + stats.noteStateChange(buildHistoryItem(7000, true, APP_UID2)); + stats.noteStateChange(buildHistoryItem(7000, + GnssSignalQuality.GNSS_SIGNAL_QUALITY_GOOD)); + stats.noteStateChange(buildHistoryItem(8000, + GnssSignalQuality.GNSS_SIGNAL_QUALITY_POOR)); + mStatsRule.setTime(11_000, 11_000); + + stats.finish(START_TIME + 11_000); PowerStats.Descriptor descriptor = stats.getPowerStatsDescriptor(); BinaryStatePowerStatsLayout statsLayout = new BinaryStatePowerStatsLayout(); @@ -224,8 +308,6 @@ public class GnssPowerStatsTest { powerStats -> stats.addPowerStats(powerStats, mMonotonicClock.monotonicTime())); collector.setEnabled(true); - stats.start(0); - // Establish a baseline when(mConsumedEnergyRetriever.getConsumedEnergy(new int[]{ENERGY_CONSUMER_ID})) .thenReturn(createEnergyConsumerResults(ENERGY_CONSUMER_ID, 10000)); @@ -234,9 +316,11 @@ public class GnssPowerStatsTest { stats.noteStateChange(buildHistoryItem(0, true, APP_UID1)); // Turn the screen off after 2.5 seconds - stats.setState(STATE_SCREEN, SCREEN_STATE_OTHER, 2500); - stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_BACKGROUND, 2500); - stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND_SERVICE, 5000); + stats.setState(STATE_SCREEN, SCREEN_STATE_OTHER, START_TIME + 2500); + stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_BACKGROUND, + START_TIME + 2500); + stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND_SERVICE, + START_TIME + 5000); stats.noteStateChange(buildHistoryItem(6000, false, APP_UID1)); @@ -245,16 +329,14 @@ public class GnssPowerStatsTest { collector.collectAndDeliverStats(); stats.noteStateChange(buildHistoryItem(7000, true, APP_UID2)); - stats.noteStateChange(buildHistoryItem(7000, - GnssSignalQuality.GNSS_SIGNAL_QUALITY_GOOD)); - stats.noteStateChange(buildHistoryItem(8000, - GnssSignalQuality.GNSS_SIGNAL_QUALITY_POOR)); + stats.noteStateChange(buildHistoryItem(7000, GnssSignalQuality.GNSS_SIGNAL_QUALITY_GOOD)); + stats.noteStateChange(buildHistoryItem(8000, GnssSignalQuality.GNSS_SIGNAL_QUALITY_POOR)); mStatsRule.setTime(11_000, 11_000); when(mConsumedEnergyRetriever.getConsumedEnergy(new int[]{ENERGY_CONSUMER_ID})) .thenReturn(createEnergyConsumerResults(ENERGY_CONSUMER_ID, 3_610_000)); collector.collectAndDeliverStats(); - stats.finish(11_000); + stats.finish(START_TIME + 11_000); PowerStats.Descriptor descriptor = stats.getPowerStatsDescriptor(); BinaryStatePowerStatsLayout statsLayout = new BinaryStatePowerStatsLayout(); @@ -313,33 +395,45 @@ public class GnssPowerStatsTest { .isWithin(PRECISION).of(0); } - private BatteryStats.HistoryItem buildHistoryItem(int timestamp, boolean stateOn, + private BatteryStats.HistoryItem buildHistoryItemInitialStateGpsOn(long timestamp) { + mStatsRule.setTime(timestamp, timestamp); + mHistoryItem.time = mMonotonicClock.monotonicTime(); + mHistoryItem.states = BatteryStats.HistoryItem.STATE_GPS_ON_FLAG; + setGnssSignalLevel(BatteryStats.HistoryItem.GNSS_SIGNAL_QUALITY_NONE); + return mHistoryItem; + } + + private BatteryStats.HistoryItem buildHistoryItem(long timestamp, boolean stateOn, int uid) { mStatsRule.setTime(timestamp, timestamp); - BatteryStats.HistoryItem historyItem = new BatteryStats.HistoryItem(); - historyItem.time = mMonotonicClock.monotonicTime(); - historyItem.states = stateOn ? BatteryStats.HistoryItem.STATE_GPS_ON_FLAG : 0; + mHistoryItem.time = mMonotonicClock.monotonicTime(); + mHistoryItem.states = stateOn ? BatteryStats.HistoryItem.STATE_GPS_ON_FLAG : 0; if (stateOn) { - historyItem.eventCode = BatteryStats.HistoryItem.EVENT_STATE_CHANGE + mHistoryItem.eventCode = BatteryStats.HistoryItem.EVENT_STATE_CHANGE | BatteryStats.HistoryItem.EVENT_FLAG_START; } else { - historyItem.eventCode = BatteryStats.HistoryItem.EVENT_STATE_CHANGE + mHistoryItem.eventCode = BatteryStats.HistoryItem.EVENT_STATE_CHANGE | BatteryStats.HistoryItem.EVENT_FLAG_FINISH; } - historyItem.eventTag = historyItem.localEventTag; - historyItem.eventTag.uid = uid; - historyItem.eventTag.string = "gnss"; - return historyItem; + mHistoryItem.eventTag = mHistoryItem.localEventTag; + mHistoryItem.eventTag.uid = uid; + mHistoryItem.eventTag.string = "gnss"; + return mHistoryItem; } - private BatteryStats.HistoryItem buildHistoryItem(int timestamp, int signalLevel) { + private BatteryStats.HistoryItem buildHistoryItem(long timestamp, int signalLevel) { mStatsRule.setTime(timestamp, timestamp); - BatteryStats.HistoryItem historyItem = new BatteryStats.HistoryItem(); - historyItem.time = mMonotonicClock.monotonicTime(); - historyItem.states = BatteryStats.HistoryItem.STATE_GPS_ON_FLAG; - historyItem.states2 = - signalLevel << BatteryStats.HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT; - return historyItem; + mHistoryItem.time = mMonotonicClock.monotonicTime(); + setGnssSignalLevel(signalLevel); + mHistoryItem.eventCode = BatteryStats.HistoryItem.EVENT_NONE; + mHistoryItem.eventTag = null; + return mHistoryItem; + } + + private void setGnssSignalLevel(int signalLevel) { + mHistoryItem.states2 = + (mHistoryItem.states2 & ~BatteryStats.HistoryItem.STATE2_GPS_SIGNAL_QUALITY_MASK) + | signalLevel << BatteryStats.HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT; } private int[] states(int... states) { @@ -362,12 +456,14 @@ public class GnssPowerStatsTest { AggregatedPowerStats aggregatedPowerStats = new AggregatedPowerStats(config); PowerComponentAggregatedPowerStats powerComponentStats = aggregatedPowerStats.getPowerComponentStats(BatteryConsumer.POWER_COMPONENT_GNSS); - powerComponentStats.start(0); - - powerComponentStats.setState(STATE_POWER, POWER_STATE_OTHER, 0); - powerComponentStats.setState(STATE_SCREEN, SCREEN_STATE_ON, 0); - powerComponentStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND, 0); - powerComponentStats.setUidState(APP_UID2, STATE_PROCESS_STATE, PROCESS_STATE_CACHED, 0); + powerComponentStats.start(START_TIME); + + powerComponentStats.setState(STATE_POWER, POWER_STATE_OTHER, START_TIME); + powerComponentStats.setState(STATE_SCREEN, SCREEN_STATE_ON, START_TIME); + powerComponentStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND, + START_TIME); + powerComponentStats.setUidState(APP_UID2, STATE_PROCESS_STATE, PROCESS_STATE_CACHED, + START_TIME); return powerComponentStats; } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java index 9083a1e28e2c..19041451c8eb 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java @@ -1319,7 +1319,6 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { final AccessibilityWindow window = Mockito.mock(AccessibilityWindow.class); when(window.getWindowInfo()).thenReturn(windowInfo); - when(window.ignoreRecentsAnimationForAccessibility()).thenReturn(false); when(window.isFocused()).thenAnswer(invocation -> windowInfo.focused); when(window.isTouchable()).thenReturn(true); when(window.getType()).thenReturn(windowInfo.type); diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java index e45ab319146c..beed0a3d413c 100644 --- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java @@ -111,6 +111,9 @@ public class VolumeHelperTest { private static final AudioDeviceAttributes DEVICE_SPEAKER_OUT = new AudioDeviceAttributes( AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, ""); + /** Choose a default stream volume value which does not depend on min/max. */ + private static final int DEFAULT_STREAM_VOLUME = 2; + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @@ -144,6 +147,8 @@ public class VolumeHelperTest { private TestLooper mTestLooper; + private boolean mIsAutomotive; + public static final int[] BASIC_VOLUME_BEHAVIORS = { AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE, AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL, @@ -232,9 +237,10 @@ public class VolumeHelperTest { || packageManager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)); final boolean isSingleVolume = mContext.getResources().getBoolean( Resources.getSystem().getIdentifier("config_single_volume", "bool", "android")); - final boolean automotiveHardened = mContext.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_AUTOMOTIVE) && autoPublicVolumeApiHardening(); - assumeFalse("Skipping test for fixed, TV, single volume and auto devices", + mIsAutomotive = mContext.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_AUTOMOTIVE); + final boolean automotiveHardened = mIsAutomotive && autoPublicVolumeApiHardening(); + assumeFalse("Skipping test for fixed, TV, single volume and auto hardened devices", useFixedVolume || isTelevision || isSingleVolume || automotiveHardened); InstrumentationRegistry.getInstrumentation().getUiAutomation() @@ -249,15 +255,14 @@ public class VolumeHelperTest { {STREAM_MUSIC, STREAM_NOTIFICATION, STREAM_RING, STREAM_ALARM, STREAM_SYSTEM, STREAM_VOICE_CALL, STREAM_ACCESSIBILITY}; for (int streamType : usedStreamTypes) { - final int streamVolume = (mAm.getStreamMinVolume(streamType) + mAm.getStreamMaxVolume( - streamType)) / 2; - - mAudioService.setStreamVolume(streamType, streamVolume, /*flags=*/0, + mAudioService.setStreamVolume(streamType, DEFAULT_STREAM_VOLUME, /*flags=*/0, mContext.getOpPackageName()); } - mAudioService.setRingerModeInternal(RINGER_MODE_NORMAL, mContext.getOpPackageName()); - mAudioService.setRingerModeExternal(RINGER_MODE_NORMAL, mContext.getOpPackageName()); + if (!mIsAutomotive) { + mAudioService.setRingerModeInternal(RINGER_MODE_NORMAL, mContext.getOpPackageName()); + mAudioService.setRingerModeExternal(RINGER_MODE_NORMAL, mContext.getOpPackageName()); + } } private AudioVolumeGroup getStreamTypeVolumeGroup(int streamType) { @@ -297,6 +302,7 @@ public class VolumeHelperTest { @Test public void setStreamRingVolume0_setsRingerModeVibrate() throws Exception { + assumeFalse("Skipping ringer mode test on automotive", mIsAutomotive); mAudioService.setStreamVolume(STREAM_RING, 0, /*flags=*/0, mContext.getOpPackageName()); mTestLooper.dispatchAll(); @@ -462,6 +468,7 @@ public class VolumeHelperTest { @Test public void flagAllowRingerModes_onSystemStreams_changesMode() throws Exception { + assumeFalse("Skipping ringer mode test on automotive", mIsAutomotive); mAudioService.setStreamVolume(STREAM_SYSTEM, mAudioService.getStreamMinVolume(STREAM_SYSTEM), /*flags=*/0, mContext.getOpPackageName()); @@ -476,6 +483,7 @@ public class VolumeHelperTest { @Test public void flagAllowRingerModesAbsent_onNonSystemStreams_noModeChange() throws Exception { + assumeFalse("Skipping ringer mode test on automotive", mIsAutomotive); mAudioService.setStreamVolume(STREAM_MUSIC, mAudioService.getStreamMinVolume(STREAM_MUSIC), /*flags=*/0, mContext.getOpPackageName()); @@ -544,17 +552,23 @@ public class VolumeHelperTest { mAudioService.setDeviceVolume(volMin, usbDevice, mContext.getOpPackageName()); mTestLooper.dispatchAll(); - assertEquals(mAudioService.getDeviceVolume(volMin, usbDevice, - mContext.getOpPackageName()), volMin); + if (!mIsAutomotive) { + // there is a min/max index mismatch in automotive + assertEquals(mAudioService.getDeviceVolume(volMin, usbDevice, + mContext.getOpPackageName()), volMin); + } verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS( - STREAM_MUSIC, minIndex, AudioSystem.DEVICE_OUT_USB_DEVICE); + eq(STREAM_MUSIC), anyInt(), eq(AudioSystem.DEVICE_OUT_USB_DEVICE)); mAudioService.setDeviceVolume(volMid, usbDevice, mContext.getOpPackageName()); mTestLooper.dispatchAll(); - assertEquals(mAudioService.getDeviceVolume(volMid, usbDevice, - mContext.getOpPackageName()), volMid); + if (!mIsAutomotive) { + // there is a min/max index mismatch in automotive + assertEquals(mAudioService.getDeviceVolume(volMid, usbDevice, + mContext.getOpPackageName()), volMid); + } verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS( - STREAM_MUSIC, midIndex, AudioSystem.DEVICE_OUT_USB_DEVICE); + eq(STREAM_MUSIC), anyInt(), eq(AudioSystem.DEVICE_OUT_USB_DEVICE)); } @Test diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java index f47768280cc1..6ac95c829f56 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java @@ -109,6 +109,8 @@ public class FaceEnrollClientTest { private AidlResponseHandler mAidlResponseHandler; @Mock private AuthenticationStateListeners mAuthenticationStateListeners; + @Mock + private BiometricUtils<Face> mBiometricUtils; @Captor private ArgumentCaptor<OperationContextExt> mOperationContextCaptor; @Captor @@ -213,7 +215,7 @@ public class FaceEnrollClientTest { mBiometricLogger, mBiometricContext, 5 /* maxTemplatesPerUser */, true /* debugConsent */, (new FaceEnrollOptions.Builder()).setEnrollReason(ENROLL_SOURCE).build(), - mAuthenticationStateListeners); + mAuthenticationStateListeners, mBiometricUtils); } @Test diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java index 6780e60a22b0..bf970867f149 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java @@ -50,6 +50,7 @@ import com.android.server.biometrics.sensors.LockoutCache; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.LockoutTracker; import com.android.server.biometrics.sensors.UserSwitchProvider; +import com.android.server.biometrics.sensors.face.FaceUtils; import org.junit.Before; import org.junit.Test; @@ -90,6 +91,8 @@ public class SensorTest { private AidlSession mCurrentSession; @Mock private AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback; + @Mock + private FaceUtils mBiometricUtils; private final TestLooper mLooper = new TestLooper(); private final LockoutCache mLockoutCache = new LockoutCache(); @@ -114,7 +117,7 @@ public class SensorTest { mUserSwitchProvider); mHalCallback = new AidlResponseHandler(mContext, mScheduler, SENSOR_ID, USER_ID, mLockoutCache, mLockoutResetDispatcher, mAuthSessionCoordinator, - mAidlResponseHandlerCallback); + mAidlResponseHandlerCallback, mBiometricUtils); } @Test diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapterTest.java index 4248e5e37238..24ce569f644e 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapterTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/HidlToAidlSensorAdapterTest.java @@ -206,7 +206,7 @@ public class HidlToAidlSensorAdapterTest { new int[]{} /* disabledFeatures */, ENROLL_TIMEOUT_SEC, null /* previewSurface */, SENSOR_ID, mLogger, mBiometricContext, 1 /* maxTemplatesPerUser */, false /* debugConsent */, (new FaceEnrollOptions.Builder()).build(), - mAuthenticationStateListeners)); + mAuthenticationStateListeners, mBiometricUtils)); mLooper.dispatchAll(); verify(mAidlResponseHandlerCallback).onEnrollSuccess(); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java index 698db2e19661..4ef8782386d5 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java @@ -51,6 +51,7 @@ import com.android.server.biometrics.sensors.LockoutCache; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.LockoutTracker; import com.android.server.biometrics.sensors.UserSwitchProvider; +import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils; import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher; import org.junit.Before; @@ -96,6 +97,8 @@ public class SensorTest { private HandlerThread mThread; @Mock AidlResponseHandler.AidlResponseHandlerCallback mAidlResponseHandlerCallback; + @Mock + private FingerprintUtils mBiometricUtils; private final TestLooper mLooper = new TestLooper(); private final LockoutCache mLockoutCache = new LockoutCache(); @@ -121,7 +124,7 @@ public class SensorTest { mUserSwitchProvider); mHalCallback = new AidlResponseHandler(mContext, mScheduler, SENSOR_ID, USER_ID, mLockoutCache, mLockoutResetDispatcher, mAuthSessionCoordinator, - mAidlResponseHandlerCallback); + mAidlResponseHandlerCallback, mBiometricUtils); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java index 51f64ba2b483..b97a2684576c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java @@ -15,6 +15,7 @@ */ package com.android.server.notification; +import static android.app.Flags.FLAG_SORT_SECTION_BY_TIME; import static android.app.Notification.COLOR_DEFAULT; import static android.app.Notification.FLAG_AUTO_CANCEL; import static android.app.Notification.FLAG_BUBBLE; @@ -36,6 +37,7 @@ import static android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPIN import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; +import static com.android.server.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS; import static com.android.server.notification.GroupHelper.AGGREGATE_GROUP_KEY; import static com.android.server.notification.GroupHelper.AUTOGROUP_KEY; import static com.android.server.notification.GroupHelper.BASE_FLAGS; @@ -2204,7 +2206,7 @@ public class GroupHelperTest extends UiServiceTestCase { verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), eq(expectedGroupKey_silent), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), - eq(expectedGroupKey_silent), eq(false)); + eq(expectedGroupKey_silent), eq(true)); // Check that the alerting section group is removed verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg), @@ -2264,13 +2266,15 @@ public class GroupHelperTest extends UiServiceTestCase { notificationList); // Check that channel1's notifications are moved to the silent section group - expectedSummaryAttr = new NotificationAttributes(BASE_FLAGS, - mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, - "TEST_CHANNEL_ID1"); - verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), - eq(expectedGroupKey_silent), anyInt(), eq(expectedSummaryAttr)); - verify(mCallback, times(AUTOGROUP_AT_COUNT/2 + 1)).addAutoGroup(anyString(), - eq(expectedGroupKey_silent), eq(false)); + // But not enough to auto-group => remove override group key + verify(mCallback, never()).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + anyString(), anyInt(), any()); + verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean()); + for (NotificationRecord record: notificationList) { + if (record.getChannel().getId().equals(channel1.getId())) { + assertThat(record.getSbn().getOverrideGroupKey()).isNull(); + } + } // Check that the alerting section group is not removed, only updated expectedSummaryAttr = new NotificationAttributes(BASE_FLAGS, @@ -2343,7 +2347,7 @@ public class GroupHelperTest extends UiServiceTestCase { verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), eq(expectedGroupKey_silent), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); verify(mCallback, times(numSilentGroupNotifications)).addAutoGroup(anyString(), - eq(expectedGroupKey_silent), eq(false)); + eq(expectedGroupKey_silent), eq(true)); // Check that the alerting section group is removed verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg), @@ -2353,6 +2357,60 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testAutogroup_updateChannel_reachedMinAutogroupCount() { + final String pkg = "package"; + final NotificationChannel channel1 = new NotificationChannel("TEST_CHANNEL_ID1", + "TEST_CHANNEL_ID1", IMPORTANCE_DEFAULT); + final NotificationChannel channel2 = new NotificationChannel("TEST_CHANNEL_ID2", + "TEST_CHANNEL_ID2", IMPORTANCE_LOW); + final List<NotificationRecord> notificationList = new ArrayList<>(); + // Post notifications with different channels that would autogroup in different sections + NotificationRecord r; + // Not enough notifications to autogroup initially + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + if (i % 2 == 0) { + r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, null, false, channel1); + } else { + r = getNotificationRecord(pkg, i, String.valueOf(i), + UserHandle.SYSTEM, null, false, channel2); + } + notificationList.add(r); + mGroupHelper.onNotificationPosted(r, false); + } + verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), + anyString(), anyInt(), any()); + verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean()); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + Mockito.reset(mCallback); + + // Update channel1's importance + final String expectedGroupKey_silent = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier()); + channel1.setImportance(IMPORTANCE_LOW); + for (NotificationRecord record: notificationList) { + if (record.getChannel().getId().equals(channel1.getId())) { + record.updateNotificationChannel(channel1); + } + } + mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel1, + notificationList); + + // Check that channel1's notifications are moved to the silent section & autogroup all + NotificationAttributes expectedSummaryAttr = new NotificationAttributes(BASE_FLAGS, + mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, + "TEST_CHANNEL_ID1"); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey_silent), eq(true)); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey_silent), anyInt(), eq(expectedSummaryAttr)); + } + + @Test @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS}) public void testNoGroup_singletonGroup_underLimit() { @@ -2519,7 +2577,11 @@ public class GroupHelperTest extends UiServiceTestCase { assertThat(cachedSummary).isNull(); } - private void checkNonGroupableNotifications() { + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS) + public void testNonGroupableNotifications() { + // Check that there is no valid section for: conversations, calls, foreground services NotificationRecord notification_conversation = mock(NotificationRecord.class); when(notification_conversation.isConversation()).thenReturn(true); assertThat(GroupHelper.getSection(notification_conversation)).isNull(); @@ -2592,8 +2654,6 @@ public class GroupHelperTest extends UiServiceTestCase { "", false, recsChannel); assertThat(GroupHelper.getSection(notification_recs).mName).isEqualTo( "AlertingSection"); - - checkNonGroupableNotifications(); } @Test @@ -2638,8 +2698,86 @@ public class GroupHelperTest extends UiServiceTestCase { "", false, recsChannel); assertThat(GroupHelper.getSection(notification_recs).mName).isEqualTo( "RecsSection"); + } - checkNonGroupableNotifications(); + @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS}) + public void testNonGroupableNotifications_forceGroupConversations() { + // Check that there is no valid section for: calls, foreground services + NotificationRecord notification_call = spy(getNotificationRecord(mPkg, 0, "", mUser, + "", false, IMPORTANCE_LOW)); + Notification n = mock(Notification.class); + StatusBarNotification sbn = spy(getSbn("package", 0, "0", UserHandle.SYSTEM)); + when(notification_call.isConversation()).thenReturn(false); + when(notification_call.getNotification()).thenReturn(n); + when(notification_call.getSbn()).thenReturn(sbn); + when(sbn.getNotification()).thenReturn(n); + when(n.isStyle(Notification.CallStyle.class)).thenReturn(true); + assertThat(GroupHelper.getSection(notification_call)).isNull(); + + NotificationRecord notification_colorFg = spy(getNotificationRecord(mPkg, 0, "", mUser, + "", false, IMPORTANCE_LOW)); + sbn = spy(getSbn("package", 0, "0", UserHandle.SYSTEM)); + n = mock(Notification.class); + when(notification_colorFg.isConversation()).thenReturn(false); + when(notification_colorFg.getNotification()).thenReturn(n); + when(notification_colorFg.getSbn()).thenReturn(sbn); + when(sbn.getNotification()).thenReturn(n); + when(n.isForegroundService()).thenReturn(true); + when(n.isColorized()).thenReturn(true); + when(n.isStyle(Notification.CallStyle.class)).thenReturn(false); + assertThat(GroupHelper.getSection(notification_colorFg)).isNull(); + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS}) + @DisableFlags(FLAG_SORT_SECTION_BY_TIME) + public void testConversationGroupSections_disableSortSectionByTime() { + // Check that there are separate sections for conversations: alerting and silent + NotificationRecord notification_conversation_silent = getNotificationRecord(mPkg, 0, "", + mUser, "", false, IMPORTANCE_LOW); + notification_conversation_silent = spy(notification_conversation_silent); + when(notification_conversation_silent.isConversation()).thenReturn(true); + assertThat(GroupHelper.getSection(notification_conversation_silent).mName).isEqualTo( + "PeopleSection(silent)"); + + // Check that there is a correct section for conversations + NotificationRecord notification_conversation_alerting = getNotificationRecord(mPkg, 0, "", + mUser, "", false, IMPORTANCE_DEFAULT); + notification_conversation_alerting = spy(notification_conversation_alerting); + when(notification_conversation_alerting.isConversation()).thenReturn(true); + assertThat(GroupHelper.getSection(notification_conversation_alerting).mName).isEqualTo( + "PeopleSection(alerting)"); + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS, + FLAG_SORT_SECTION_BY_TIME}) + public void testConversationGroupSections() { + // Check that there is a single section for silent/alerting conversations + NotificationRecord notification_conversation_silent = getNotificationRecord(mPkg, 0, "", + mUser, "", false, IMPORTANCE_LOW); + notification_conversation_silent = spy(notification_conversation_silent); + when(notification_conversation_silent.isConversation()).thenReturn(true); + assertThat(GroupHelper.getSection(notification_conversation_silent).mName).isEqualTo( + "PeopleSection"); + + NotificationRecord notification_conversation_alerting = getNotificationRecord(mPkg, 0, "", + mUser, "", false, IMPORTANCE_DEFAULT); + notification_conversation_alerting = spy(notification_conversation_alerting); + when(notification_conversation_alerting.isConversation()).thenReturn(true); + assertThat(GroupHelper.getSection(notification_conversation_alerting).mName).isEqualTo( + "PeopleSection"); + + // Check that there is a section for priority conversations + NotificationRecord notification_conversation_prio = getNotificationRecord(mPkg, 0, "", + mUser, "", false, IMPORTANCE_DEFAULT); + notification_conversation_prio = spy(notification_conversation_prio); + when(notification_conversation_prio.isConversation()).thenReturn(true); + notification_conversation_prio.getChannel().setImportantConversation(true); + assertThat(GroupHelper.getSection(notification_conversation_prio).mName).isEqualTo( + "PeopleSection(priority)"); } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index dd2b84504e51..c1e3f47679ca 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -2186,6 +2186,80 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void testReadXml_upgradeToModesUi_resetsImplicitRuleIcon() throws Exception { + setupZenConfig(); + mZenModeHelper.mConfig.automaticRules.clear(); + + ZenRule implicitRuleWithModesUi = expectedImplicitRule("pkg", + ZEN_MODE_IMPORTANT_INTERRUPTIONS, POLICY, null); + + // Add one implicit rule in the pre-MODES_UI configuration. + ZenRule implicitRuleBeforeModesUi = implicitRuleWithModesUi.copy(); + implicitRuleBeforeModesUi.iconResName = "pkg_icon"; + mZenModeHelper.mConfig.automaticRules.put(implicitRuleBeforeModesUi.id, + implicitRuleBeforeModesUi); + // Plus one other normal rule. + ZenRule anotherRule = newZenRule("other_pkg", Instant.now(), null); + anotherRule.id = "other_rule"; + anotherRule.iconResName = "other_icon"; + anotherRule.type = TYPE_IMMERSIVE; + mZenModeHelper.mConfig.automaticRules.put(anotherRule.id, anotherRule); + + // Write with pre-modes-ui = (modes_api) version, then re-read. + ByteArrayOutputStream baos = writeXmlAndPurge(ZenModeConfig.XML_VERSION_MODES_API); + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream( + new ByteArrayInputStream(baos.toByteArray())), null); + parser.nextTag(); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + + // Implicit rule was updated. + assertThat(mZenModeHelper.mConfig.automaticRules.get(implicitRuleBeforeModesUi.id)) + .isEqualTo(implicitRuleWithModesUi); + + // The other rule was untouched. + assertThat(mZenModeHelper.mConfig.automaticRules.get(anotherRule.id)) + .isEqualTo(anotherRule); + } + + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void testReadXml_onModesUi_implicitRulesUntouched() throws Exception { + setupZenConfig(); + mZenModeHelper.mConfig.automaticRules.clear(); + + // Add one implicit rule already in its post-modes-UI configuration, also customized with + // an icon; + ZenRule implicitRuleWithModesUi = expectedImplicitRule("pkg", + ZEN_MODE_IMPORTANT_INTERRUPTIONS, POLICY, null); + implicitRuleWithModesUi.iconResName = "icon_chosen_by_user"; + mZenModeHelper.mConfig.automaticRules.put(implicitRuleWithModesUi.id, + implicitRuleWithModesUi); + + // Plus one other normal rule. + ZenRule anotherRule = newZenRule("other_pkg", Instant.now(), null); + anotherRule.id = "other_rule"; + anotherRule.iconResName = "other_icon"; + anotherRule.type = TYPE_IMMERSIVE; + mZenModeHelper.mConfig.automaticRules.put(anotherRule.id, anotherRule); + + // Write with modes_ui version, then re-read. + ByteArrayOutputStream baos = writeXmlAndPurge(ZenModeConfig.XML_VERSION_MODES_UI); + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream( + new ByteArrayInputStream(baos.toByteArray())), null); + parser.nextTag(); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + + // Both rules were untouched + assertThat(mZenModeHelper.mConfig.automaticRules.get(implicitRuleWithModesUi.id)) + .isEqualTo(implicitRuleWithModesUi); + assertThat(mZenModeHelper.mConfig.automaticRules.get(anotherRule.id)) + .isEqualTo(anotherRule); + } + + @Test public void testCountdownConditionSubscription() throws Exception { ZenModeConfig config = new ZenModeConfig(); mZenModeHelper.mConfig = config; @@ -5538,6 +5612,49 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void removeAndAddAutomaticZenRule_ifChangingComponent_isAllowedAndDoesNotRestore() { + // Start with a rule. + mZenModeHelper.mConfig.automaticRules.clear(); + AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID) + .setOwner(new ComponentName("first", "owner")) + .setInterruptionFilter(INTERRUPTION_FILTER_ALL) + .build(); + String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, + ORIGIN_APP, "add it", CUSTOM_PKG_UID); + + // User customizes it. + AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, ORIGIN_USER_IN_SYSTEMUI, + "userUpdate", SYSTEM_UID); + + // App deletes it. It's preserved for a possible restoration. + mZenModeHelper.removeAutomaticZenRule(ruleId, ORIGIN_APP, "delete it", + CUSTOM_PKG_UID); + assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); + assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1); + + // App adds it again, but this time with a different owner! + AutomaticZenRule readdingWithDifferentOwner = new AutomaticZenRule.Builder(rule) + .setOwner(new ComponentName("second", "owner")) + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .build(); + String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + readdingWithDifferentOwner, ORIGIN_APP, "add it again", CUSTOM_PKG_UID); + + // Verify that the rule was NOT restored: + assertThat(newRuleId).isNotEqualTo(ruleId); + AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId); + assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS); + assertThat(finalRule.getOwner()).isEqualTo(new ComponentName("second", "owner")); + + // Also, we discarded the "deleted rule" since we found it but decided not to use it. + assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0); + } + + @Test @EnableFlags(FLAG_MODES_API) public void removeAutomaticZenRule_preservedForRestoringByPackageAndConditionId() { mContext.getTestablePermissions().setPermission(Manifest.permission.MANAGE_NOTIFICATIONS, @@ -6809,7 +6926,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { rule.zenPolicy = policy; rule.pkg = ownerPkg; rule.name = CUSTOM_APP_LABEL; - rule.iconResName = ICON_RES_NAME; + if (!Flags.modesUi()) { + rule.iconResName = ICON_RES_NAME; + } rule.triggerDescription = mContext.getString(R.string.zen_mode_implicit_trigger_description, CUSTOM_APP_LABEL); rule.type = AutomaticZenRule.TYPE_OTHER; diff --git a/services/tests/vibrator/Android.bp b/services/tests/vibrator/Android.bp index 43ad44f057cc..2549ff5360ec 100644 --- a/services/tests/vibrator/Android.bp +++ b/services/tests/vibrator/Android.bp @@ -32,11 +32,11 @@ android_test { "frameworks-base-testutils", "frameworks-services-vibrator-testutils", "junit", - "junit-params", "mockito-target-inline-minus-junit4", "platform-test-annotations", "service-permission.stubs.system_server", "services.core", + "TestParameterInjector", ], jni_libs: ["libdexmakerjvmtiagent"], platform_apis: true, diff --git a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackCustomizationTest.java b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackCustomizationTest.java index e0d05df1de80..2d312d2649dd 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackCustomizationTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackCustomizationTest.java @@ -18,32 +18,36 @@ package com.android.server.vibrator; import static android.os.VibrationEffect.Composition.PRIMITIVE_TICK; import static android.os.VibrationEffect.EFFECT_CLICK; +import static android.os.VibrationEffect.EFFECT_TICK; +import static android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED; +import static android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_VIBRATION_OEM_CUSTOMIZATION_ENABLED; +import static android.os.vibrator.Flags.FLAG_LOAD_HAPTIC_FEEDBACK_VIBRATION_CUSTOMIZATION_FROM_RESOURCES; import static com.android.internal.R.xml.haptic_feedback_customization; -import static com.android.server.vibrator.HapticFeedbackCustomization.CustomizationParserException; +import static com.android.internal.R.xml.haptic_feedback_customization_source_rotary_encoder; +import static com.android.internal.R.xml.haptic_feedback_customization_source_touchscreen; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; +import android.annotation.Nullable; import android.content.res.Resources; import android.os.VibrationEffect; import android.os.VibratorInfo; -import android.os.vibrator.Flags; import android.platform.test.flag.junit.SetFlagsRule; import android.util.AtomicFile; -import android.util.SparseArray; +import android.view.InputDevice; import androidx.test.InstrumentationRegistry; import com.android.internal.R; -import com.android.internal.annotations.Keep; -import junitparams.JUnitParamsRunner; -import junitparams.Parameters; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; import org.junit.Before; import org.junit.Rule; @@ -56,7 +60,7 @@ import org.mockito.junit.MockitoRule; import java.io.File; import java.io.FileOutputStream; -@RunWith(JUnitParamsRunner.class) +@RunWith(TestParameterInjector.class) public class HapticFeedbackCustomizationTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -69,83 +73,78 @@ public class HapticFeedbackCustomizationTest { private static final VibrationEffect COMPOSITION_VIBRATION = VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK, 0.2497f).compose(); - private static final String PREDEFINED_VIBRATION_XML = + private static final String PREDEFINED_VIBRATION_CLICK_XML = "<vibration-effect><predefined-effect name=\"click\"/></vibration-effect>"; - private static final VibrationEffect PREDEFINED_VIBRATION = + private static final VibrationEffect PREDEFINED_VIBRATION_CLICK = VibrationEffect.createPredefined(EFFECT_CLICK); + private static final String PREDEFINED_VIBRATION_TICK_XML = + "<vibration-effect><predefined-effect name=\"tick\"/></vibration-effect>"; + private static final VibrationEffect PREDEFINED_VIBRATION_TICK = + VibrationEffect.createPredefined(EFFECT_TICK); + private static final String WAVEFORM_VIBRATION_XML = "<vibration-effect>" + "<waveform-effect>" + "<waveform-entry durationMs=\"123\" amplitude=\"254\"/>" + "</waveform-effect>" + "</vibration-effect>"; - private static final VibrationEffect WAVEFORM_VIBARTION = + private static final VibrationEffect WAVEFORM_VIBRATION = VibrationEffect.createWaveform(new long[] {123}, new int[] {254}, -1); @Mock private Resources mResourcesMock; @Mock private VibratorInfo mVibratorInfoMock; - @Keep - private static Object[][] hapticFeedbackCustomizationTestArguments() { - // (boolean hasConfigFile, boolean hasRes). - return new Object[][] {{true, true}, {true, false}, {false, true}}; + private enum CustomizationSource { + DEVICE_CONFIG_FILE, + DEVICE_RESOURCE, + DEVICE_RESOURCE_INPUT_ROTARY, + DEVICE_RESOURCE_INPUT_TOUCHSCREEN } @Before public void setUp() { + clearFileAndResourceSetup(); when(mVibratorInfoMock.areVibrationFeaturesSupported(any())).thenReturn(true); - mSetFlagsRule.enableFlags(Flags.FLAG_HAPTIC_FEEDBACK_VIBRATION_OEM_CUSTOMIZATION_ENABLED); - mSetFlagsRule.disableFlags( - Flags.FLAG_LOAD_HAPTIC_FEEDBACK_VIBRATION_CUSTOMIZATION_FROM_RESOURCES); - } - - @Test - @Parameters(method = "hapticFeedbackCustomizationTestArguments") - public void testParseCustomizations_noCustomization_success( - boolean hasConfigFile, boolean hasRes) throws Exception { - String xml = "<haptic-feedback-constants></haptic-feedback-constants>"; - SparseArray<VibrationEffect> expectedMapping = new SparseArray<>(); - setupParseCustomizations(xml, hasConfigFile, hasRes); - - assertParseCustomizationsSucceeds(xml, expectedMapping, hasConfigFile, hasRes); + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_VIBRATION_OEM_CUSTOMIZATION_ENABLED); } @Test - @Parameters(method = "hapticFeedbackCustomizationTestArguments") - public void testParseCustomizations_featureFlagDisabled_returnsNull( - boolean hasConfigFile, boolean hasRes) throws Exception { - mSetFlagsRule.disableFlags(Flags.FLAG_HAPTIC_FEEDBACK_VIBRATION_OEM_CUSTOMIZATION_ENABLED); + public void testParseCustomizations_featureFlagDisabled_customizationNotLoaded( + @TestParameter CustomizationSource customizationSource) throws Exception { + mSetFlagsRule.disableFlags(FLAG_HAPTIC_FEEDBACK_VIBRATION_OEM_CUSTOMIZATION_ENABLED); // Valid customization XML. String xml = "<haptic-feedback-constants>" + "<constant id=\"10\">" + COMPOSITION_VIBRATION_XML + "</constant>" + "</haptic-feedback-constants>"; + HapticFeedbackCustomization customization = createCustomizationForSource(xml, + customizationSource); - setupParseCustomizations(xml, hasConfigFile, hasRes); - assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorInfoMock)) + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, customization)) .isNull(); } @Test - @Parameters(method = "hapticFeedbackCustomizationTestArguments") public void testParseCustomizations_oneVibrationCustomization_success( - boolean hasConfigFile, boolean hasRes) throws Exception { + @TestParameter CustomizationSource customizationSource) + throws Exception { String xml = "<haptic-feedback-constants>" + "<constant id=\"10\">" + COMPOSITION_VIBRATION_XML + "</constant>" + "</haptic-feedback-constants>"; - SparseArray<VibrationEffect> expectedMapping = new SparseArray<>(); - expectedMapping.put(10, COMPOSITION_VIBRATION); + HapticFeedbackCustomization customization = createCustomizationForSource(xml, + customizationSource); - assertParseCustomizationsSucceeds(xml, expectedMapping, hasConfigFile, hasRes); + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, customization)) + .isEqualTo(COMPOSITION_VIBRATION); } @Test - @Parameters(method = "hapticFeedbackCustomizationTestArguments") public void testParseCustomizations_oneVibrationSelectCustomization_success( - boolean hasConfigFile, boolean hasRes) throws Exception { + @TestParameter CustomizationSource customizationSource) + throws Exception { String xml = "<haptic-feedback-constants>" + "<constant id=\"10\">" + "<vibration-select>" @@ -153,28 +152,28 @@ public class HapticFeedbackCustomizationTest { + "</vibration-select>" + "</constant>" + "</haptic-feedback-constants>"; - SparseArray<VibrationEffect> expectedMapping = new SparseArray<>(); - expectedMapping.put(10, COMPOSITION_VIBRATION); + HapticFeedbackCustomization customization = createCustomizationForSource(xml, + customizationSource); - assertParseCustomizationsSucceeds(xml, expectedMapping, hasConfigFile, hasRes); + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, customization)) + .isEqualTo(COMPOSITION_VIBRATION); } @Test - @Parameters(method = "hapticFeedbackCustomizationTestArguments") public void testParseCustomizations_multipleCustomizations_success( - boolean hasConfigFile, boolean hasRes) throws Exception { + @TestParameter CustomizationSource customizationSource) throws Exception { String xml = "<haptic-feedback-constants>" + "<constant id=\"1\">" + COMPOSITION_VIBRATION_XML + "</constant>" + "<constant id=\"12\">" + "<vibration-select>" - + PREDEFINED_VIBRATION_XML + + PREDEFINED_VIBRATION_CLICK_XML + WAVEFORM_VIBRATION_XML + "</vibration-select>" + "</constant>" + "<constant id=\"150\">" - + PREDEFINED_VIBRATION_XML + + PREDEFINED_VIBRATION_CLICK_XML + "</constant>" + "<constant id=\"10\">" + "<vibration-select>" @@ -183,33 +182,39 @@ public class HapticFeedbackCustomizationTest { + "</vibration-select>" + "</constant>" + "</haptic-feedback-constants>"; - SparseArray<VibrationEffect> expectedMapping = new SparseArray<>(); - expectedMapping.put(1, COMPOSITION_VIBRATION); - expectedMapping.put(12, PREDEFINED_VIBRATION); - expectedMapping.put(150, PREDEFINED_VIBRATION); - expectedMapping.put(10, WAVEFORM_VIBARTION); - - assertParseCustomizationsSucceeds(xml, expectedMapping, hasConfigFile, hasRes); + HapticFeedbackCustomization customization = createCustomizationForSource(xml, + customizationSource); + + assertThat(getEffectForSource(/* effectId= */ 1, customizationSource, + customization)) + .isEqualTo(COMPOSITION_VIBRATION); + assertThat(getEffectForSource(/* effectId= */ 12, customizationSource, + customization)) + .isEqualTo(PREDEFINED_VIBRATION_CLICK); + assertThat(getEffectForSource(/* effectId= */ 150, customizationSource, + customization)) + .isEqualTo(PREDEFINED_VIBRATION_CLICK); + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, + customization)) + .isEqualTo(WAVEFORM_VIBRATION); } @Test - @Parameters(method = "hapticFeedbackCustomizationTestArguments") public void testParseCustomizations_multipleCustomizations_noSupportedVibration_success( - boolean hasConfigFile, boolean hasRes) - throws Exception { - makeUnsupported(COMPOSITION_VIBRATION, PREDEFINED_VIBRATION, WAVEFORM_VIBARTION); + @TestParameter CustomizationSource customizationSource) throws Exception { + makeUnsupported(COMPOSITION_VIBRATION, PREDEFINED_VIBRATION_CLICK, WAVEFORM_VIBRATION); String xml = "<haptic-feedback-constants>" + "<constant id=\"1\">" + COMPOSITION_VIBRATION_XML + "</constant>" + "<constant id=\"12\">" + "<vibration-select>" - + PREDEFINED_VIBRATION_XML + + PREDEFINED_VIBRATION_CLICK_XML + WAVEFORM_VIBRATION_XML + "</vibration-select>" + "</constant>" + "<constant id=\"150\">" - + PREDEFINED_VIBRATION_XML + + PREDEFINED_VIBRATION_CLICK_XML + "</constant>" + "<constant id=\"10\">" + "<vibration-select>" @@ -218,17 +223,23 @@ public class HapticFeedbackCustomizationTest { + "</vibration-select>" + "</constant>" + "</haptic-feedback-constants>"; - SparseArray<VibrationEffect> expectedMapping = new SparseArray<>(); - - assertParseCustomizationsSucceeds(xml, expectedMapping, hasConfigFile, hasRes); + HapticFeedbackCustomization customization = createCustomizationForSource(xml, + customizationSource); + + assertThat(getEffectForSource(/* effectId= */ 1, customizationSource, + customization)).isNull(); + assertThat(getEffectForSource(/* effectId= */ 12, customizationSource, + customization)).isNull(); + assertThat(getEffectForSource(/* effectId= */ 150, customizationSource, + customization)).isNull(); + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, + customization)).isNull(); } @Test - @Parameters(method = "hapticFeedbackCustomizationTestArguments") public void testParseCustomizations_multipleCustomizations_someUnsupportedVibration_success( - boolean hasConfigFile, boolean hasRes) - throws Exception { - makeSupported(PREDEFINED_VIBRATION, WAVEFORM_VIBARTION); + @TestParameter CustomizationSource customizationSource) throws Exception { + makeSupported(PREDEFINED_VIBRATION_CLICK, WAVEFORM_VIBRATION); makeUnsupported(COMPOSITION_VIBRATION); String xml = "<haptic-feedback-constants>" + "<constant id=\"1\">" // No supported customization. @@ -236,68 +247,89 @@ public class HapticFeedbackCustomizationTest { + "</constant>" + "<constant id=\"12\">" // PREDEFINED_VIBRATION is the first/only supported. + "<vibration-select>" - + PREDEFINED_VIBRATION_XML + + PREDEFINED_VIBRATION_CLICK_XML + COMPOSITION_VIBRATION_XML + "</vibration-select>" + "</constant>" - + "<constant id=\"14\">" // WAVEFORM_VIBARTION is the first/only supported. + + "<constant id=\"14\">" // WAVEFORM_VIBRATION is the first/only supported. + "<vibration-select>" + COMPOSITION_VIBRATION_XML + WAVEFORM_VIBRATION_XML + "</vibration-select>" + "</constant>" + "<constant id=\"150\">" // PREDEFINED_VIBRATION is the first/only supported. - + PREDEFINED_VIBRATION_XML + + PREDEFINED_VIBRATION_CLICK_XML + "</constant>" + "<constant id=\"10\">" // PREDEFINED_VIBRATION is the first supported. + "<vibration-select>" - + PREDEFINED_VIBRATION_XML + + PREDEFINED_VIBRATION_CLICK_XML + WAVEFORM_VIBRATION_XML + "</vibration-select>" + "</constant>" + "</haptic-feedback-constants>"; - SparseArray<VibrationEffect> expectedMapping = new SparseArray<>(); - expectedMapping.put(12, PREDEFINED_VIBRATION); - expectedMapping.put(14, WAVEFORM_VIBARTION); - expectedMapping.put(150, PREDEFINED_VIBRATION); - expectedMapping.put(10, PREDEFINED_VIBRATION); - - assertParseCustomizationsSucceeds(xml, expectedMapping, hasConfigFile, hasRes); + HapticFeedbackCustomization customization = createCustomizationForSource(xml, + customizationSource); + + assertThat(getEffectForSource(/* effectId= */ 1, customizationSource, + customization)).isNull(); + assertThat(getEffectForSource(/* effectId= */ 12, customizationSource, + customization)).isEqualTo(PREDEFINED_VIBRATION_CLICK); + assertThat(getEffectForSource(/* effectId= */ 14, customizationSource, + customization)).isEqualTo(WAVEFORM_VIBRATION); + assertThat(getEffectForSource(/* effectId= */ 150, customizationSource, + customization)).isEqualTo(PREDEFINED_VIBRATION_CLICK); + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, + customization)).isEqualTo(PREDEFINED_VIBRATION_CLICK); } @Test - public void testParseCustomizations_noCustomizationFile_returnsNull() throws Exception { - setCustomizationFilePath(""); - - assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorInfoMock)) - .isNull(); - - setCustomizationFilePath(null); - - assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorInfoMock)) - .isNull(); - - setCustomizationFilePath("non_existent_file.xml"); - - assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorInfoMock)) - .isNull(); - } - - @Test - public void testParseCustomizations_noCustomizationResource_returnsNull() throws Exception { - mSetFlagsRule.enableFlags( - Flags.FLAG_LOAD_HAPTIC_FEEDBACK_VIBRATION_CUSTOMIZATION_FROM_RESOURCES); - doThrow(new Resources.NotFoundException()) - .when(mResourcesMock).getXml(haptic_feedback_customization); - - assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorInfoMock)) - .isNull(); + public void testParseCustomizations_malformedXml_notLoaded( + @TestParameter CustomizationSource customizationSource) throws Exception { + // No end "<constant>" tag + String xmlNoEndConstantTag = "<haptic-feedback-constants>" + + "<constant id=\"10\">" + + COMPOSITION_VIBRATION_XML + + "</haptic-feedback-constants>"; + HapticFeedbackCustomization customizationNoEndConstantTag = createCustomizationForSource( + xmlNoEndConstantTag, customizationSource); + // No start "<haptic-feedback-constants>" tag + String xmlNoStartCustomizationTag = "<constant id=\"10\">" + + COMPOSITION_VIBRATION_XML + + "</constant>" + + "</haptic-feedback-constants>"; + clearFileAndResourceSetup(); + HapticFeedbackCustomization customizationNoStartCustomizationTag = + createCustomizationForSource(xmlNoStartCustomizationTag, customizationSource); + // No end "<haptic-feedback-constants>" tag + String xmlNoEndCustomizationTag = "<haptic-feedback-constants>" + + "<constant id=\"10\">" + + COMPOSITION_VIBRATION_XML + + "</constant>"; + clearFileAndResourceSetup(); + HapticFeedbackCustomization customizationNoEndCustomizationTag = + createCustomizationForSource(xmlNoEndCustomizationTag, customizationSource); + // No start "<constant>" tag + String xmlNoStartConstantTag = "<haptic-feedback-constants>" + + COMPOSITION_VIBRATION_XML + + "</constant>" + + "</haptic-feedback-constants>"; + clearFileAndResourceSetup(); + HapticFeedbackCustomization customizationNoStartConstantTag = createCustomizationForSource( + xmlNoStartConstantTag, customizationSource); + + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, + customizationNoEndConstantTag)).isNull(); + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, + customizationNoStartCustomizationTag)).isNull(); + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, + customizationNoEndCustomizationTag)).isNull(); + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, + customizationNoStartConstantTag)).isNull(); } @Test - @Parameters(method = "hapticFeedbackCustomizationTestArguments") - public void testParseCustomizations_disallowedVibrationForHapticFeedback_throwsException( - boolean hasConfigFile, boolean hasRes) throws Exception { + public void testParseCustomizations_disallowedVibrationForHapticFeedback_notLoaded( + @TestParameter CustomizationSource customizationSource) throws Exception { // The XML content is good, but the serialized vibration is not supported for haptic // feedback usage (i.e. repeating vibration). String xml = "<haptic-feedback-constants>" @@ -311,185 +343,245 @@ public class HapticFeedbackCustomizationTest { + "</vibration-effect>" + "</constant>" + "</haptic-feedback-constants>"; + HapticFeedbackCustomization customization = createCustomizationForSource(xml, + customizationSource); - assertParseCustomizationsFails(xml, hasConfigFile, hasRes); - } - - @Test - @Parameters(method = "hapticFeedbackCustomizationTestArguments") - public void testParseCustomizations_emptyXml_throwsException( - boolean hasConfigFile, boolean hasRes) throws Exception { - assertParseCustomizationsFails("", hasConfigFile, hasRes); + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, customization)) + .isNull(); } @Test - @Parameters(method = "hapticFeedbackCustomizationTestArguments") - public void testParseCustomizations_noVibrationXml_throwsException( - boolean hasConfigFile, boolean hasRes) throws Exception { + public void testParseCustomizations_xmlNoVibration_notLoaded( + @TestParameter CustomizationSource customizationSource) throws Exception { String xml = "<haptic-feedback-constants>" + "<constant id=\"1\">" + "</constant>" + "</haptic-feedback-constants>"; + HapticFeedbackCustomization customization = createCustomizationForSource(xml, + customizationSource); - assertParseCustomizationsFails(xml, hasConfigFile, hasRes); + assertThat(getEffectForSource(/* effectId= */ 1, customizationSource, customization)) + .isNull(); } + @Test - @Parameters(method = "hapticFeedbackCustomizationTestArguments") - public void testParseCustomizations_badEffectId_throwsException( - boolean hasConfigFile, boolean hasRes) throws Exception { - // Negative id + public void testParseCustomizations_badEffectId_notLoaded( + @TestParameter CustomizationSource customizationSource) throws Exception { String xmlNegativeId = "<haptic-feedback-constants>" + "<constant id=\"-10\">" + COMPOSITION_VIBRATION_XML + "</constant>" + "</haptic-feedback-constants>"; - // Non-numeral id - String xmlNonNumericalId = "<haptic-feedback-constants>" - + "<constant id=\"xyz\">" - + COMPOSITION_VIBRATION_XML - + "</constant>" - + "</haptic-feedback-constants>"; - - assertParseCustomizationsFails(xmlNegativeId, hasConfigFile, hasRes); - assertParseCustomizationsFails(xmlNonNumericalId, hasConfigFile, hasRes); - } + HapticFeedbackCustomization customization = createCustomizationForSource( + xmlNegativeId, customizationSource); - @Test - @Parameters(method = "hapticFeedbackCustomizationTestArguments") - public void testParseCustomizations_malformedXml_throwsException( - boolean hasConfigFile, boolean hasRes) throws Exception { - // No start "<constant>" tag - String xmlNoStartConstantTag = "<haptic-feedback-constants>" - + COMPOSITION_VIBRATION_XML - + "</constant>" - + "</haptic-feedback-constants>"; - // No end "<constant>" tag - String xmlNoEndConstantTag = "<haptic-feedback-constants>" - + "<constant id=\"10\">" - + COMPOSITION_VIBRATION_XML - + "</haptic-feedback-constants>"; - // No start "<haptic-feedback-constants>" tag - String xmlNoStartCustomizationTag = "<constant id=\"10\">" - + COMPOSITION_VIBRATION_XML - + "</constant>" - + "</haptic-feedback-constants>"; - // No end "<haptic-feedback-constants>" tag - String xmlNoEndCustomizationTag = "<haptic-feedback-constants>" - + "<constant id=\"10\">" - + COMPOSITION_VIBRATION_XML - + "</constant>"; - - assertParseCustomizationsFails(xmlNoStartConstantTag, hasConfigFile, hasRes); - assertParseCustomizationsFails(xmlNoEndConstantTag, hasConfigFile, hasRes); - assertParseCustomizationsFails(xmlNoStartCustomizationTag, hasConfigFile, hasRes); - assertParseCustomizationsFails(xmlNoEndCustomizationTag, hasConfigFile, hasRes); + assertThat(getEffectForSource(/* effectId= */ -10, customizationSource, customization)) + .isNull(); } @Test - @Parameters(method = "hapticFeedbackCustomizationTestArguments") - public void testParseCustomizations_badVibrationXml_throwsException( - boolean hasConfigFile, boolean hasRes) throws Exception { - String xmlBad1 = "<haptic-feedback-constants>" + public void testParseCustomizations_badVibrationXml_notLoaded( + @TestParameter CustomizationSource customizationSource) throws Exception { + // Case#1 - bad opening tag <bad-vibration-effect> + String xmlBadTag = "<haptic-feedback-constants>" + "<constant id=\"10\">" + "<bad-vibration-effect></bad-vibration-effect>" + "</constant>" + "</haptic-feedback-constants>"; - String xmlBad2 = "<haptic-feedback-constants>" + HapticFeedbackCustomization customizationBadTag = createCustomizationForSource( + xmlBadTag, customizationSource); + // Case#2 - bad attribute "name" for tag <predefined-effect> + String xmlBadEffectName = "<haptic-feedback-constants>" + "<constant id=\"10\">" + "<vibration-effect><predefined-effect name=\"bad-effect\"/></vibration-effect>" + "</constant>" + "</haptic-feedback-constants>"; - String xmlBad3 = "<haptic-feedback-constants>" + clearFileAndResourceSetup(); + HapticFeedbackCustomization customizationBadEffectName = createCustomizationForSource( + xmlBadEffectName, customizationSource); + // Case#3 - miss "</vibration-select>" + String xmlBadEffectNameAndMissingCloseTag = "<haptic-feedback-constants>" + "<constant id=\"10\">" + "<vibration-select>" + "<vibration-effect><predefined-effect name=\"bad-effect\"/></vibration-effect>" + "</constant>" + "</haptic-feedback-constants>"; - String xmlBad4 = "<haptic-feedback-constants>" + clearFileAndResourceSetup(); + HapticFeedbackCustomization customizationBadEffectNameAndMissingCloseTag = + createCustomizationForSource(xmlBadEffectNameAndMissingCloseTag, + customizationSource); + // Case#4 - miss "<vibration-select>" + String xmlBadEffectNameAndMissingOpenTag = "<haptic-feedback-constants>" + "<constant id=\"10\">" + "<vibration-effect><predefined-effect name=\"bad-effect\"/></vibration-effect>" + "</vibration-select>" + "</constant>" + "</haptic-feedback-constants>"; - - assertParseCustomizationsFails(xmlBad1, hasConfigFile, hasRes); - assertParseCustomizationsFails(xmlBad2, hasConfigFile, hasRes); - assertParseCustomizationsFails(xmlBad3, hasConfigFile, hasRes); - assertParseCustomizationsFails(xmlBad4, hasConfigFile, hasRes); + clearFileAndResourceSetup(); + HapticFeedbackCustomization customizationBadEffectNameAndMissingOpenTag = + createCustomizationForSource(xmlBadEffectNameAndMissingOpenTag, + customizationSource); + + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, + customizationBadTag)).isNull(); + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, + customizationBadEffectName)).isNull(); + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, + customizationBadEffectNameAndMissingCloseTag)).isNull(); + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, + customizationBadEffectNameAndMissingOpenTag)).isNull(); } @Test - @Parameters(method = "hapticFeedbackCustomizationTestArguments") - public void testParseCustomizations_badConstantAttribute_throwsException( - boolean hasConfigFile, boolean hasRes) throws Exception { - String xmlBadConstantAttribute1 = "<haptic-feedback-constants>" + public void testParseCustomizations_badConstantAttribute_notLoaded( + @TestParameter CustomizationSource customizationSource) throws Exception { + // Case#1 - bad attribute id for tag <constant> + String xmlBadConstantIdAttribute = "<haptic-feedback-constants>" + "<constant iddddd=\"10\">" + COMPOSITION_VIBRATION_XML + "</constant>" + "</haptic-feedback-constants>"; - String xmlBadConstantAttribute2 = "<haptic-feedback-constants>" + HapticFeedbackCustomization customizationBadConstantIdAttribute = + createCustomizationForSource(xmlBadConstantIdAttribute, customizationSource); + // Case#2 - unexpected attribute "unwanted" for tag <constant> + String xmlUnwantedConstantAttribute = "<haptic-feedback-constants>" + "<constant id=\"10\" unwanted-attr=\"1\">" + COMPOSITION_VIBRATION_XML + "</constant>" + "</haptic-feedback-constants>"; + clearFileAndResourceSetup(); + HapticFeedbackCustomization customizationUnwantedConstantAttribute = + createCustomizationForSource(xmlUnwantedConstantAttribute, customizationSource); - assertParseCustomizationsFails(xmlBadConstantAttribute1, hasConfigFile, hasRes); - assertParseCustomizationsFails(xmlBadConstantAttribute2, hasConfigFile, hasRes); + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, + customizationBadConstantIdAttribute)).isNull(); + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, + customizationUnwantedConstantAttribute)).isNull(); } @Test - @Parameters(method = "hapticFeedbackCustomizationTestArguments") - public void testParseCustomizations_duplicateEffects_throwsException( - boolean hasConfigFile, boolean hasRes) throws Exception { + public void testParseCustomizations_duplicateEffects_notLoaded( + @TestParameter CustomizationSource customizationSource) throws Exception { String xmlDuplicateEffect = "<haptic-feedback-constants>" + "<constant id=\"10\">" + COMPOSITION_VIBRATION_XML + "</constant>" + "<constant id=\"10\">" - + PREDEFINED_VIBRATION_XML + + PREDEFINED_VIBRATION_CLICK_XML + "</constant>" + "<constant id=\"11\">" - + PREDEFINED_VIBRATION_XML + + PREDEFINED_VIBRATION_CLICK_XML + "</constant>" + "</haptic-feedback-constants>"; + HapticFeedbackCustomization customization = createCustomizationForSource(xmlDuplicateEffect, + customizationSource); - assertParseCustomizationsFails(xmlDuplicateEffect, hasConfigFile, hasRes); + assertThat(getEffectForSource(/* effectId= */ 10, customizationSource, customization)) + .isNull(); + assertThat(getEffectForSource(/* effectId= */ 11, customizationSource, customization)) + .isNull(); } - private void assertParseCustomizationsSucceeds(String xml, - SparseArray<VibrationEffect> expectedCustomizations, boolean hasConfigFile, - boolean hasRes) throws Exception { - setupParseCustomizations(xml, hasConfigFile, hasRes); - assertThat(expectedCustomizations.contentEquals( - HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorInfoMock))) - .isTrue(); + @Test + public void testParseCustomizations_withDifferentCustomizations_loadsCorrectOne() + throws Exception { + String xmlBaseCustomization = "<haptic-feedback-constants>" + + "<constant id=\"10\">" + + COMPOSITION_VIBRATION_XML + + "</constant>" + + "<constant id=\"14\">" + + "<vibration-select>" + + WAVEFORM_VIBRATION_XML + + "</vibration-select>" + + "</constant>" + + "</haptic-feedback-constants>"; + String xmlRotaryInputCustomization = "<haptic-feedback-constants>" + + "<constant id=\"10\">" + + "<vibration-select>" + + PREDEFINED_VIBRATION_CLICK_XML + + COMPOSITION_VIBRATION_XML + + "</vibration-select>" + + "</constant>" + + "</haptic-feedback-constants>"; + String xmlTouchScreenInputCustomization = "<haptic-feedback-constants>" + + "<constant id=\"10\">" + + PREDEFINED_VIBRATION_TICK_XML + + "</constant>" + + "</haptic-feedback-constants>"; + setupCustomizations(xmlBaseCustomization, CustomizationSource.DEVICE_RESOURCE); + setupCustomizations(xmlRotaryInputCustomization, + CustomizationSource.DEVICE_RESOURCE_INPUT_ROTARY); + HapticFeedbackCustomization customization = createCustomizationForSource( + xmlTouchScreenInputCustomization, + CustomizationSource.DEVICE_RESOURCE_INPUT_TOUCHSCREEN); + + // Matching customizations. + assertThat(customization.getEffect(/* effectId= */ 10)).isEqualTo(COMPOSITION_VIBRATION); + assertThat(customization.getEffect(/* effectId= */ 14)).isEqualTo(WAVEFORM_VIBRATION); + assertThat(customization.getEffect(/* effectId= */ 10, + InputDevice.SOURCE_ROTARY_ENCODER)).isEqualTo(PREDEFINED_VIBRATION_CLICK); + assertThat(customization.getEffect(/* effectId= */ 10, + InputDevice.SOURCE_TOUCHSCREEN)).isEqualTo(PREDEFINED_VIBRATION_TICK); + // Missing from input source customization xml. Fallback to base. + assertThat(customization.getEffect(/* effectId= */ 14, + InputDevice.SOURCE_ROTARY_ENCODER)).isEqualTo(WAVEFORM_VIBRATION); + assertThat(customization.getEffect(/* effectId= */ 14, + InputDevice.SOURCE_TOUCHSCREEN)).isEqualTo(WAVEFORM_VIBRATION); } - private void assertParseCustomizationsFails(String xml, boolean hasConfigFile, boolean hasRes) + @Test + public void testParseCustomizations_customizationsFromConfigFileAndRes_preferConfigFile() throws Exception { - setupParseCustomizations(xml, hasConfigFile, hasRes); - assertThrows("Expected haptic feedback customization to fail", - CustomizationParserException.class, - () -> HapticFeedbackCustomization.loadVibrations( - mResourcesMock, mVibratorInfoMock)); + String xmlConfigFileCustomization = "<haptic-feedback-constants>" + + "<constant id=\"10\">" + + COMPOSITION_VIBRATION_XML + + "</constant>" + + "</haptic-feedback-constants>"; + String xmlResourceCustomization = "<haptic-feedback-constants>" + + "<constant id=\"10\">" + + "<vibration-select>" + + PREDEFINED_VIBRATION_CLICK_XML + + "</vibration-select>" + + "</constant>" + + "<constant id=\"14\">" + + "<vibration-select>" + + WAVEFORM_VIBRATION_XML + + "</vibration-select>" + + "</constant>" + + "</haptic-feedback-constants>"; + setupCustomizations(xmlConfigFileCustomization, CustomizationSource.DEVICE_CONFIG_FILE); + HapticFeedbackCustomization customization = createCustomizationForSource( + xmlResourceCustomization, CustomizationSource.DEVICE_RESOURCE); + + // When config file and resource customizations are both available. Load the config file + // Customization. + assertThat(customization.getEffect(/* effectId= */ 10)).isEqualTo(COMPOSITION_VIBRATION); + assertThat(customization.getEffect(/* effectId= */ 14)).isNull(); } - private void setupParseCustomizations(String xml, boolean hasConfigFile, boolean hasRes) + private HapticFeedbackCustomization createCustomizationForSource(String xml, + CustomizationSource customizationSource) throws Exception { + setupCustomizations(xml, customizationSource); + return new HapticFeedbackCustomization(mResourcesMock, mVibratorInfoMock); + } + + private void setupCustomizations(String xml, CustomizationSource customizationSource) throws Exception { - clearFileAndResourceSetup(); - if (hasConfigFile) { - setupCustomizationFile(xml); - } - if (hasRes) { - setupCustomizationResource(xml); + switch (customizationSource) { + case DEVICE_CONFIG_FILE -> setupCustomizationFile(xml); + case DEVICE_RESOURCE -> setupCustomizationResource(xml, haptic_feedback_customization); + case DEVICE_RESOURCE_INPUT_ROTARY -> setupCustomizationResource(xml, + haptic_feedback_customization_source_rotary_encoder); + case DEVICE_RESOURCE_INPUT_TOUCHSCREEN -> setupCustomizationResource(xml, + haptic_feedback_customization_source_touchscreen); } } - private void clearFileAndResourceSetup() { - when(mResourcesMock.getString(R.string.config_hapticFeedbackCustomizationFile)) - .thenReturn(null); - when(mResourcesMock.getXml(haptic_feedback_customization)).thenReturn(null); + private void setupCustomizationResource(String xml, int xmlResId) throws Exception { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + mSetFlagsRule.enableFlags(FLAG_LOAD_HAPTIC_FEEDBACK_VIBRATION_CUSTOMIZATION_FROM_RESOURCES); + doReturn(FakeXmlResourceParser.fromXml(xml)).when(mResourcesMock).getXml(xmlResId); } private void setupCustomizationFile(String xml) throws Exception { @@ -498,15 +590,34 @@ public class HapticFeedbackCustomizationTest { } private void setCustomizationFilePath(String path) { - when(mResourcesMock.getString(R.string.config_hapticFeedbackCustomizationFile)) - .thenReturn(path); + doReturn(path).when(mResourcesMock) + .getString(R.string.config_hapticFeedbackCustomizationFile); + } + + private void clearFileAndResourceSetup() { + doThrow(new Resources.NotFoundException()).when(mResourcesMock) + .getString(R.string.config_hapticFeedbackCustomizationFile); + doThrow(new Resources.NotFoundException()).when(mResourcesMock) + .getXml(haptic_feedback_customization); + doThrow(new Resources.NotFoundException()).when(mResourcesMock) + .getXml(haptic_feedback_customization_source_rotary_encoder); + doThrow(new Resources.NotFoundException()).when(mResourcesMock) + .getXml(haptic_feedback_customization_source_touchscreen); } - private void setupCustomizationResource(String xml) throws Exception { - mSetFlagsRule.enableFlags( - Flags.FLAG_LOAD_HAPTIC_FEEDBACK_VIBRATION_CUSTOMIZATION_FROM_RESOURCES); - when(mResourcesMock.getXml(haptic_feedback_customization)) - .thenReturn(FakeXmlResourceParser.fromXml(xml)); + @Nullable + private VibrationEffect getEffectForSource(int effectId, + CustomizationSource customizationSource, + HapticFeedbackCustomization hapticFeedbackCustomization) { + return switch (customizationSource) { + case DEVICE_CONFIG_FILE, DEVICE_RESOURCE -> hapticFeedbackCustomization.getEffect( + effectId); + case DEVICE_RESOURCE_INPUT_ROTARY -> hapticFeedbackCustomization.getEffect(effectId, + InputDevice.SOURCE_ROTARY_ENCODER); + case DEVICE_RESOURCE_INPUT_TOUCHSCREEN -> hapticFeedbackCustomization.getEffect( + effectId, + InputDevice.SOURCE_TOUCHSCREEN); + }; } private void makeSupported(VibrationEffect... effects) { diff --git a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java index 8797e63dab27..6076d3318c40 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java @@ -21,16 +21,21 @@ import static android.os.VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSIT import static android.os.VibrationAttributes.USAGE_IME_FEEDBACK; import static android.os.VibrationAttributes.USAGE_TOUCH; import static android.os.VibrationEffect.Composition.PRIMITIVE_CLICK; +import static android.os.VibrationEffect.Composition.PRIMITIVE_QUICK_RISE; +import static android.os.VibrationEffect.Composition.PRIMITIVE_THUD; import static android.os.VibrationEffect.Composition.PRIMITIVE_TICK; import static android.os.VibrationEffect.EFFECT_CLICK; import static android.os.VibrationEffect.EFFECT_TEXTURE_TICK; import static android.os.VibrationEffect.EFFECT_TICK; +import static android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED; import static android.view.HapticFeedbackConstants.BIOMETRIC_CONFIRM; import static android.view.HapticFeedbackConstants.BIOMETRIC_REJECT; import static android.view.HapticFeedbackConstants.CLOCK_TICK; import static android.view.HapticFeedbackConstants.CONTEXT_CLICK; +import static android.view.HapticFeedbackConstants.DRAG_START; import static android.view.HapticFeedbackConstants.KEYBOARD_RELEASE; import static android.view.HapticFeedbackConstants.KEYBOARD_TAP; +import static android.view.HapticFeedbackConstants.NO_HAPTICS; import static android.view.HapticFeedbackConstants.SAFE_MODE_ENABLED; import static android.view.HapticFeedbackConstants.SCROLL_ITEM_FOCUS; import static android.view.HapticFeedbackConstants.SCROLL_LIMIT; @@ -42,6 +47,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.Mockito.when; +import android.annotation.NonNull; import android.content.Context; import android.content.res.Resources; import android.hardware.vibrator.IVibrator; @@ -52,11 +58,13 @@ import android.platform.test.flag.junit.SetFlagsRule; import android.util.AtomicFile; import android.util.SparseArray; import android.view.HapticFeedbackConstants; +import android.view.InputDevice; import androidx.test.InstrumentationRegistry; import com.android.internal.R; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; @@ -75,6 +83,11 @@ public class HapticFeedbackVibrationProviderTest { VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK, 0.2497f).compose(); private static final VibrationEffect PRIMITIVE_CLICK_EFFECT = VibrationEffect.startComposition().addPrimitive(PRIMITIVE_CLICK, 0.3497f).compose(); + private static final VibrationEffect PRIMITIVE_THUD_EFFECT = + VibrationEffect.startComposition().addPrimitive(PRIMITIVE_THUD, 0.5497f).compose(); + private static final VibrationEffect PRIMITIVE_QUICK_RISE_EFFECT = + VibrationEffect.startComposition().addPrimitive(PRIMITIVE_QUICK_RISE, + 0.6497f).compose(); private static final int[] SCROLL_FEEDBACK_CONSTANTS = new int[] {SCROLL_ITEM_FOCUS, SCROLL_LIMIT, SCROLL_TICK}; @@ -90,45 +103,52 @@ public class HapticFeedbackVibrationProviderTest { @Mock private Resources mResourcesMock; - @Test - public void testNonExistentCustomization_useDefault() throws Exception { - // No customization file is set. - HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); - - assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK)) - .isEqualTo(VibrationEffect.get(EFFECT_TICK)); - - // The customization file specifies no customization. - setupCustomizationFile("<haptic-feedback-constants></haptic-feedback-constants>"); - hapticProvider = createProviderWithDefaultCustomizations(); - - assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK)) - .isEqualTo(VibrationEffect.get(EFFECT_TICK)); + @Before + public void setUp() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); } @Test - public void testExceptionParsingCustomizations_useDefault() throws Exception { - setupCustomizationFile("<bad-xml></bad-xml>"); - HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); - - assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK)) - .isEqualTo(VibrationEffect.get(EFFECT_TICK)); + public void testNonExistentCustomization_useDefault() { + HapticFeedbackVibrationProvider provider = createProviderWithoutCustomizations(); + + // No customization for `CLOCK_TICK`, so the default vibration is used. + assertThat(provider.getVibration(CLOCK_TICK)).isEqualTo( + VibrationEffect.get(EFFECT_TEXTURE_TICK)); + assertThat(provider.getVibration(CLOCK_TICK, + InputDevice.SOURCE_ROTARY_ENCODER)).isEqualTo( + VibrationEffect.get(EFFECT_TEXTURE_TICK)); + assertThat(provider.getVibration(CLOCK_TICK, InputDevice.SOURCE_TOUCHSCREEN)) + .isEqualTo(VibrationEffect.get(EFFECT_TEXTURE_TICK)); } @Test - public void testUseValidCustomizedVibration() throws Exception { - mockVibratorPrimitiveSupport(PRIMITIVE_CLICK); + public void testUseValidCustomizedVibration() { + mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK, PRIMITIVE_THUD, + PRIMITIVE_QUICK_RISE); SparseArray<VibrationEffect> customizations = new SparseArray<>(); customizations.put(CONTEXT_CLICK, PRIMITIVE_CLICK_EFFECT); - - HapticFeedbackVibrationProvider hapticProvider = createProvider(customizations); - - // The override for `CONTEXT_CLICK` is used. - assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK)) + SparseArray<VibrationEffect> customizationsRotary = new SparseArray<>(); + customizationsRotary.put(CONTEXT_CLICK, PRIMITIVE_TICK_EFFECT); + customizationsRotary.put(DRAG_START, PRIMITIVE_QUICK_RISE_EFFECT); + SparseArray<VibrationEffect> customizationsTouchScreen = new SparseArray<>(); + customizationsTouchScreen.put(CONTEXT_CLICK, PRIMITIVE_THUD_EFFECT); + customizationsTouchScreen.put(DRAG_START, PRIMITIVE_CLICK_EFFECT); + HapticFeedbackVibrationProvider provider = createProvider(customizations, + customizationsRotary, customizationsTouchScreen); + + // The customization for `CONTEXT_CLICK`. + assertThat(provider.getVibration(CONTEXT_CLICK)) .isEqualTo(PRIMITIVE_CLICK_EFFECT); - // `CLOCK_TICK` has no override, so the default vibration is used. - assertThat(hapticProvider.getVibrationForHapticFeedback(CLOCK_TICK)) - .isEqualTo(VibrationEffect.get(EFFECT_TEXTURE_TICK)); + assertThat(provider.getVibration(CONTEXT_CLICK, + InputDevice.SOURCE_ROTARY_ENCODER)).isEqualTo(PRIMITIVE_TICK_EFFECT); + assertThat(provider.getVibration(CONTEXT_CLICK, + InputDevice.SOURCE_TOUCHSCREEN)).isEqualTo(PRIMITIVE_THUD_EFFECT); + // The customization for `DRAG_START`. + assertThat(provider.getVibration(DRAG_START, + InputDevice.SOURCE_ROTARY_ENCODER)).isEqualTo(PRIMITIVE_QUICK_RISE_EFFECT); + assertThat(provider.getVibration(DRAG_START, + InputDevice.SOURCE_TOUCHSCREEN)).isEqualTo(PRIMITIVE_CLICK_EFFECT); } @Test @@ -140,92 +160,151 @@ public class HapticFeedbackVibrationProviderTest { + "</constant>" + "</haptic-feedback-constants>"; setupCustomizationFile(xml); - - HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); + HapticFeedbackVibrationProvider provider = createProviderWithoutCustomizations(); // The override for `CONTEXT_CLICK` is not used because the vibration is not supported. - assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK)) + assertThat(provider.getVibration(CONTEXT_CLICK)) .isEqualTo(VibrationEffect.get(EFFECT_TICK)); // `CLOCK_TICK` has no override, so the default vibration is used. - assertThat(hapticProvider.getVibrationForHapticFeedback(CLOCK_TICK)) + assertThat(provider.getVibration(CLOCK_TICK)) .isEqualTo(VibrationEffect.get(EFFECT_TEXTURE_TICK)); } @Test - public void testHapticTextDisabled_noVibrationReturnedForTextHandleMove() throws Exception { + public void testHapticTextDisabled_noVibrationReturnedForTextHandleMove() { mockHapticTextSupport(false); mockVibratorPrimitiveSupport(PRIMITIVE_CLICK); SparseArray<VibrationEffect> customizations = new SparseArray<>(); customizations.put(TEXT_HANDLE_MOVE, PRIMITIVE_CLICK_EFFECT); + HapticFeedbackVibrationProvider provider = createProvider( + /* customizations= */ customizations, + /* customizationsForRotary= */ customizations, + /* customizationsForTouchScreen= */ customizations); // Test with a customization available for `TEXT_HANDLE_MOVE`. - HapticFeedbackVibrationProvider hapticProvider = createProvider(customizations); - - assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE)).isNull(); + assertThat(provider.getVibration(TEXT_HANDLE_MOVE)).isNull(); + assertThat(provider.getVibration(TEXT_HANDLE_MOVE, + InputDevice.SOURCE_ROTARY_ENCODER)).isNull(); + assertThat( + provider.getVibration(TEXT_HANDLE_MOVE, InputDevice.SOURCE_TOUCHSCREEN)).isNull(); // Test with no customization available for `TEXT_HANDLE_MOVE`. - hapticProvider = createProvider(/* customizations= */ null); + provider = createProviderWithoutCustomizations(); - assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE)).isNull(); + assertThat(provider.getVibration(TEXT_HANDLE_MOVE)).isNull(); + assertThat(provider.getVibration(TEXT_HANDLE_MOVE, + InputDevice.SOURCE_ROTARY_ENCODER)).isNull(); + assertThat( + provider.getVibration(TEXT_HANDLE_MOVE, InputDevice.SOURCE_TOUCHSCREEN)).isNull(); } @Test - public void testHapticTextEnabled_vibrationReturnedForTextHandleMove() throws Exception { + public void testHapticTextEnabled_vibrationReturnedForTextHandleMove() { mockHapticTextSupport(true); - mockVibratorPrimitiveSupport(PRIMITIVE_CLICK); + mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_THUD, PRIMITIVE_TICK); SparseArray<VibrationEffect> customizations = new SparseArray<>(); customizations.put(TEXT_HANDLE_MOVE, PRIMITIVE_CLICK_EFFECT); - + SparseArray<VibrationEffect> customizationsByRotary = new SparseArray<>(); + customizationsByRotary.put(TEXT_HANDLE_MOVE, PRIMITIVE_TICK_EFFECT); + SparseArray<VibrationEffect> customizationsByTouchScreen = new SparseArray<>(); + customizationsByTouchScreen.put(TEXT_HANDLE_MOVE, PRIMITIVE_THUD_EFFECT); // Test with a customization available for `TEXT_HANDLE_MOVE`. - HapticFeedbackVibrationProvider hapticProvider = createProvider(customizations); + HapticFeedbackVibrationProvider provider = createProvider(customizations, + customizationsByRotary, customizationsByTouchScreen); - assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE)) - .isEqualTo(PRIMITIVE_CLICK_EFFECT); + assertThat(provider.getVibration(TEXT_HANDLE_MOVE)).isEqualTo(PRIMITIVE_CLICK_EFFECT); + assertThat(provider.getVibration(TEXT_HANDLE_MOVE, + InputDevice.SOURCE_ROTARY_ENCODER)).isEqualTo(PRIMITIVE_TICK_EFFECT); + assertThat(provider.getVibration(TEXT_HANDLE_MOVE, InputDevice.SOURCE_TOUCHSCREEN)) + .isEqualTo(PRIMITIVE_THUD_EFFECT); // Test with no customization available for `TEXT_HANDLE_MOVE`. - hapticProvider = createProvider(/* customizations= */ null); - - assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE)) + provider = createProviderWithoutCustomizations(); + + assertThat(provider.getVibration(TEXT_HANDLE_MOVE)).isEqualTo( + VibrationEffect.get(EFFECT_TEXTURE_TICK)); + assertThat(provider.getVibration(TEXT_HANDLE_MOVE, + InputDevice.SOURCE_ROTARY_ENCODER)).isEqualTo( + VibrationEffect.get(EFFECT_TEXTURE_TICK)); + assertThat(provider.getVibration(TEXT_HANDLE_MOVE, InputDevice.SOURCE_TOUCHSCREEN)) .isEqualTo(VibrationEffect.get(EFFECT_TEXTURE_TICK)); } @Test - public void testValidCustomizationPresentForSafeModeEnabled_usedRegardlessOfVibrationResource() - throws Exception { - mockSafeModeEnabledVibration(10, 20, 30, 40); - mockVibratorPrimitiveSupport(PRIMITIVE_CLICK); + public void testFeedbackConstantNoHapticEffect_noVibrationRegardlessCustomizations() { + mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_THUD, PRIMITIVE_TICK); SparseArray<VibrationEffect> customizations = new SparseArray<>(); - customizations.put(SAFE_MODE_ENABLED, PRIMITIVE_CLICK_EFFECT); - - HapticFeedbackVibrationProvider hapticProvider = createProvider(customizations); + customizations.put(NO_HAPTICS, PRIMITIVE_CLICK_EFFECT); + SparseArray<VibrationEffect> customizationsByRotary = new SparseArray<>(); + customizationsByRotary.put(NO_HAPTICS, PRIMITIVE_TICK_EFFECT); + SparseArray<VibrationEffect> customizationsByTouchScreen = new SparseArray<>(); + customizationsByTouchScreen.put(NO_HAPTICS, PRIMITIVE_THUD_EFFECT); + HapticFeedbackVibrationProvider provider = createProvider(customizations, + customizationsByRotary, customizationsByTouchScreen); + + // Whatever customization set to NO_HAPTICS, no vibration happens. + assertThat(provider.getVibration(NO_HAPTICS)).isNull(); + assertThat(provider.getVibration(NO_HAPTICS, InputDevice.SOURCE_ROTARY_ENCODER)).isNull(); + assertThat(provider.getVibration(NO_HAPTICS, InputDevice.SOURCE_TOUCHSCREEN)).isNull(); + } - assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED)) - .isEqualTo(PRIMITIVE_CLICK_EFFECT); + @Test + public void testValidCustomizationPresentForSafeModeEnabled_usedRegardlessOfVibrationResource() { + mockSafeModeEnabledVibration(10, 20, 30, 40); + mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK, PRIMITIVE_THUD); + SparseArray<VibrationEffect> safeModeCustomizations = new SparseArray<>(); + safeModeCustomizations.put(SAFE_MODE_ENABLED, PRIMITIVE_CLICK_EFFECT); + SparseArray<VibrationEffect> safeModeCustomizationsByRotary = new SparseArray<>(); + safeModeCustomizationsByRotary.put(SAFE_MODE_ENABLED, PRIMITIVE_THUD_EFFECT); + SparseArray<VibrationEffect> safeModeCustomizationsByTouchScreen = new SparseArray<>(); + safeModeCustomizationsByTouchScreen.put(SAFE_MODE_ENABLED, PRIMITIVE_TICK_EFFECT); + HapticFeedbackVibrationProvider provider = + createProvider(safeModeCustomizations, safeModeCustomizationsByRotary, + safeModeCustomizationsByTouchScreen); + + assertThat(provider.getVibration(SAFE_MODE_ENABLED)).isEqualTo(PRIMITIVE_CLICK_EFFECT); + assertThat(provider.getVibration(SAFE_MODE_ENABLED, InputDevice.SOURCE_ROTARY_ENCODER)) + .isEqualTo(PRIMITIVE_THUD_EFFECT); + assertThat(provider.getVibration(SAFE_MODE_ENABLED, InputDevice.SOURCE_TOUCHSCREEN)) + .isEqualTo(PRIMITIVE_TICK_EFFECT); + // Resource changed mockSafeModeEnabledVibration(null); - hapticProvider = createProvider(customizations); - - assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED)) - .isEqualTo(PRIMITIVE_CLICK_EFFECT); + provider = + createProvider(safeModeCustomizations, safeModeCustomizationsByRotary, + safeModeCustomizationsByTouchScreen); + + assertThat(provider.getVibration(SAFE_MODE_ENABLED)).isEqualTo(PRIMITIVE_CLICK_EFFECT); + assertThat(provider.getVibration(SAFE_MODE_ENABLED, InputDevice.SOURCE_ROTARY_ENCODER)) + .isEqualTo(PRIMITIVE_THUD_EFFECT); + assertThat(provider.getVibration(SAFE_MODE_ENABLED, InputDevice.SOURCE_TOUCHSCREEN)) + .isEqualTo(PRIMITIVE_TICK_EFFECT); } @Test - public void testNoValidCustomizationPresentForSafeModeEnabled_resourceBasedVibrationUsed() - throws Exception { + public void testNoValidCustomizationPresentForSafeModeEnabled_resourceBasedVibrationUsed() { mockSafeModeEnabledVibration(10, 20, 30, 40); - HapticFeedbackVibrationProvider hapticProvider = createProvider(/* customizations= */ null); - - assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED)) - .isEqualTo(VibrationEffect.createWaveform(new long[] {10, 20, 30, 40}, -1)); + HapticFeedbackVibrationProvider provider = createProviderWithoutCustomizations(); + + assertThat(provider.getVibration(SAFE_MODE_ENABLED)) + .isEqualTo(VibrationEffect.createWaveform(new long[]{10, 20, 30, 40}, -1)); + assertThat(provider.getVibration(SAFE_MODE_ENABLED, InputDevice.SOURCE_ROTARY_ENCODER)) + .isEqualTo(VibrationEffect.createWaveform(new long[]{10, 20, 30, 40}, -1)); + assertThat(provider.getVibration(SAFE_MODE_ENABLED, InputDevice.SOURCE_TOUCHSCREEN)) + .isEqualTo(VibrationEffect.createWaveform(new long[]{10, 20, 30, 40}, -1)); } @Test - public void testNoValidCustomizationAndResourcePresentForSafeModeEnabled_noVibrationUsed() - throws Exception { + public void testNoValidCustomizationAndResourcePresentForSafeModeEnabled_noVibrationUsed() { mockSafeModeEnabledVibration(null); - HapticFeedbackVibrationProvider hapticProvider = createProvider(/* customizations= */ null); - - assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED)).isNull(); + HapticFeedbackVibrationProvider provider = createProviderWithoutCustomizations(); + + assertThat(provider.getVibration(SAFE_MODE_ENABLED)).isNull(); + assertThat(provider.getVibration(SAFE_MODE_ENABLED)).isNull(); + assertThat(provider.getVibration(SAFE_MODE_ENABLED, InputDevice.SOURCE_ROTARY_ENCODER)) + .isNull(); + assertThat(provider.getVibration(SAFE_MODE_ENABLED, InputDevice.SOURCE_TOUCHSCREEN)) + .isNull(); } @Test @@ -236,19 +315,25 @@ public class HapticFeedbackVibrationProviderTest { customizations.put(KEYBOARD_RELEASE, PRIMITIVE_TICK_EFFECT); // Test with a customization available for `KEYBOARD_TAP` & `KEYBOARD_RELEASE`. - HapticFeedbackVibrationProvider hapticProvider = createProvider(customizations); + HapticFeedbackVibrationProvider provider = createProvider(customizations); - assertThat(hapticProvider.getVibrationForHapticFeedback(KEYBOARD_TAP)) - .isEqualTo(PRIMITIVE_CLICK_EFFECT); - assertThat(hapticProvider.getVibrationForHapticFeedback(KEYBOARD_RELEASE)) - .isEqualTo(PRIMITIVE_TICK_EFFECT); + assertThat(provider.getVibration(KEYBOARD_TAP)).isEqualTo(PRIMITIVE_CLICK_EFFECT); + assertThat(provider.getVibration(KEYBOARD_RELEASE)).isEqualTo(PRIMITIVE_TICK_EFFECT); // Test with no customization available for `KEYBOARD_TAP` & `KEYBOARD_RELEASE`. - hapticProvider = createProviderWithDefaultCustomizations(); + provider = createProviderWithoutCustomizations(); - assertThat(hapticProvider.getVibrationForHapticFeedback(KEYBOARD_TAP)) + assertThat(provider.getVibration(KEYBOARD_TAP)) + .isEqualTo(VibrationEffect.get(EFFECT_CLICK, true /* fallback */)); + assertThat(provider.getVibration(KEYBOARD_RELEASE)) + .isEqualTo(VibrationEffect.get(EFFECT_TICK, false /* fallback */)); + assertThat(provider.getVibration(KEYBOARD_TAP, InputDevice.SOURCE_ROTARY_ENCODER)) + .isEqualTo(VibrationEffect.get(EFFECT_CLICK, true /* fallback */)); + assertThat(provider.getVibration(KEYBOARD_RELEASE, InputDevice.SOURCE_ROTARY_ENCODER)) + .isEqualTo(VibrationEffect.get(EFFECT_TICK, false /* fallback */)); + assertThat(provider.getVibration(KEYBOARD_TAP, InputDevice.SOURCE_TOUCHSCREEN)) .isEqualTo(VibrationEffect.get(EFFECT_CLICK, true /* fallback */)); - assertThat(hapticProvider.getVibrationForHapticFeedback(KEYBOARD_RELEASE)) + assertThat(provider.getVibration(KEYBOARD_RELEASE, InputDevice.SOURCE_TOUCHSCREEN)) .isEqualTo(VibrationEffect.get(EFFECT_TICK, false /* fallback */)); } @@ -256,25 +341,64 @@ public class HapticFeedbackVibrationProviderTest { public void testKeyboardHaptic_fixAmplitude_keyboardVibrationReturned() { mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK); mockKeyboardVibrationFixedAmplitude(KEYBOARD_VIBRATION_FIXED_AMPLITUDE); + HapticFeedbackVibrationProvider provider = createProviderWithoutCustomizations(); + + assertThat(provider.getVibration(KEYBOARD_TAP)).isEqualTo( + VibrationEffect.startComposition().addPrimitive(PRIMITIVE_CLICK, + KEYBOARD_VIBRATION_FIXED_AMPLITUDE).compose()); + assertThat(provider.getVibration(KEYBOARD_RELEASE)).isEqualTo( + VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK, + KEYBOARD_VIBRATION_FIXED_AMPLITUDE).compose()); + assertThat(provider.getVibration(KEYBOARD_TAP, + InputDevice.SOURCE_ROTARY_ENCODER)).isEqualTo( + VibrationEffect.startComposition().addPrimitive(PRIMITIVE_CLICK, + KEYBOARD_VIBRATION_FIXED_AMPLITUDE).compose()); + assertThat(provider.getVibration(KEYBOARD_RELEASE, + InputDevice.SOURCE_ROTARY_ENCODER)).isEqualTo( + VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK, + KEYBOARD_VIBRATION_FIXED_AMPLITUDE).compose()); + assertThat(provider.getVibration(KEYBOARD_TAP, + InputDevice.SOURCE_TOUCHSCREEN)).isEqualTo( + VibrationEffect.startComposition().addPrimitive(PRIMITIVE_CLICK, + KEYBOARD_VIBRATION_FIXED_AMPLITUDE).compose()); + assertThat(provider.getVibration(KEYBOARD_RELEASE, + InputDevice.SOURCE_TOUCHSCREEN)).isEqualTo( + VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK, + KEYBOARD_VIBRATION_FIXED_AMPLITUDE).compose()); + } - HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); - - assertThat(hapticProvider.getVibrationForHapticFeedback(KEYBOARD_TAP)) - .isEqualTo(VibrationEffect.startComposition() - .addPrimitive(PRIMITIVE_CLICK, KEYBOARD_VIBRATION_FIXED_AMPLITUDE) - .compose()); - assertThat(hapticProvider.getVibrationForHapticFeedback(KEYBOARD_RELEASE)) - .isEqualTo(VibrationEffect.startComposition() - .addPrimitive(PRIMITIVE_TICK, KEYBOARD_VIBRATION_FIXED_AMPLITUDE) - .compose()); + @Test + public void testKeyboardHaptic_withCustomizations_customEffectsUsed() { + mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK, PRIMITIVE_THUD, + PRIMITIVE_QUICK_RISE); + SparseArray<VibrationEffect> customizations = new SparseArray<>(); + customizations.put(KEYBOARD_TAP, PRIMITIVE_CLICK_EFFECT); + customizations.put(KEYBOARD_RELEASE, PRIMITIVE_TICK_EFFECT); + SparseArray<VibrationEffect> customizationsByRotary = new SparseArray<>(); + customizationsByRotary.put(KEYBOARD_TAP, PRIMITIVE_THUD_EFFECT); + customizationsByRotary.put(KEYBOARD_RELEASE, PRIMITIVE_QUICK_RISE_EFFECT); + SparseArray<VibrationEffect> customizationsByTouchScreen = new SparseArray<>(); + customizationsByTouchScreen.put(KEYBOARD_TAP, PRIMITIVE_QUICK_RISE_EFFECT); + customizationsByTouchScreen.put(KEYBOARD_RELEASE, PRIMITIVE_THUD_EFFECT); + HapticFeedbackVibrationProvider provider = createProvider(customizations, + customizationsByRotary, customizationsByTouchScreen); + + assertThat(provider.getVibration(KEYBOARD_TAP, InputDevice.SOURCE_ROTARY_ENCODER)) + .isEqualTo(PRIMITIVE_THUD_EFFECT); + assertThat(provider.getVibration(KEYBOARD_RELEASE, InputDevice.SOURCE_ROTARY_ENCODER)) + .isEqualTo(PRIMITIVE_QUICK_RISE_EFFECT); + assertThat(provider.getVibration(KEYBOARD_TAP, InputDevice.SOURCE_TOUCHSCREEN)) + .isEqualTo(PRIMITIVE_QUICK_RISE_EFFECT); + assertThat(provider.getVibration(KEYBOARD_RELEASE, InputDevice.SOURCE_TOUCHSCREEN)) + .isEqualTo(PRIMITIVE_THUD_EFFECT); } @Test public void testVibrationAttribute_biometricConstants_returnsCommunicationRequestUsage() { - HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); + HapticFeedbackVibrationProvider provider = createProviderWithoutCustomizations(); for (int effectId : BIOMETRIC_FEEDBACK_CONSTANTS) { - VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( + VibrationAttributes attrs = provider.getVibrationAttributesForHapticFeedback( effectId, /* flags */ 0, /* privFlags */ 0); assertThat(attrs.getUsage()).isEqualTo(VibrationAttributes.USAGE_COMMUNICATION_REQUEST); } @@ -282,9 +406,9 @@ public class HapticFeedbackVibrationProviderTest { @Test public void testVibrationAttribute_forNotBypassingIntensitySettings() { - HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); + HapticFeedbackVibrationProvider provider = createProviderWithoutCustomizations(); - VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( + VibrationAttributes attrs = provider.getVibrationAttributesForHapticFeedback( SAFE_MODE_ENABLED, /* flags */ 0, /* privFlags */ 0); assertThat(attrs.isFlagSet(FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF)).isFalse(); @@ -292,9 +416,9 @@ public class HapticFeedbackVibrationProviderTest { @Test public void testVibrationAttribute_forByassingIntensitySettings() { - HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); + HapticFeedbackVibrationProvider provider = createProviderWithoutCustomizations(); - VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( + VibrationAttributes attrs = provider.getVibrationAttributesForHapticFeedback( SAFE_MODE_ENABLED, /* flags */ HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, /* privFlags */ 0); @@ -304,10 +428,10 @@ public class HapticFeedbackVibrationProviderTest { @Test public void testVibrationAttribute_scrollFeedback_scrollApiFlagOn_bypassInterruptPolicy() { mSetFlagsRule.enableFlags(android.view.flags.Flags.FLAG_SCROLL_FEEDBACK_API); - HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); + HapticFeedbackVibrationProvider provider = createProviderWithoutCustomizations(); for (int effectId : SCROLL_FEEDBACK_CONSTANTS) { - VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( + VibrationAttributes attrs = provider.getVibrationAttributesForHapticFeedback( effectId, /* flags */ 0, /* privFlags */ 0); assertWithMessage("Expected FLAG_BYPASS_INTERRUPTION_POLICY for effect " + effectId) .that(attrs.isFlagSet(FLAG_BYPASS_INTERRUPTION_POLICY)).isTrue(); @@ -317,10 +441,10 @@ public class HapticFeedbackVibrationProviderTest { @Test public void testVibrationAttribute_scrollFeedback_scrollApiFlagOff_noBypassInterruptPolicy() { mSetFlagsRule.disableFlags(android.view.flags.Flags.FLAG_SCROLL_FEEDBACK_API); - HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); + HapticFeedbackVibrationProvider provider = createProviderWithoutCustomizations(); for (int effectId : SCROLL_FEEDBACK_CONSTANTS) { - VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( + VibrationAttributes attrs = provider.getVibrationAttributesForHapticFeedback( effectId, /* flags */ 0, /* privFlags */ 0); assertWithMessage("Expected no FLAG_BYPASS_INTERRUPTION_POLICY for effect " + effectId) .that(attrs.isFlagSet(FLAG_BYPASS_INTERRUPTION_POLICY)).isFalse(); @@ -329,10 +453,10 @@ public class HapticFeedbackVibrationProviderTest { @Test public void testVibrationAttribute_notIme_useTouchUsage() { - HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); + HapticFeedbackVibrationProvider provider = createProviderWithoutCustomizations(); for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) { - VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( + VibrationAttributes attrs = provider.getVibrationAttributesForHapticFeedback( effectId, /* flags */ 0, /* privFlags */ 0); assertWithMessage("Expected USAGE_TOUCH for effect " + effectId) .that(attrs.getUsage()).isEqualTo(USAGE_TOUCH); @@ -341,10 +465,10 @@ public class HapticFeedbackVibrationProviderTest { @Test public void testVibrationAttribute_isIme_useImeFeedbackUsage() { - HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); + HapticFeedbackVibrationProvider provider = createProviderWithoutCustomizations(); for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) { - VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( + VibrationAttributes attrs = provider.getVibrationAttributesForHapticFeedback( effectId, /* flags */ 0, HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS); assertWithMessage("Expected USAGE_IME_FEEDBACK for effect " + effectId) @@ -354,20 +478,34 @@ public class HapticFeedbackVibrationProviderTest { @Test public void testIsRestricted_biometricConstants_returnsTrue() { - HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); + HapticFeedbackVibrationProvider provider = createProviderWithoutCustomizations(); for (int effectId : BIOMETRIC_FEEDBACK_CONSTANTS) { - assertThat(hapticProvider.isRestrictedHapticFeedback(effectId)).isTrue(); + assertThat(provider.isRestrictedHapticFeedback(effectId)).isTrue(); } } - private HapticFeedbackVibrationProvider createProviderWithDefaultCustomizations() { - return createProvider(/* customizations= */ null); + private HapticFeedbackVibrationProvider createProviderWithoutCustomizations() { + return createProvider(/* customizations= */ new SparseArray<>(), + /* customizationsRotary= */ new SparseArray<>(), + /* customizationsTouchScreen */ new SparseArray<>()); } private HapticFeedbackVibrationProvider createProvider( SparseArray<VibrationEffect> customizations) { - return new HapticFeedbackVibrationProvider(mResourcesMock, mVibratorInfo, customizations); + return createProvider(customizations, /* customizationsRotary= */ new SparseArray<>(), + /* customizationsTouchScreen */ new SparseArray<>()); + } + + private HapticFeedbackVibrationProvider createProvider( + @NonNull SparseArray<VibrationEffect> customizations, + @NonNull SparseArray<VibrationEffect> customizationsRotary, + @NonNull SparseArray<VibrationEffect> customizationsTouchScreen) { + return new HapticFeedbackVibrationProvider(mResourcesMock, mVibratorInfo, + new HapticFeedbackCustomization( + customizations, + customizationsRotary, + customizationsTouchScreen)); } private void mockVibratorPrimitiveSupport(int... supportedPrimitives) { diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java index 40135876303b..4afb56265563 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -16,6 +16,8 @@ package com.android.server.vibrator; +import static android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertArrayEquals; @@ -192,6 +194,11 @@ public class VibratorManagerServiceTest { private final Map<Integer, FakeVibratorControllerProvider> mVibratorProviders = new HashMap<>(); private final SparseArray<VibrationEffect> mHapticFeedbackVibrationMap = new SparseArray<>(); + private final SparseArray<VibrationEffect> mHapticFeedbackVibrationMapSourceRotary = + new SparseArray<>(); + private final SparseArray<VibrationEffect> mHapticFeedbackVibrationMapSourceTouchScreen = + new SparseArray<>(); + private final List<HalVibration> mPendingVibrations = new ArrayList<>(); private VibratorManagerService mService; @@ -339,8 +346,10 @@ public class VibratorManagerServiceTest { @Override HapticFeedbackVibrationProvider createHapticFeedbackVibrationProvider( Resources resources, VibratorInfo vibratorInfo) { - return new HapticFeedbackVibrationProvider( - resources, vibratorInfo, mHapticFeedbackVibrationMap); + return new HapticFeedbackVibrationProvider(resources, vibratorInfo, + new HapticFeedbackCustomization(mHapticFeedbackVibrationMap, + mHapticFeedbackVibrationMapSourceRotary, + mHapticFeedbackVibrationMapSourceTouchScreen)); } @Override @@ -1475,6 +1484,60 @@ public class VibratorManagerServiceTest { } @Test + public void performHapticFeedbackForInputDevice_doesNotRequireVibrateOrBypassPermissions() + throws Exception { + // Deny permissions that would have been required for regular vibrations, and check that + // the vibration proceed as expected to verify that haptic feedback does not need these + // permissions. + denyPermission(android.Manifest.permission.VIBRATE); + denyPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS); + denyPermission(android.Manifest.permission.MODIFY_PHONE_STATE); + denyPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING); + // Flag override to enable the scroll feedback constants to bypass interruption policies. + mSetFlagsRule.enableFlags(Flags.FLAG_SCROLL_FEEDBACK_API); + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + mHapticFeedbackVibrationMapSourceRotary.put( + HapticFeedbackConstants.SCROLL_TICK, + VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)); + mHapticFeedbackVibrationMapSourceTouchScreen.put( + HapticFeedbackConstants.DRAG_START, + VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)); + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_TICK); + VibratorManagerService service = createSystemReadyService(); + + HalVibration vibrationByRotary = + performHapticFeedbackForInputDeviceAndWaitUntilFinished( + service, HapticFeedbackConstants.SCROLL_TICK, /* inputDeviceId= */ 0, + InputDevice.SOURCE_ROTARY_ENCODER, /* always= */ true); + HalVibration vibrationByTouchScreen = + performHapticFeedbackForInputDeviceAndWaitUntilFinished( + service, HapticFeedbackConstants.DRAG_START, /* inputDeviceId= */ 0, + InputDevice.SOURCE_TOUCHSCREEN, /* always= */ true); + + List<VibrationEffectSegment> playedSegments = fakeVibrator.getAllEffectSegments(); + // 2 haptics: 1 by rotary + 1 by touch screen + assertEquals(2, playedSegments.size()); + // Verify feedback by rotary input + PrebakedSegment segmentByRotary = (PrebakedSegment) playedSegments.get(0); + assertEquals(VibrationEffect.EFFECT_CLICK, segmentByRotary.getEffectId()); + VibrationAttributes attrsByRotary = vibrationByRotary.callerInfo.attrs; + assertEquals(VibrationAttributes.USAGE_HARDWARE_FEEDBACK, attrsByRotary.getUsage()); + assertTrue(attrsByRotary.isFlagSet( + VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF)); + assertTrue(attrsByRotary.isFlagSet(VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY)); + // Verify feedback by touch screen input + PrebakedSegment segmentByTouchScreen = (PrebakedSegment) playedSegments.get(1); + assertEquals(VibrationEffect.EFFECT_TICK, segmentByTouchScreen.getEffectId()); + VibrationAttributes attrsByTouchScreen = vibrationByTouchScreen.callerInfo.attrs; + assertEquals(VibrationAttributes.USAGE_TOUCH, attrsByTouchScreen.getUsage()); + assertTrue(attrsByRotary.isFlagSet( + VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF)); + assertTrue(attrsByRotary.isFlagSet(VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY)); + } + + @Test public void performHapticFeedback_restrictedConstantsWithoutPermission_doesNotVibrate() throws Exception { // Deny permission to vibrate with restricted constants @@ -1506,6 +1569,42 @@ public class VibratorManagerServiceTest { } @Test + public void performHapticFeedbackForInputDevice_restrictedConstantsWithoutPermission_doesNotVibrate() + throws Exception { + // Deny permission to vibrate with restricted constants + denyPermission(android.Manifest.permission.VIBRATE_SYSTEM_CONSTANTS); + mSetFlagsRule.enableFlags(Flags.FLAG_SCROLL_FEEDBACK_API); + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + // Public constant, no permission required + mHapticFeedbackVibrationMapSourceRotary.put( + HapticFeedbackConstants.CONFIRM, + VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)); + // Hidden system-only constant, permission required + mHapticFeedbackVibrationMapSourceTouchScreen.put( + HapticFeedbackConstants.BIOMETRIC_CONFIRM, + VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK)); + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setSupportedEffects( + VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_HEAVY_CLICK); + VibratorManagerService service = createSystemReadyService(); + + // This vibrates. + performHapticFeedbackForInputDeviceAndWaitUntilFinished( + service, HapticFeedbackConstants.CONFIRM, /* inputDeviceId= */ 0, + InputDevice.SOURCE_ROTARY_ENCODER, /* always= */ false); + // This doesn't. + performHapticFeedbackForInputDeviceAndWaitUntilFinished( + service, HapticFeedbackConstants.BIOMETRIC_CONFIRM, /* inputDeviceId= */ 0, + InputDevice.SOURCE_TOUCHSCREEN, /* always= */ false); + + List<VibrationEffectSegment> playedSegments = fakeVibrator.getAllEffectSegments(); + assertEquals(1, playedSegments.size()); + PrebakedSegment segment = (PrebakedSegment) playedSegments.get(0); + assertEquals(VibrationEffect.EFFECT_CLICK, segment.getEffectId()); + } + + @Test public void performHapticFeedback_restrictedConstantsWithPermission_playsVibration() throws Exception { // Grant permission to vibrate with restricted constants @@ -1539,33 +1638,95 @@ public class VibratorManagerServiceTest { } @Test + public void performHapticFeedbackForInputDevice_restrictedConstantsWithPermission_playsVibration() + throws Exception { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + // Grant permission to vibrate with restricted constants + grantPermission(android.Manifest.permission.VIBRATE_SYSTEM_CONSTANTS); + // Public constant, no permission required + mHapticFeedbackVibrationMapSourceRotary.put( + HapticFeedbackConstants.CONFIRM, + VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)); + // Hidden system-only constant, permission required + mHapticFeedbackVibrationMapSourceTouchScreen.put( + HapticFeedbackConstants.BIOMETRIC_CONFIRM, + VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK)); + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setSupportedEffects( + VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_HEAVY_CLICK); + VibratorManagerService service = createSystemReadyService(); + + performHapticFeedbackForInputDeviceAndWaitUntilFinished( + service, HapticFeedbackConstants.CONFIRM, /* inputDeviceId= */ 0, + InputDevice.SOURCE_ROTARY_ENCODER, /* always= */ false); + performHapticFeedbackForInputDeviceAndWaitUntilFinished( + service, HapticFeedbackConstants.BIOMETRIC_CONFIRM, /* inputDeviceId= */ 0, + InputDevice.SOURCE_TOUCHSCREEN, /* always= */ false); + + List<VibrationEffectSegment> playedSegments = fakeVibrator.getAllEffectSegments(); + assertEquals(2, playedSegments.size()); + assertEquals(VibrationEffect.EFFECT_CLICK, + ((PrebakedSegment) playedSegments.get(0)).getEffectId()); + assertEquals(VibrationEffect.EFFECT_HEAVY_CLICK, + ((PrebakedSegment) playedSegments.get(1)).getEffectId()); + } + + @Test public void performHapticFeedback_doesNotVibrateWhenVibratorInfoNotReady() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_SCROLL_FEEDBACK_API); + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); denyPermission(android.Manifest.permission.VIBRATE); mHapticFeedbackVibrationMap.put( HapticFeedbackConstants.KEYBOARD_TAP, VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)); + mHapticFeedbackVibrationMapSourceRotary.put( + HapticFeedbackConstants.KEYBOARD_TAP, + VibrationEffect.createPredefined(VibrationEffect.EFFECT_THUD)); + mHapticFeedbackVibrationMapSourceTouchScreen.put( + HapticFeedbackConstants.KEYBOARD_TAP, + VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)); mockVibrators(1); FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); fakeVibrator.setVibratorInfoLoadSuccessful(false); - fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK); + fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_TICK, + VibrationEffect.EFFECT_THUD); VibratorManagerService service = createService(); + // performHapticFeedback. performHapticFeedbackAndWaitUntilFinished( service, HapticFeedbackConstants.KEYBOARD_TAP, /* always= */ true); + // performHapticFeedbackForInputDevice. + performHapticFeedbackForInputDeviceAndWaitUntilFinished( + service, HapticFeedbackConstants.KEYBOARD_TAP, /* inputDeviceId= */ 0, + InputDevice.SOURCE_ROTARY_ENCODER, /* always= */ true); + performHapticFeedbackForInputDeviceAndWaitUntilFinished( + service, HapticFeedbackConstants.KEYBOARD_TAP, /* inputDeviceId= */ 0, + InputDevice.SOURCE_TOUCHSCREEN, /* always= */ true); assertTrue(fakeVibrator.getAllEffectSegments().isEmpty()); } @Test public void performHapticFeedback_doesNotVibrateForInvalidConstant() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_SCROLL_FEEDBACK_API); + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); denyPermission(android.Manifest.permission.VIBRATE); mockVibrators(1); VibratorManagerService service = createSystemReadyService(); // These are bad haptic feedback IDs, so expect no vibration played. + // Test performHapticFeedback performHapticFeedbackAndWaitUntilFinished(service, /* constant= */ -1, /* always= */ false); performHapticFeedbackAndWaitUntilFinished( service, HapticFeedbackConstants.NO_HAPTICS, /* always= */ true); + // Test performHapticFeedbackForInputDevice + performHapticFeedbackForInputDeviceAndWaitUntilFinished( + service, /* constant= */ -1, /* inputDeviceId= */ 0, + InputDevice.SOURCE_ROTARY_ENCODER, /* always= */ true); + performHapticFeedbackForInputDeviceAndWaitUntilFinished( + service, /* constant= */ -1, /* inputDeviceId= */ 0, + InputDevice.SOURCE_TOUCHSCREEN, /* always= */ true); assertTrue(mVibratorProviders.get(1).getAllEffectSegments().isEmpty()); } @@ -1582,6 +1743,17 @@ public class VibratorManagerServiceTest { } @Test + public void performHapticFeedbackForInputDevice_usesServiceAsToken() throws Exception { + VibratorManagerService service = createSystemReadyService(); + + HalVibration vibration = performHapticFeedbackForInputDeviceAndWaitUntilFinished( + service, HapticFeedbackConstants.SCROLL_TICK, /* inputDeviceId= */ 0, + InputDevice.SOURCE_ROTARY_ENCODER, /* always= */ true); + + assertTrue(vibration.callerToken == service); + } + + @Test @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) public void vibrate_vendorEffectsWithoutPermission_doesNotVibrate() throws Exception { // Deny permission to vibrate with vendor effects @@ -2761,6 +2933,21 @@ public class VibratorManagerServiceTest { return vib; } + private HalVibration performHapticFeedbackForInputDeviceAndWaitUntilFinished( + VibratorManagerService service, int constant, int inputDeviceId, int inputSource, + boolean always) throws InterruptedException { + HalVibration vib = service.performHapticFeedbackForInputDeviceInternal(UID, + Context.DEVICE_ID_DEFAULT, PACKAGE_NAME, constant, inputDeviceId, inputSource, + "some reason", service, + always ? HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING : 0 /* flags */, + 0 /* privFlags */); + if (vib != null) { + vib.waitForEnd(); + } + + return vib; + } + private HalVibration vibrateAndWaitUntilFinished(VibratorManagerService service, VibrationEffect effect, VibrationAttributes attrs) throws InterruptedException { return vibrateAndWaitUntilFinished( 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 1423811da0c1..0d8b7208bcac 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -705,7 +705,7 @@ public class ActivityRecordTests extends WindowTestsBase { assertEquals(ORIENTATION_PORTRAIT, activity.getConfiguration().orientation); // Clear size compat. - activity.clearSizeCompatMode(); + activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); activity.ensureActivityConfiguration(); mDisplayContent.sendNewConfiguration(); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java index d7cef599358a..a7a08b25fba2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java @@ -499,7 +499,7 @@ class AppCompatActivityRobot { activity.setRequestedOrientation(screenOrientation); } // Make sure to use the provided configuration to construct the size compat fields. - activity.clearSizeCompatMode(); + activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); activity.ensureActivityConfiguration(); // Make sure the display configuration reflects the change of activity. if (activity.mDisplayContent.updateOrientation()) { diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java index 99505414b934..b6e393d7be0c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java @@ -39,10 +39,8 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS; import static com.android.server.wm.WindowContainer.POSITION_TOP; import static org.junit.Assert.assertEquals; @@ -51,9 +49,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import android.graphics.Rect; import android.os.Binder; @@ -377,41 +373,6 @@ public class AppTransitionTests extends WindowTestsBase { } @Test - public void testDelayWhileRecents() { - final DisplayContent dc = createNewDisplay(Display.STATE_ON); - doReturn(false).when(dc).onDescendantOrientationChanged(any()); - final Task task = createTask(dc); - - // Simulate activity1 launches activity2. - final ActivityRecord activity1 = createActivityRecord(task); - activity1.setVisible(true); - activity1.setVisibleRequested(false); - activity1.allDrawn = true; - final ActivityRecord activity2 = createActivityRecord(task); - activity2.setVisible(false); - activity2.setVisibleRequested(true); - activity2.allDrawn = true; - - dc.mClosingApps.add(activity1); - dc.mOpeningApps.add(activity2); - dc.prepareAppTransition(TRANSIT_OPEN); - assertTrue(dc.mAppTransition.containsTransitRequest(TRANSIT_OPEN)); - - // Wait until everything in animation handler get executed to prevent the exiting window - // from being removed during WindowSurfacePlacer Traversal. - waitUntilHandlersIdle(); - - // Start recents - doReturn(true).when(task) - .isSelfAnimating(anyInt(), eq(ANIMATION_TYPE_RECENTS)); - - dc.mAppTransitionController.handleAppTransitionReady(); - - verify(activity1, never()).commitVisibility(anyBoolean(), anyBoolean(), anyBoolean()); - verify(activity2, never()).commitVisibility(anyBoolean(), anyBoolean(), anyBoolean()); - } - - @Test public void testGetAnimationStyleResId() { // Verify getAnimationStyleResId will return as LayoutParams.windowAnimations when without // specifying window type. diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 58e919dda964..f2ea1c972b90 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1732,25 +1732,6 @@ public class DisplayContentTests extends WindowTestsBase { assertFalse(mDisplayContent.hasTopFixedRotationLaunchingApp()); } - @SetupWindows(addWindows = W_ACTIVITY) - @Test - public void testRotateSeamlesslyWithFixedRotation() { - final DisplayRotation displayRotation = mDisplayContent.getDisplayRotation(); - final ActivityRecord app = mAppWindow.mActivityRecord; - mDisplayContent.setFixedRotationLaunchingAppUnchecked(app); - mAppWindow.mAttrs.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE; - - // Use seamless rotation if the top app is rotated. - assertTrue(displayRotation.shouldRotateSeamlessly(ROTATION_0 /* oldRotation */, - ROTATION_90 /* newRotation */, false /* forceUpdate */)); - - mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(app); - - // Use normal rotation because animating recents is an intermediate state. - assertFalse(displayRotation.shouldRotateSeamlessly(ROTATION_0 /* oldRotation */, - ROTATION_90 /* newRotation */, false /* forceUpdate */)); - } - @Test public void testFixedRotationWithPip() { final DisplayContent displayContent = mDefaultDisplay; @@ -1828,49 +1809,6 @@ public class DisplayContentTests extends WindowTestsBase { assertFalse(mDisplayContent.hasTopFixedRotationLaunchingApp()); } - @Test - public void testRecentsNotRotatingWithFixedRotation() { - unblockDisplayRotation(mDisplayContent); - final DisplayRotation displayRotation = mDisplayContent.getDisplayRotation(); - // Skip freezing so the unrelated conditions in updateRotationUnchecked won't disturb. - doNothing().when(mWm).startFreezingDisplay(anyInt(), anyInt(), any(), anyInt()); - - final ActivityRecord activity = createActivityRecord(mDisplayContent); - final ActivityRecord recentsActivity = createActivityRecord(mDisplayContent); - recentsActivity.setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR); - doReturn(mock(RecentsAnimationController.class)).when(mWm).getRecentsAnimationController(); - - // Do not rotate if the recents animation is animating on top. - mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(recentsActivity); - displayRotation.setRotation((displayRotation.getRotation() + 1) % 4); - assertFalse(displayRotation.updateRotationUnchecked(false)); - - // Rotation can be updated if the recents animation is finished. - mDisplayContent.mFixedRotationTransitionListener.onFinishRecentsAnimation(); - assertTrue(displayRotation.updateRotationUnchecked(false)); - - // Rotation can be updated if the policy is not ok to animate (e.g. going to sleep). - mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(recentsActivity); - displayRotation.setRotation((displayRotation.getRotation() + 1) % 4); - ((TestWindowManagerPolicy) mWm.mPolicy).mOkToAnimate = false; - assertTrue(displayRotation.updateRotationUnchecked(false)); - - // Rotation can be updated if the recents animation is animating but it is not on top, e.g. - // switching activities in different orientations by quickstep gesture. - mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(recentsActivity); - mDisplayContent.setFixedRotationLaunchingAppUnchecked(activity); - displayRotation.setRotation((displayRotation.getRotation() + 1) % 4); - assertTrue(displayRotation.updateRotationUnchecked(false)); - - // The recents activity should not apply fixed rotation if the top activity is not opaque. - mDisplayContent.mFocusedApp = activity; - doReturn(false).when(mDisplayContent.mFocusedApp).occludesParent(); - doReturn(ROTATION_90).when(mDisplayContent).rotationForActivityInDifferentOrientation( - eq(recentsActivity)); - mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(recentsActivity); - assertFalse(recentsActivity.hasFixedRotationTransform()); - } - @EnableFlags(com.android.window.flags.Flags.FLAG_RESPECT_NON_TOP_VISIBLE_FIXED_ORIENTATION) @Test public void testRespectNonTopVisibleFixedOrientation() { diff --git a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java index d8d5729700ca..ea175a5a52b0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java @@ -24,6 +24,8 @@ import static org.junit.Assert.assertTrue; import android.graphics.PixelFormat; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; import androidx.test.filters.SmallTest; @@ -72,6 +74,7 @@ public class ImeInsetsSourceProviderTest extends WindowTestsBase { * Checks that scheduling with all the state set and manually triggering the show does succeed. */ @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testScheduleShowIme() { final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, "ime"); makeWindowVisibleAndDrawn(ime); @@ -99,6 +102,7 @@ public class ImeInsetsSourceProviderTest extends WindowTestsBase { * all the state becomes available. */ @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testScheduleShowIme_noInitialState() { final WindowState target = createWindow(null, TYPE_APPLICATION, "app"); @@ -126,6 +130,7 @@ public class ImeInsetsSourceProviderTest extends WindowTestsBase { * does continue and succeed when the runnable is started. */ @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testScheduleShowIme_delayedAfterPrepareSurfaces() { final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, "ime"); makeWindowVisibleAndDrawn(ime); @@ -158,6 +163,7 @@ public class ImeInsetsSourceProviderTest extends WindowTestsBase { * when the surface placement happens. */ @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testScheduleShowIme_delayedSurfacePlacement() { final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, "ime"); makeWindowVisibleAndDrawn(ime); diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java index 0dc56f8afc53..964264d82b65 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java @@ -32,6 +32,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.server.wm.WindowContainer.POSITION_TOP; +import static com.android.server.wm.WindowStateAnimator.HAS_DRAWN; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -202,6 +203,11 @@ public class InsetsStateControllerTest extends WindowTestsBase { getController().onImeControlTargetChanged(base); base.setRequestedVisibleTypes(ime(), ime()); getController().onRequestedVisibleTypesChanged(base, null /* statsToken */); + if (android.view.inputmethod.Flags.refactorInsetsController()) { + // to set the serverVisibility, the IME needs to be drawn and onPostLayout be called. + mImeWindow.mWinAnimator.mDrawState = HAS_DRAWN; + getController().onPostLayout(); + } // Send our spy window (app) into the system so that we can detect the invocation. final WindowState win = createWindow(null, TYPE_APPLICATION, "app"); @@ -500,6 +506,12 @@ public class InsetsStateControllerTest extends WindowTestsBase { getController().onRequestedVisibleTypesChanged(app, null /* statsToken */); assertTrue(ime.getControllableInsetProvider().getSource().isVisible()); + if (android.view.inputmethod.Flags.refactorInsetsController()) { + // The IME is only set to shown, after onPostLayout is called and all preconditions + // (serverVisible, no givenInsetsPending, etc.) are fulfilled + getController().getImeSourceProvider().onPostLayout(); + } + getController().updateAboveInsetsState(true /* notifyInsetsChange */); assertNotNull(app.getInsetsState().peekSource(ID_IME)); verify(app, atLeastOnce()).notifyInsetsChanged(); diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java deleted file mode 100644 index 63e3e5cf865a..000000000000 --- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java +++ /dev/null @@ -1,834 +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 android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; -import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; -import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; -import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.atLeast; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyNoMoreInteractions; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; -import static com.android.server.wm.RecentsAnimationController.REORDER_KEEP_IN_PLACE; -import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_ORIGINAL_POSITION; -import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_TOP; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_TOKEN_TRANSFORM; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; - -import android.content.pm.ActivityInfo; -import android.content.res.Configuration; -import android.os.Binder; -import android.os.IBinder; -import android.os.IInterface; -import android.platform.test.annotations.Presubmit; -import android.util.SparseBooleanArray; -import android.view.IRecentsAnimationRunner; -import android.view.SurfaceControl; -import android.view.WindowManager.LayoutParams; -import android.window.TaskSnapshot; - -import androidx.test.filters.SmallTest; - -import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback; - -import com.google.common.truth.Truth; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.ArrayList; - -/** - * Build/Install/Run: - * atest WmTests:RecentsAnimationControllerTest - */ -@SmallTest -@Presubmit -@RunWith(WindowTestRunner.class) -public class RecentsAnimationControllerTest extends WindowTestsBase { - - @Mock SurfaceControl mMockLeash; - @Mock SurfaceControl.Transaction mMockTransaction; - @Mock OnAnimationFinishedCallback mFinishedCallback; - @Mock IRecentsAnimationRunner mMockRunner; - @Mock RecentsAnimationController.RecentsAnimationCallbacks mAnimationCallbacks; - @Mock TaskSnapshot mMockTaskSnapshot; - private RecentsAnimationController mController; - private Task mRootHomeTask; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - doNothing().when(mWm.mRoot).performSurfacePlacement(); - when(mMockRunner.asBinder()).thenReturn(new Binder()); - mController = spy(new RecentsAnimationController(mWm, mMockRunner, mAnimationCallbacks, - DEFAULT_DISPLAY)); - mRootHomeTask = mDefaultDisplay.getDefaultTaskDisplayArea().getRootHomeTask(); - assertNotNull(mRootHomeTask); - } - - @Test - public void testRemovedBeforeStarted_expectCanceled() throws Exception { - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - AnimationAdapter adapter = mController.addAnimation(activity.getTask(), - false /* isRecentTaskInvisible */); - adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_RECENTS, - mFinishedCallback); - - // The activity doesn't contain window so the animation target cannot be created. - mController.startAnimation(); - - // Verify that the finish callback to reparent the leash is called - verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_RECENTS), eq(adapter)); - // Verify the animation canceled callback to the app was made - verify(mMockRunner).onAnimationCanceled(null /* taskIds */, null /* taskSnapshots */); - verifyNoMoreInteractionsExceptAsBinder(mMockRunner); - } - - @Test - public void testCancelAfterRemove_expectIgnored() { - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - AnimationAdapter adapter = mController.addAnimation(activity.getTask(), - false /* isRecentTaskInvisible */); - adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_RECENTS, - mFinishedCallback); - - // Remove the app window so that the animation target can not be created - activity.removeImmediately(); - mController.startAnimation(); - mController.cleanupAnimation(REORDER_KEEP_IN_PLACE); - try { - mController.cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "test"); - } catch (Exception e) { - fail("Unexpected failure when canceling animation after finishing it"); - } - } - - @Test - public void testIncludedApps_expectTargetAndVisible() { - mWm.setRecentsAnimationController(mController); - final ActivityRecord homeActivity = createHomeActivity(); - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - final ActivityRecord hiddenActivity = createActivityRecord(mDefaultDisplay); - hiddenActivity.setVisible(false); - mDefaultDisplay.getConfiguration().windowConfiguration.setRotation( - mDefaultDisplay.getRotation()); - initializeRecentsAnimationController(mController, homeActivity); - - // Ensure that we are animating the target activity as well - assertTrue(mController.isAnimatingTask(homeActivity.getTask())); - assertTrue(mController.isAnimatingTask(activity.getTask())); - assertFalse(mController.isAnimatingTask(hiddenActivity.getTask())); - } - - @Test - public void testLaunchAndStartRecents_expectTargetAndVisible() throws Exception { - mWm.setRecentsAnimationController(mController); - final ActivityRecord homeActivity = createHomeActivity(); - final Task task = createTask(mDefaultDisplay); - // Emulate that activity1 has just launched activity2, but app transition has not yet been - // executed. - final ActivityRecord activity1 = createActivityRecord(task); - activity1.setVisible(true); - activity1.setVisibleRequested(false); - activity1.addWindow(createWindowState(new LayoutParams(TYPE_BASE_APPLICATION), activity1)); - - final ActivityRecord activity2 = createActivityRecord(task); - activity2.setVisible(false); - activity2.setVisibleRequested(true); - - mDefaultDisplay.getConfiguration().windowConfiguration.setRotation( - mDefaultDisplay.getRotation()); - initializeRecentsAnimationController(mController, homeActivity); - mController.startAnimation(); - verify(mMockRunner, never()).onAnimationCanceled(null /* taskIds */, - null /* taskSnapshots */); - } - - @Test - public void testWallpaperIncluded_expectTarget() throws Exception { - mWm.setRecentsAnimationController(mController); - final ActivityRecord homeActivity = createHomeActivity(); - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1"); - activity.addWindow(win1); - final WallpaperWindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm, - mock(IBinder.class), true, mDefaultDisplay, true /* ownerCanManageAppTokens */); - spyOn(mDefaultDisplay.mWallpaperController); - doReturn(true).when(mDefaultDisplay.mWallpaperController).isWallpaperVisible(); - - mDefaultDisplay.getConfiguration().windowConfiguration.setRotation( - mDefaultDisplay.getRotation()); - initializeRecentsAnimationController(mController, homeActivity); - mController.startAnimation(); - - // Ensure that we are animating the app and wallpaper target - assertTrue(mController.isAnimatingTask(activity.getTask())); - assertTrue(mController.isAnimatingWallpaper(wallpaperWindowToken)); - } - - @Test - public void testWallpaperAnimatorCanceled_expectAnimationKeepsRunning() throws Exception { - mWm.setRecentsAnimationController(mController); - final ActivityRecord homeActivity = createHomeActivity(); - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1"); - activity.addWindow(win1); - final WallpaperWindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm, - mock(IBinder.class), true, mDefaultDisplay, true /* ownerCanManageAppTokens */); - spyOn(mDefaultDisplay.mWallpaperController); - doReturn(true).when(mDefaultDisplay.mWallpaperController).isWallpaperVisible(); - - mDefaultDisplay.getConfiguration().windowConfiguration.setRotation( - mDefaultDisplay.getRotation()); - initializeRecentsAnimationController(mController, homeActivity); - mController.startAnimation(); - - // Cancel the animation and ensure the controller is still running - wallpaperWindowToken.cancelAnimation(); - assertTrue(mController.isAnimatingTask(activity.getTask())); - assertFalse(mController.isAnimatingWallpaper(wallpaperWindowToken)); - verify(mMockRunner, never()).onAnimationCanceled(null /* taskIds */, - null /* taskSnapshots */); - } - - @Test - public void testFinish_expectTargetAndWallpaperAdaptersRemoved() { - mWm.setRecentsAnimationController(mController); - final ActivityRecord homeActivity = createHomeActivity(); - final WindowState hwin1 = createWindow(null, TYPE_BASE_APPLICATION, homeActivity, "hwin1"); - homeActivity.addWindow(hwin1); - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1"); - activity.addWindow(win1); - final WallpaperWindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm, - mock(IBinder.class), true, mDefaultDisplay, true /* ownerCanManageAppTokens */); - spyOn(mDefaultDisplay.mWallpaperController); - doReturn(true).when(mDefaultDisplay.mWallpaperController).isWallpaperVisible(); - - // Start and finish the animation - initializeRecentsAnimationController(mController, homeActivity); - mController.startAnimation(); - - assertTrue(mController.isAnimatingTask(homeActivity.getTask())); - assertTrue(mController.isAnimatingTask(activity.getTask())); - - // Reset at this point since we may remove adapters that couldn't be created - clearInvocations(mController); - mController.cleanupAnimation(REORDER_MOVE_TO_TOP); - - // Ensure that we remove the task (home & app) and wallpaper adapters - verify(mController, times(2)).removeAnimation(any()); - verify(mController, times(1)).removeWallpaperAnimation(any()); - } - - @Test - public void testDeferCancelAnimation() throws Exception { - mWm.setRecentsAnimationController(mController); - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1"); - activity.addWindow(win1); - assertEquals(activity.getTask().getTopVisibleActivity(), activity); - assertEquals(activity.findMainWindow(), win1); - - mController.addAnimation(activity.getTask(), false /* isRecentTaskInvisible */); - assertTrue(mController.isAnimatingTask(activity.getTask())); - - mController.setDeferredCancel(true /* deferred */, false /* screenshot */); - mController.cancelAnimationWithScreenshot(false /* screenshot */); - verify(mMockRunner).onAnimationCanceled(null /* taskIds */, null /* taskSnapshots */); - - // Simulate the app transition finishing - mController.mAppTransitionListener.onAppTransitionStartingLocked(0, 0); - verify(mAnimationCallbacks).onAnimationFinished(REORDER_KEEP_IN_PLACE, false); - } - - @Test - public void testDeferCancelAnimationWithScreenShot() throws Exception { - mWm.setRecentsAnimationController(mController); - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1"); - activity.addWindow(win1); - assertEquals(activity.getTask().getTopVisibleActivity(), activity); - assertEquals(activity.findMainWindow(), win1); - - RecentsAnimationController.TaskAnimationAdapter adapter = mController.addAnimation( - activity.getTask(), false /* isRecentTaskInvisible */); - assertTrue(mController.isAnimatingTask(activity.getTask())); - - spyOn(mWm.mTaskSnapshotController); - doReturn(mMockTaskSnapshot).when(mWm.mTaskSnapshotController).getSnapshot(anyInt(), - anyInt(), eq(false) /* restoreFromDisk */, eq(false) /* isLowResolution */); - mController.setDeferredCancel(true /* deferred */, true /* screenshot */); - mController.cancelAnimationWithScreenshot(true /* screenshot */); - verify(mMockRunner).onAnimationCanceled(any(int[].class) /* taskIds */, - any(TaskSnapshot[].class) /* taskSnapshots */); - - // Continue the animation (simulating a call to cleanupScreenshot()) - mController.continueDeferredCancelAnimation(); - verify(mAnimationCallbacks).onAnimationFinished(REORDER_KEEP_IN_PLACE, false); - } - - @Test - public void testShouldAnimateWhenNoCancelWithDeferredScreenshot() { - mWm.setRecentsAnimationController(mController); - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1"); - activity.addWindow(win1); - assertEquals(activity.getTask().getTopVisibleActivity(), activity); - assertEquals(activity.findMainWindow(), win1); - - mController.addAnimation(activity.getTask(), false /* isRecentTaskInvisible */); - assertTrue(mController.isAnimatingTask(activity.getTask())); - - // Assume activity transition should animate when no - // IRecentsAnimationController#setDeferCancelUntilNextTransition called. - assertFalse(mController.shouldDeferCancelWithScreenshot()); - assertTrue(activity.shouldAnimate()); - } - - @Test - public void testBinderDiedAfterCancelWithDeferredScreenshot() throws Exception { - mWm.setRecentsAnimationController(mController); - final ActivityRecord homeActivity = createHomeActivity(); - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1"); - activity.addWindow(win1); - - initializeRecentsAnimationController(mController, homeActivity); - mController.setWillFinishToHome(true); - - // Verify cancel is called with a snapshot and that we've created an overlay - spyOn(mWm.mTaskSnapshotController); - doReturn(mMockTaskSnapshot).when(mWm.mTaskSnapshotController).getSnapshot(anyInt(), - anyInt(), eq(false) /* restoreFromDisk */, eq(false) /* isLowResolution */); - mController.cancelAnimationWithScreenshot(true /* screenshot */); - verify(mMockRunner).onAnimationCanceled(any(), any()); - - // Simulate process crashing and ensure the animation is still canceled - mController.binderDied(); - verify(mAnimationCallbacks).onAnimationFinished(REORDER_KEEP_IN_PLACE, false); - } - - @Test - public void testRecentViewInFixedPortraitWhenTopAppInLandscape() { - makeDisplayPortrait(mDefaultDisplay); - unblockDisplayRotation(mDefaultDisplay); - mWm.setRecentsAnimationController(mController); - - final ActivityRecord homeActivity = createHomeActivity(); - homeActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - - final ActivityRecord landActivity = createActivityRecord(mDefaultDisplay); - landActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); - final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, landActivity, "win1"); - landActivity.addWindow(win1); - - assertEquals(landActivity.getTask().getTopVisibleActivity(), landActivity); - assertEquals(landActivity.findMainWindow(), win1); - - // Ensure that the display is in Landscape - landActivity.onDescendantOrientationChanged(landActivity); - assertEquals(Configuration.ORIENTATION_LANDSCAPE, - mDefaultDisplay.getConfiguration().orientation); - - initializeRecentsAnimationController(mController, homeActivity); - - assertTrue(mDefaultDisplay.isFixedRotationLaunchingApp(homeActivity)); - - // Check that the home app is in portrait - assertEquals(Configuration.ORIENTATION_PORTRAIT, - homeActivity.getConfiguration().orientation); - - // Home activity won't become top (return to landActivity), so the top rotated record should - // be cleared. - mController.cleanupAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION); - assertFalse(mDefaultDisplay.isFixedRotationLaunchingApp(homeActivity)); - assertFalse(mDefaultDisplay.hasTopFixedRotationLaunchingApp()); - // The transform should keep until the transition is done, so the restored configuration - // won't be sent to activity and cause unnecessary configuration change. - assertTrue(homeActivity.hasFixedRotationTransform()); - - // In real case the transition will be executed from RecentsAnimation#finishAnimation. - mDefaultDisplay.mFixedRotationTransitionListener.onAppTransitionFinishedLocked( - homeActivity.token); - assertFalse(homeActivity.hasFixedRotationTransform()); - } - - private ActivityRecord prepareFixedRotationLaunchingAppWithRecentsAnim() { - final ActivityRecord homeActivity = createHomeActivity(); - homeActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - // Add a window so it can be animated by the recents. - final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, activity, "win"); - activity.addWindow(win); - // Assume an activity is launching to different rotation. - mDefaultDisplay.setFixedRotationLaunchingApp(activity, - (mDefaultDisplay.getRotation() + 1) % 4); - - assertTrue(activity.hasFixedRotationTransform()); - assertTrue(mDefaultDisplay.isFixedRotationLaunchingApp(activity)); - - // Before the transition is done, the recents animation is triggered. - initializeRecentsAnimationController(mController, homeActivity); - assertFalse(homeActivity.hasFixedRotationTransform()); - assertTrue(mController.isAnimatingTask(activity.getTask())); - - return activity; - } - - @Test - public void testClearFixedRotationLaunchingAppAfterCleanupAnimation() { - final ActivityRecord activity = prepareFixedRotationLaunchingAppWithRecentsAnim(); - - // Simulate giving up the swipe up gesture to keep the original activity as top. - mController.cleanupAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION); - // The rotation transform should be cleared after updating orientation with display. - assertTopFixedRotationLaunchingAppCleared(activity); - - // Simulate swiping up recents (home) in different rotation. - final ActivityRecord home = mDefaultDisplay.getDefaultTaskDisplayArea().getHomeActivity(); - startRecentsInDifferentRotation(home); - - // If the recents activity becomes the top running activity (e.g. the original top activity - // is either finishing or moved to back during recents animation), the display orientation - // will be determined by it so the fixed rotation must be cleared. - activity.finishing = true; - mController.cleanupAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION); - assertTopFixedRotationLaunchingAppCleared(home); - - startRecentsInDifferentRotation(home); - // Assume recents activity becomes invisible for some reason (e.g. screen off). - home.setVisible(false); - mController.cleanupAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION); - // Although there won't be a transition finish callback, the fixed rotation must be cleared. - assertTopFixedRotationLaunchingAppCleared(home); - } - - @Test - public void testKeepFixedRotationWhenMovingRecentsToTop() { - final ActivityRecord activity = prepareFixedRotationLaunchingAppWithRecentsAnim(); - // Assume a transition animation has started running before recents animation. Then the - // activity will receive onAnimationFinished that notifies app transition finished when - // removing the recents animation of task. - activity.getTask().getAnimationSources().add(activity); - - // Simulate swiping to home/recents before the transition is done. - mController.cleanupAnimation(REORDER_MOVE_TO_TOP); - // The rotation transform should be preserved. In real case, it will be cleared by the next - // move-to-top transition. - assertTrue(activity.hasFixedRotationTransform()); - } - - @Test - public void testCheckRotationAfterCleanup() { - mWm.setRecentsAnimationController(mController); - spyOn(mDisplayContent.mFixedRotationTransitionListener); - final ActivityRecord recents = mock(ActivityRecord.class); - recents.setOverrideOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR); - doReturn(ORIENTATION_PORTRAIT).when(recents) - .getRequestedConfigurationOrientation(anyBoolean()); - mDisplayContent.mFixedRotationTransitionListener.onStartRecentsAnimation(recents); - - // Rotation update is skipped while the recents animation is running. - final DisplayRotation displayRotation = mDisplayContent.getDisplayRotation(); - final int topOrientation = DisplayContentTests.getRotatedOrientation(mDefaultDisplay); - assertFalse(displayRotation.updateOrientation(topOrientation, false /* forceUpdate */)); - assertEquals(ActivityInfo.SCREEN_ORIENTATION_UNSET, displayRotation.getLastOrientation()); - final int prevRotation = mDisplayContent.getRotation(); - mWm.cleanupRecentsAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION); - - // In real case, it is called from RecentsAnimation#finishAnimation -> continueWindowLayout - // -> handleAppTransitionReady -> add FINISH_LAYOUT_REDO_CONFIG, and DisplayContent# - // applySurfaceChangesTransaction will call updateOrientation for FINISH_LAYOUT_REDO_CONFIG. - assertTrue(displayRotation.updateOrientation(topOrientation, false /* forceUpdate */)); - // The display should be updated to the changed orientation after the animation is finished. - assertNotEquals(displayRotation.getRotation(), prevRotation); - } - - @Test - public void testWallpaperHasFixedRotationApplied() { - makeDisplayPortrait(mDefaultDisplay); - unblockDisplayRotation(mDefaultDisplay); - mWm.setRecentsAnimationController(mController); - - // Create a portrait home activity, a wallpaper and a landscape activity displayed on top. - final ActivityRecord homeActivity = createHomeActivity(); - homeActivity.setOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - - final WindowState homeWindow = createWindow(null, TYPE_BASE_APPLICATION, homeActivity, - "homeWindow"); - makeWindowVisible(homeWindow); - homeActivity.addWindow(homeWindow); - homeWindow.getAttrs().flags |= FLAG_SHOW_WALLPAPER; - - // Landscape application - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - final WindowState applicationWindow = createWindow(null, TYPE_BASE_APPLICATION, activity, - "applicationWindow"); - activity.addWindow(applicationWindow); - activity.setOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); - - // Wallpaper - final WallpaperWindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm, - mock(IBinder.class), true, mDefaultDisplay, true /* ownerCanManageAppTokens */); - final WindowState wallpaperWindow = createWindow(null, TYPE_WALLPAPER, wallpaperWindowToken, - "wallpaperWindow"); - - // Make sure the landscape activity is on top and the display is in landscape - activity.moveFocusableActivityToTop("test"); - mDefaultDisplay.getConfiguration().windowConfiguration.setRotation( - mDefaultDisplay.getRotation()); - - spyOn(mDefaultDisplay.mWallpaperController); - doReturn(true).when(mDefaultDisplay.mWallpaperController).isWallpaperVisible(); - - // Start the recents animation - initializeRecentsAnimationController(mController, homeActivity); - - mDefaultDisplay.mWallpaperController.adjustWallpaperWindows(); - - // Check preconditions - ArrayList<WallpaperWindowToken> wallpapers = new ArrayList<>(1); - mDefaultDisplay.forAllWallpaperWindows(wallpapers::add); - - Truth.assertThat(wallpapers).hasSize(1); - Truth.assertThat(wallpapers.get(0).getTopChild()).isEqualTo(wallpaperWindow); - - // Actual check - assertEquals(Configuration.ORIENTATION_PORTRAIT, - wallpapers.get(0).getConfiguration().orientation); - - mController.cleanupAnimation(REORDER_MOVE_TO_TOP); - // The transform state should keep because we expect to listen the signal from the - // transition executed by moving the task to front. - assertTrue(homeActivity.hasFixedRotationTransform()); - assertTrue(mDefaultDisplay.isFixedRotationLaunchingApp(homeActivity)); - - mDefaultDisplay.mFixedRotationTransitionListener.onAppTransitionFinishedLocked( - homeActivity.token); - // Wallpaper's transform state should be cleared with home. - assertFalse(homeActivity.hasFixedRotationTransform()); - assertFalse(wallpaperWindowToken.hasFixedRotationTransform()); - } - - @Test - public void testIsAnimatingByRecents() { - final ActivityRecord homeActivity = createHomeActivity(); - final Task rootTask = createTask(mDefaultDisplay); - final Task childTask = createTaskInRootTask(rootTask, 0 /* userId */); - final Task leafTask = createTaskInRootTask(childTask, 0 /* userId */); - spyOn(leafTask); - doReturn(true).when(leafTask).isVisible(); - - initializeRecentsAnimationController(mController, homeActivity); - - // Verify RecentsAnimationController will animate visible leaf task by default. - verify(mController).addAnimation(eq(leafTask), anyBoolean(), anyBoolean(), any()); - assertTrue(leafTask.isAnimatingByRecents()); - - // Make sure isAnimatingByRecents will also return true when it called by the parent task. - assertTrue(rootTask.isAnimatingByRecents()); - assertTrue(childTask.isAnimatingByRecents()); - } - - @Test - public void testRestoreNavBarWhenEnteringRecents_expectAnimation() { - setupForShouldAttachNavBarDuringTransition(); - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - final ActivityRecord homeActivity = createHomeActivity(); - initializeRecentsAnimationController(mController, homeActivity); - - final WindowToken navToken = mDefaultDisplay.getDisplayPolicy().getNavigationBar().mToken; - final SurfaceControl.Transaction transaction = navToken.getPendingTransaction(); - - verify(mController.mStatusBar).setNavigationBarLumaSamplingEnabled( - eq(mDefaultDisplay.mDisplayId), eq(false)); - verify(transaction).reparent(navToken.getSurfaceControl(), activity.getSurfaceControl()); - verify(transaction).setLayer(navToken.getSurfaceControl(), Integer.MAX_VALUE); - assertTrue(mController.isNavigationBarAttachedToApp()); - - mController.cleanupAnimation(REORDER_MOVE_TO_TOP); - verify(mController).restoreNavigationBarFromApp(eq(true)); - verify(mController.mStatusBar).setNavigationBarLumaSamplingEnabled( - eq(mDefaultDisplay.mDisplayId), eq(true)); - verify(transaction).setLayer(navToken.getSurfaceControl(), 0); - assertFalse(mController.isNavigationBarAttachedToApp()); - assertTrue(navToken.isAnimating(ANIMATION_TYPE_TOKEN_TRANSFORM)); - } - - @Test - public void testRestoreNavBarWhenBackToApp_expectNoAnimation() { - setupForShouldAttachNavBarDuringTransition(); - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - final ActivityRecord homeActivity = createHomeActivity(); - initializeRecentsAnimationController(mController, homeActivity); - - final WindowToken navToken = mDefaultDisplay.getDisplayPolicy().getNavigationBar().mToken; - final SurfaceControl.Transaction transaction = navToken.getPendingTransaction(); - - verify(mController.mStatusBar).setNavigationBarLumaSamplingEnabled( - eq(mDefaultDisplay.mDisplayId), eq(false)); - verify(transaction).reparent(navToken.getSurfaceControl(), activity.getSurfaceControl()); - verify(transaction).setLayer(navToken.getSurfaceControl(), Integer.MAX_VALUE); - assertTrue(mController.isNavigationBarAttachedToApp()); - - final WindowContainer parent = navToken.getParent(); - - mController.cleanupAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION); - verify(mController).restoreNavigationBarFromApp(eq(false)); - verify(mController.mStatusBar).setNavigationBarLumaSamplingEnabled( - eq(mDefaultDisplay.mDisplayId), eq(true)); - verify(transaction).setLayer(navToken.getSurfaceControl(), 0); - verify(transaction).reparent(navToken.getSurfaceControl(), parent.getSurfaceControl()); - assertFalse(mController.isNavigationBarAttachedToApp()); - assertFalse(navToken.isAnimating(ANIMATION_TYPE_TOKEN_TRANSFORM)); - } - - @Test - public void testAddTaskToTargets_expectAnimation() { - setupForShouldAttachNavBarDuringTransition(); - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - final ActivityRecord homeActivity = createHomeActivity(); - initializeRecentsAnimationController(mController, homeActivity); - - final WindowToken navToken = mDefaultDisplay.getDisplayPolicy().getNavigationBar().mToken; - final SurfaceControl.Transaction transaction = navToken.getPendingTransaction(); - - verify(mController.mStatusBar).setNavigationBarLumaSamplingEnabled( - eq(mDefaultDisplay.mDisplayId), eq(false)); - verify(transaction).reparent(navToken.getSurfaceControl(), activity.getSurfaceControl()); - verify(transaction).setLayer(navToken.getSurfaceControl(), Integer.MAX_VALUE); - assertTrue(mController.isNavigationBarAttachedToApp()); - - final WindowContainer parent = navToken.getParent(); - - mController.addTaskToTargets(createTask(mDefaultDisplay), (type, anim) -> {}); - mController.cleanupAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION); - verify(mController).restoreNavigationBarFromApp(eq(true)); - verify(mController.mStatusBar).setNavigationBarLumaSamplingEnabled( - eq(mDefaultDisplay.mDisplayId), eq(true)); - verify(transaction).setLayer(navToken.getSurfaceControl(), 0); - assertFalse(mController.isNavigationBarAttachedToApp()); - assertTrue(navToken.isAnimating(ANIMATION_TYPE_TOKEN_TRANSFORM)); - } - - @Test - public void testNotAttachNavigationBar_controlledByFadeRotationAnimation() { - setupForShouldAttachNavBarDuringTransition(); - AsyncRotationController mockController = - mock(AsyncRotationController.class); - doReturn(mockController).when(mDefaultDisplay).getAsyncRotationController(); - final ActivityRecord homeActivity = createHomeActivity(); - initializeRecentsAnimationController(mController, homeActivity); - assertFalse(mController.isNavigationBarAttachedToApp()); - } - - @Test - public void testAttachNavBarInSplitScreenMode() { - setupForShouldAttachNavBarDuringTransition(); - TestSplitOrganizer organizer = new TestSplitOrganizer(mAtm); - final ActivityRecord primary = createActivityRecordWithParentTask( - organizer.createTaskToPrimary(true)); - final ActivityRecord secondary = createActivityRecordWithParentTask( - organizer.createTaskToSecondary(true)); - final ActivityRecord homeActivity = createHomeActivity(); - homeActivity.setVisibility(true); - initializeRecentsAnimationController(mController, homeActivity); - - WindowState navWindow = mController.getNavigationBarWindow(); - final WindowToken navToken = navWindow.mToken; - final SurfaceControl.Transaction transaction = navToken.getPendingTransaction(); - - verify(mController.mStatusBar).setNavigationBarLumaSamplingEnabled( - eq(mDefaultDisplay.mDisplayId), eq(false)); - verify(navWindow).setSurfaceTranslationY(-secondary.getBounds().top); - verify(transaction).reparent(navToken.getSurfaceControl(), secondary.getSurfaceControl()); - assertTrue(mController.isNavigationBarAttachedToApp()); - reset(navWindow); - - mController.cleanupAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION); - final WindowContainer parent = navToken.getParent(); - verify(mController.mStatusBar).setNavigationBarLumaSamplingEnabled( - eq(mDefaultDisplay.mDisplayId), eq(true)); - verify(navWindow).setSurfaceTranslationY(0); - verify(transaction).reparent(navToken.getSurfaceControl(), parent.getSurfaceControl()); - verify(mController).restoreNavigationBarFromApp(eq(false)); - assertFalse(mController.isNavigationBarAttachedToApp()); - } - - @Test - public void testCleanupAnimation_expectExitAnimationDone() { - mWm.setRecentsAnimationController(mController); - final ActivityRecord homeActivity = createHomeActivity(); - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1"); - activity.addWindow(win1); - - initializeRecentsAnimationController(mController, homeActivity); - mController.startAnimation(); - - spyOn(win1); - spyOn(win1.mWinAnimator); - // Simulate when the window is exiting and cleanupAnimation invoked - // (e.g. screen off during RecentsAnimation animating), will expect the window receives - // onExitAnimationDone to destroy the surface when the removal is allowed. - win1.mWinAnimator.mSurfaceControl = mock(SurfaceControl.class); - win1.mHasSurface = true; - win1.mAnimatingExit = true; - win1.mRemoveOnExit = true; - win1.mWindowRemovalAllowed = true; - mController.cleanupAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION); - verify(win1).onAnimationFinished(eq(ANIMATION_TYPE_RECENTS), any()); - verify(win1).onExitAnimationDone(); - verify(win1).destroySurface(eq(false), eq(false)); - assertFalse(win1.mAnimatingExit); - assertFalse(win1.mHasSurface); - } - - @Test - public void testCancelForRotation_ReorderToTop() throws Exception { - mWm.setRecentsAnimationController(mController); - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1"); - activity.addWindow(win1); - - mController.addAnimation(activity.getTask(), false /* isRecentTaskInvisible */); - mController.setWillFinishToHome(true); - mController.cancelAnimationForDisplayChange(); - - verify(mMockRunner).onAnimationCanceled(any(), any()); - verify(mAnimationCallbacks).onAnimationFinished(REORDER_MOVE_TO_TOP, false); - } - - @Test - public void testCancelForRotation_ReorderToOriginalPosition() throws Exception { - mWm.setRecentsAnimationController(mController); - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1"); - activity.addWindow(win1); - - mController.addAnimation(activity.getTask(), false /* isRecentTaskInvisible */); - mController.setWillFinishToHome(false); - mController.cancelAnimationForDisplayChange(); - - verify(mMockRunner).onAnimationCanceled(any(), any()); - verify(mAnimationCallbacks).onAnimationFinished(REORDER_MOVE_TO_ORIGINAL_POSITION, false); - } - - @Test - public void testCancelForStartHome() throws Exception { - mWm.setRecentsAnimationController(mController); - final ActivityRecord homeActivity = createHomeActivity(); - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); - final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity, "win1"); - activity.addWindow(win1); - - initializeRecentsAnimationController(mController, homeActivity); - mController.setWillFinishToHome(true); - - // Verify cancel is called with a snapshot and that we've created an overlay - spyOn(mWm.mTaskSnapshotController); - doReturn(mMockTaskSnapshot).when(mWm.mTaskSnapshotController).getSnapshot(anyInt(), - anyInt(), eq(false) /* restoreFromDisk */, eq(false) /* isLowResolution */); - mController.cancelAnimationForHomeStart(); - verify(mMockRunner).onAnimationCanceled(any(), any()); - - // Continue the animation (simulating a call to cleanupScreenshot()) - mController.continueDeferredCancelAnimation(); - verify(mAnimationCallbacks).onAnimationFinished(REORDER_MOVE_TO_TOP, false); - - // Assume home was moved to front so will-be-top callback should not be called. - homeActivity.moveFocusableActivityToTop("test"); - spyOn(mDefaultDisplay.mFixedRotationTransitionListener); - mController.cleanupAnimation(REORDER_MOVE_TO_TOP); - verify(mDefaultDisplay.mFixedRotationTransitionListener, never()).notifyRecentsWillBeTop(); - } - - private ActivityRecord createHomeActivity() { - final ActivityRecord homeActivity = new ActivityBuilder(mWm.mAtmService) - .setParentTask(mRootHomeTask) - .setCreateTask(true) - .build(); - // Avoid {@link RecentsAnimationController.TaskAnimationAdapter#createRemoteAnimationTarget} - // returning null when calling {@link RecentsAnimationController#createAppAnimations}. - homeActivity.setVisibility(true); - return homeActivity; - } - - private void startRecentsInDifferentRotation(ActivityRecord recentsActivity) { - final DisplayContent displayContent = recentsActivity.mDisplayContent; - displayContent.setFixedRotationLaunchingApp(recentsActivity, - (displayContent.getRotation() + 1) % 4); - mController = new RecentsAnimationController(mWm, mMockRunner, mAnimationCallbacks, - displayContent.getDisplayId()); - initializeRecentsAnimationController(mController, recentsActivity); - assertTrue(recentsActivity.hasFixedRotationTransform()); - } - - private static void assertTopFixedRotationLaunchingAppCleared(ActivityRecord activity) { - assertFalse(activity.hasFixedRotationTransform()); - assertFalse(activity.mDisplayContent.hasTopFixedRotationLaunchingApp()); - } - - private void setupForShouldAttachNavBarDuringTransition() { - final WindowState navBar = spy(createWindow(null, TYPE_NAVIGATION_BAR, "NavigationBar")); - mDefaultDisplay.getDisplayPolicy().addWindowLw(navBar, navBar.mAttrs); - mWm.setRecentsAnimationController(mController); - doReturn(navBar).when(mController).getNavigationBarWindow(); - final DisplayPolicy displayPolicy = spy(mDefaultDisplay.getDisplayPolicy()); - doReturn(displayPolicy).when(mDefaultDisplay).getDisplayPolicy(); - doReturn(true).when(displayPolicy).shouldAttachNavBarToAppDuringTransition(); - } - - private static void initializeRecentsAnimationController(RecentsAnimationController controller, - ActivityRecord activity) { - controller.initialize(activity.getActivityType(), new SparseBooleanArray(), activity); - } - - private static void verifyNoMoreInteractionsExceptAsBinder(IInterface binder) { - verify(binder, atLeast(0)).asBinder(); - verifyNoMoreInteractions(binder); - } -} diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java index f93ffb83178f..6f7d0dced484 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java @@ -36,7 +36,6 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION; -import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.fail; import static org.junit.Assert.assertEquals; @@ -728,70 +727,6 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { } } - @Test - public void testNonAppTarget_notSendNavBar_controlledByRecents() throws Exception { - final RecentsAnimationController mockController = - mock(RecentsAnimationController.class); - doReturn(mockController).when(mWm).getRecentsAnimationController(); - final int transit = TRANSIT_OLD_TASK_OPEN; - setupForNonAppTargetNavBar(transit, true); - - final ArgumentCaptor<RemoteAnimationTarget[]> nonAppsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - verify(mMockRunner).onAnimationStart(eq(transit), - any(), any(), nonAppsCaptor.capture(), any()); - for (int i = 0; i < nonAppsCaptor.getValue().length; i++) { - if (nonAppsCaptor.getValue()[0].windowType == TYPE_NAVIGATION_BAR) { - fail("Non-app animation target must not contain navbar"); - } - } - } - - @android.platform.test.annotations.RequiresFlagsDisabled( - com.android.window.flags.Flags.FLAG_DO_NOT_SKIP_IME_BY_TARGET_VISIBILITY) - @SetupWindows(addWindows = W_INPUT_METHOD) - @Test - public void testLaunchRemoteAnimationWithoutImeBehind() { - final WindowState win1 = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin1"); - final WindowState win2 = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin2"); - - // Simulating win1 has shown IME and being IME layering/input target - mDisplayContent.setImeLayeringTarget(win1); - mDisplayContent.setImeInputTarget(win1); - mImeWindow.mWinAnimator.hide(mDisplayContent.getPendingTransaction(), "test"); - spyOn(mDisplayContent); - mImeWindow.mWinAnimator.mSurfaceControl = mock(SurfaceControl.class); - makeWindowVisibleAndDrawn(mImeWindow); - assertTrue(mImeWindow.isOnScreen()); - assertFalse(mImeWindow.isParentWindowHidden()); - - try { - // Simulating now win1 is being covered by the lockscreen which has no surface, - // and then launching an activity win2 with the remote animation - win1.mHasSurface = false; - win1.mActivityRecord.setVisibility(false); - mDisplayContent.mOpeningApps.add(win2.mActivityRecord); - final AnimationAdapter adapter = mController.createRemoteAnimationRecord( - win2.mActivityRecord, new Point(50, 100), null, - new Rect(50, 100, 150, 150), null, false).mAdapter; - adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, - mFinishedCallback); - - mDisplayContent.applySurfaceChangesTransaction(); - mController.goodToGo(TRANSIT_OLD_TASK_OPEN); - mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); - - verify(mMockRunner).onAnimationStart(eq(TRANSIT_OLD_TASK_OPEN), - any(), any(), any(), any()); - // Verify the IME window won't apply surface change transaction with forAllImeWindows - verify(mDisplayContent, never()).forAllImeWindows(any(), eq(true)); - } catch (Exception e) { - // no-op - } finally { - mDisplayContent.mOpeningApps.clear(); - } - } - private AnimationAdapter setupForNonAppTargetNavBar(int transit, boolean shouldAttachNavBar) { final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin"); mDisplayContent.mOpeningApps.add(win.mActivityRecord); diff --git a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java index e019a416c069..6adf0fe15ba8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java @@ -1260,68 +1260,38 @@ public class RootTaskTests extends WindowTestsBase { @Test public void testShouldSleepActivities() { + final Task task = new TaskBuilder(mSupervisor).build(); + task.mDisplayContent = mock(DisplayContent.class); // When focused activity and keyguard is going away, we should not sleep regardless // of the display state, but keyguard-going-away should only take effects on default - // display since there is no keyguard on secondary displays (yet). - verifyShouldSleepActivities(true /* focusedRootTask */, true /*keyguardGoingAway*/, + // display because the keyguard-going-away state of secondary displays are already the + // same as default display. + verifyShouldSleepActivities(task, true /* isVisibleTask */, true /* keyguardGoingAway */, true /* displaySleeping */, true /* isDefaultDisplay */, false /* expected */); - verifyShouldSleepActivities(true /* focusedRootTask */, true /*keyguardGoingAway*/, + verifyShouldSleepActivities(task, true /* isVisibleTask */, true /* keyguardGoingAway */, true /* displaySleeping */, false /* isDefaultDisplay */, true /* expected */); // When not the focused root task, defer to display sleeping state. - verifyShouldSleepActivities(false /* focusedRootTask */, true /*keyguardGoingAway*/, + verifyShouldSleepActivities(task, false /* isVisibleTask */, true /* keyguardGoingAway */, true /* displaySleeping */, true /* isDefaultDisplay */, true /* expected */); // If keyguard is going away, defer to the display sleeping state. - verifyShouldSleepActivities(true /* focusedRootTask */, false /*keyguardGoingAway*/, + verifyShouldSleepActivities(task, true /* isVisibleTask */, false /* keyguardGoingAway */, true /* displaySleeping */, true /* isDefaultDisplay */, true /* expected */); - verifyShouldSleepActivities(true /* focusedRootTask */, false /*keyguardGoingAway*/, + verifyShouldSleepActivities(task, true /* isVisibleTask */, false /* keyguardGoingAway */, false /* displaySleeping */, true /* isDefaultDisplay */, false /* expected */); } - @Test - public void testRootTaskOrderChangedOnRemoveRootTask() { - final Task task = new TaskBuilder(mSupervisor).build(); - RootTaskOrderChangedListener listener = new RootTaskOrderChangedListener(); - mDefaultTaskDisplayArea.registerRootTaskOrderChangedListener(listener); - try { - mDefaultTaskDisplayArea.removeRootTask(task); - } finally { - mDefaultTaskDisplayArea.unregisterRootTaskOrderChangedListener(listener); - } - assertTrue(listener.mChanged); - } - - @Test - public void testRootTaskOrderChangedOnAddPositionRootTask() { - final Task task = new TaskBuilder(mSupervisor).build(); - mDefaultTaskDisplayArea.removeRootTask(task); - - RootTaskOrderChangedListener listener = new RootTaskOrderChangedListener(); - mDefaultTaskDisplayArea.registerRootTaskOrderChangedListener(listener); - try { - task.mReparenting = true; - mDefaultTaskDisplayArea.addChild(task, 0); - } finally { - mDefaultTaskDisplayArea.unregisterRootTaskOrderChangedListener(listener); - } - assertTrue(listener.mChanged); - } + private static void verifyShouldSleepActivities(Task task, boolean isVisibleTask, + boolean keyguardGoingAway, boolean displaySleeping, boolean isDefaultDisplay, + boolean expected) { + final DisplayContent display = task.mDisplayContent; + display.isDefaultDisplay = isDefaultDisplay; + doReturn(keyguardGoingAway).when(display).isKeyguardGoingAway(); + doReturn(displaySleeping).when(display).isSleeping(); + doReturn(isVisibleTask).when(task).shouldBeVisible(null /* starting */); - @Test - public void testRootTaskOrderChangedOnPositionRootTask() { - RootTaskOrderChangedListener listener = new RootTaskOrderChangedListener(); - try { - final Task fullscreenRootTask1 = createTaskForShouldBeVisibleTest( - mDefaultTaskDisplayArea, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, - true /* onTop */); - mDefaultTaskDisplayArea.registerRootTaskOrderChangedListener(listener); - mDefaultTaskDisplayArea.positionChildAt(POSITION_BOTTOM, fullscreenRootTask1, - false /*includingParents*/); - } finally { - mDefaultTaskDisplayArea.unregisterRootTaskOrderChangedListener(listener); - } - assertTrue(listener.mChanged); + assertEquals(expected, task.shouldSleepActivities()); } @Test @@ -1451,11 +1421,6 @@ public class RootTaskTests extends WindowTestsBase { anyBoolean()); } - private boolean isAssistantOnTop() { - return mContext.getResources().getBoolean( - com.android.internal.R.bool.config_assistantOnTopOfDream); - } - private void verifyShouldSleepActivities(boolean focusedRootTask, boolean keyguardGoingAway, boolean displaySleeping, boolean isDefaultDisplay, boolean expected) { @@ -1471,14 +1436,4 @@ public class RootTaskTests extends WindowTestsBase { assertEquals(expected, task.shouldSleepActivities()); } - - private static class RootTaskOrderChangedListener - implements TaskDisplayArea.OnRootTaskOrderChangedListener { - public boolean mChanged = false; - - @Override - public void onRootTaskOrderChanged(Task rootTask) { - mChanged = true; - } - } } diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 5bb437827dae..f74340113a04 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -613,7 +613,7 @@ public class SizeCompatTests extends WindowTestsBase { assertFalse(mActivity.mDisplayContent.shouldImeAttachedToApp()); // Recompute the natural configuration without resolving size compat configuration. - mActivity.clearSizeCompatMode(); + mActivity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); mActivity.onConfigurationChanged(mTask.getConfiguration()); // It should keep non-attachable because the resolved bounds will be computed according to // the aspect ratio that won't match its parent bounds. @@ -706,7 +706,7 @@ public class SizeCompatTests extends WindowTestsBase { / originalBounds.width())); // Recompute the natural configuration in the new display. - mActivity.clearSizeCompatMode(); + mActivity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); mActivity.ensureActivityConfiguration(); // Because the display cannot rotate, the portrait activity will fit the short side of // display with keeping portrait bounds [200, 0 - 700, 1000] in center. @@ -1482,7 +1482,7 @@ public class SizeCompatTests extends WindowTestsBase { // After changing the orientation to portrait the override should be applied. activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - activity.clearSizeCompatMode(); + activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); // The per-package override forces the activity into a 3:2 aspect ratio assertEquals(1200, activity.getBounds().height()); @@ -1511,7 +1511,7 @@ public class SizeCompatTests extends WindowTestsBase { // After changing the orientation to portrait the override should be applied. activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - activity.clearSizeCompatMode(); + activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); // The per-package override forces the activity into a 3:2 aspect ratio assertEquals(1200, activity.getBounds().height()); @@ -1538,7 +1538,7 @@ public class SizeCompatTests extends WindowTestsBase { // After changing the orientation to landscape the override shouldn't be applied. activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); - activity.clearSizeCompatMode(); + activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); // The per-package override should have no effect assertEquals(1200, activity.getBounds().height()); @@ -3054,7 +3054,10 @@ public class SizeCompatTests extends WindowTestsBase { false /* deferPause */); // App still in size compat, and the bounds don't change. - verify(mActivity, never()).clearSizeCompatMode(); + final AppCompatSizeCompatModePolicy scmPolicy = mActivity.mAppCompatController + .getAppCompatSizeCompatModePolicy(); + spyOn(scmPolicy); + verify(scmPolicy, never()).clearSizeCompatMode(); assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertDownScaled(); @@ -3896,7 +3899,7 @@ public class SizeCompatTests extends WindowTestsBase { private void recomputeNaturalConfigurationOfUnresizableActivity() { // Recompute the natural configuration of the non-resizable activity and the split screen. - mActivity.clearSizeCompatMode(); + mActivity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); // Draw letterbox. mActivity.setVisible(false); @@ -4827,7 +4830,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(origDensity, mActivity.getConfiguration().densityDpi); // Activity should exit size compat with new density. - mActivity.clearSizeCompatMode(); + mActivity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); assertFitted(); assertEquals(newDensity, mActivity.getConfiguration().densityDpi); @@ -5013,7 +5016,7 @@ public class SizeCompatTests extends WindowTestsBase { activity.setRequestedOrientation(screenOrientation); } // Make sure to use the provided configuration to construct the size compat fields. - activity.clearSizeCompatMode(); + activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); activity.ensureActivityConfiguration(); // Make sure the display configuration reflects the change of activity. if (activity.mDisplayContent.updateOrientation()) { diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java index 6c8a7ac0c613..9981a4dd9fce 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java @@ -23,7 +23,6 @@ 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.verifyZeroInteractions; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -96,18 +95,18 @@ public class SurfaceAnimatorTest extends WindowTestsBase { @Test public void testRunAnimation() { mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */, - ANIMATION_TYPE_RECENTS); + ANIMATION_TYPE_APP_TRANSITION); final ArgumentCaptor<OnAnimationFinishedCallback> callbackCaptor = ArgumentCaptor.forClass( OnAnimationFinishedCallback.class); assertAnimating(mAnimatable); verify(mTransaction).reparent(eq(mAnimatable.mSurface), eq(mAnimatable.mLeash)); - verify(mSpec).startAnimation(any(), any(), eq(ANIMATION_TYPE_RECENTS), + verify(mSpec).startAnimation(any(), any(), eq(ANIMATION_TYPE_APP_TRANSITION), callbackCaptor.capture()); - callbackCaptor.getValue().onAnimationFinished(ANIMATION_TYPE_RECENTS, mSpec); + callbackCaptor.getValue().onAnimationFinished(ANIMATION_TYPE_APP_TRANSITION, mSpec); assertNotAnimating(mAnimatable); assertTrue(mAnimatable.mFinishedCallbackCalled); - assertEquals(ANIMATION_TYPE_RECENTS, mAnimatable.mFinishedAnimationType); + assertEquals(ANIMATION_TYPE_APP_TRANSITION, mAnimatable.mFinishedAnimationType); verify(mTransaction).remove(eq(mAnimatable.mLeash)); // TODO: Verify reparenting once we use mPendingTransaction to reparent it back } diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 56fca31afa37..52a80b01971d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -1251,7 +1251,7 @@ public class TransitionTests extends WindowTestsBase { final Transition transition = app.mTransitionController.createTransition(TRANSIT_OPEN); app.mTransitionController.requestStartTransition(transition, app.getTask(), null /* remoteTransition */, null /* displayChange */); - app.mTransitionController.collectExistenceChange(app.getTask()); + transition.collectExistenceChange(app.getTask()); mDisplayContent.setFixedRotationLaunchingAppUnchecked(app); final AsyncRotationController asyncRotationController = mDisplayContent.getAsyncRotationController(); @@ -1416,7 +1416,8 @@ public class TransitionTests extends WindowTestsBase { activity1.setVisibleRequested(false); activity2.setVisibleRequested(true); - openTransition.finishTransition(); + final ActionChain chain = ActionChain.testFinish(null); + openTransition.finishTransition(chain); // We finished the openTransition. Even though activity1 is visibleRequested=false, since // the closeTransition animation hasn't played yet, make sure that we didn't commit @@ -1429,7 +1430,7 @@ public class TransitionTests extends WindowTestsBase { // normally. mWm.mSyncEngine.abort(closeTransition.getSyncId()); - closeTransition.finishTransition(); + closeTransition.finishTransition(chain); assertFalse(activity1.isVisible()); assertTrue(activity2.isVisible()); @@ -1449,7 +1450,7 @@ public class TransitionTests extends WindowTestsBase { activity1.setState(ActivityRecord.State.INITIALIZING, "test"); activity1.mLaunchTaskBehind = true; mWm.mSyncEngine.abort(noChangeTransition.getSyncId()); - noChangeTransition.finishTransition(); + noChangeTransition.finishTransition(chain); assertTrue(activity1.mLaunchTaskBehind); } @@ -1468,7 +1469,7 @@ public class TransitionTests extends WindowTestsBase { // We didn't call abort on the transition itself, so it will still run onTransactionReady // normally. mWm.mSyncEngine.abort(transition1.getSyncId()); - transition1.finishTransition(); + transition1.finishTransition(ActionChain.testFinish(transition1)); verify(transitionEndedListener).run(); @@ -1530,7 +1531,7 @@ public class TransitionTests extends WindowTestsBase { verify(taskSnapshotController, times(1)).recordSnapshot(eq(task2)); - controller.finishTransition(openTransition); + controller.finishTransition(ActionChain.testFinish(openTransition)); // We are now going to simulate closing task1 to return back to (open) task2. final Transition closeTransition = createTestTransition(TRANSIT_CLOSE, controller); @@ -1595,7 +1596,7 @@ public class TransitionTests extends WindowTestsBase { doReturn(true).when(task1).isTranslucentForTransition(); assertFalse(controller.canApplyDim(task1)); - controller.finishTransition(closeTransition); + controller.finishTransition(ActionChain.testFinish(closeTransition)); assertTrue(wasInFinishingTransition[0]); assertFalse(calledListenerOnOtherDisplay[0]); assertNull(controller.mFinishingTransition); @@ -1651,7 +1652,7 @@ public class TransitionTests extends WindowTestsBase { // to avoid the latency to resume the current top, i.e. appB. assertTrue(controller.isTransientVisible(taskRecent)); // The recent is paused after the transient transition is finished. - controller.finishTransition(transition); + controller.finishTransition(ActionChain.testFinish(transition)); assertFalse(controller.isTransientVisible(taskRecent)); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java index fa28d117c5bc..7a440e676b39 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java @@ -323,7 +323,10 @@ public class TransparentPolicyTest extends WindowTestsBase { ta.launchTransparentActivityInTask(); a.assertNotNullOnTopActivity(ActivityRecord::getAppCompatDisplayInsets); - a.applyToTopActivity(ActivityRecord::clearSizeCompatMode); + a.applyToTopActivity((top) -> { + top.mAppCompatController.getAppCompatSizeCompatModePolicy() + .clearSizeCompatMode(); + }); a.assertNullOnTopActivity(ActivityRecord::getAppCompatDisplayInsets); }); }); diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java index c65b76efd614..9602ae29604f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java @@ -37,7 +37,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -276,9 +275,8 @@ public class WallpaperControllerTests extends WindowTestsBase { final DisplayContent dc = mDisplayContent; final WindowState homeWin = createWallpaperTargetWindow(dc); final WindowState appWin = createWindow(null, TYPE_BASE_APPLICATION, "app"); - final RecentsAnimationController recentsController = mock(RecentsAnimationController.class); - doReturn(true).when(recentsController).isWallpaperVisible(eq(appWin)); - mWm.setRecentsAnimationController(recentsController); + appWin.mAttrs.flags |= FLAG_SHOW_WALLPAPER; + makeWindowVisible(appWin); dc.mWallpaperController.adjustWallpaperWindows(); assertEquals(appWin, dc.mWallpaperController.getWallpaperTarget()); @@ -354,46 +352,6 @@ public class WallpaperControllerTests extends WindowTestsBase { } @Test - public void testFixedRotationRecentsAnimatingTask() { - final WindowState wallpaperWindow = createWallpaperWindow(mDisplayContent); - final WallpaperWindowToken wallpaperToken = wallpaperWindow.mToken.asWallpaperToken(); - final WindowState appWin = createWindow(null, TYPE_BASE_APPLICATION, "app"); - makeWindowVisible(appWin); - final ActivityRecord r = appWin.mActivityRecord; - final RecentsAnimationController recentsController = mock(RecentsAnimationController.class); - doReturn(true).when(recentsController).isWallpaperVisible(eq(appWin)); - mWm.setRecentsAnimationController(recentsController); - - r.applyFixedRotationTransform(mDisplayContent.getDisplayInfo(), - mDisplayContent.mDisplayFrames, mDisplayContent.getConfiguration()); - // Invisible requested activity should not share its rotation transform. - r.setVisibleRequested(false); - mDisplayContent.mWallpaperController.adjustWallpaperWindows(); - assertFalse(wallpaperToken.hasFixedRotationTransform()); - - // Wallpaper should link the transform of its target. - r.setVisibleRequested(true); - mDisplayContent.mWallpaperController.adjustWallpaperWindows(); - assertEquals(appWin, mDisplayContent.mWallpaperController.getWallpaperTarget()); - assertTrue(r.hasFixedRotationTransform()); - assertTrue(wallpaperToken.hasFixedRotationTransform()); - - // The case with shell transition. - registerTestTransitionPlayer(); - final Transition t = r.mTransitionController.createTransition(TRANSIT_OPEN); - final ActivityRecord recents = mock(ActivityRecord.class); - t.collect(r.getTask()); - r.mTransitionController.setTransientLaunch(recents, r.getTask()); - // The activity in restore-below task should not be the target if keyguard is not locked. - mDisplayContent.mWallpaperController.adjustWallpaperWindows(); - assertNotEquals(appWin, mDisplayContent.mWallpaperController.getWallpaperTarget()); - // The activity in restore-below task should not be the target if keyguard is occluded. - doReturn(true).when(mDisplayContent).isKeyguardLocked(); - mDisplayContent.mWallpaperController.adjustWallpaperWindows(); - assertNotEquals(appWin, mDisplayContent.mWallpaperController.getWallpaperTarget()); - } - - @Test public void testWallpaperReportConfigChange() { final WindowState wallpaperWindow = createWallpaperWindow(mDisplayContent); createWallpaperTargetWindow(mDisplayContent); @@ -449,7 +407,7 @@ public class WallpaperControllerTests extends WindowTestsBase { final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); token.finishSync(t, token.getSyncGroup(), false /* cancel */); transit.onTransactionReady(transit.getSyncId(), t); - dc.mTransitionController.finishTransition(transit); + dc.mTransitionController.finishTransition(ActionChain.testFinish(transit)); assertFalse(wallpaperWindow.isVisible()); assertFalse(token.isVisible()); } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java index 22def515a98e..72935cb546d9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java @@ -62,8 +62,6 @@ public class WindowContainerTraversalTests extends WindowTestsBase { verify(c).accept(eq(mImeWindow)); } - @android.platform.test.annotations.RequiresFlagsEnabled( - com.android.window.flags.Flags.FLAG_DO_NOT_SKIP_IME_BY_TARGET_VISIBILITY) @SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD }) @Test public void testTraverseImeRegardlessOfImeTarget() { diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java index 0cb22ad47355..9bad2ec2ca2a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java @@ -44,7 +44,6 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; import android.Manifest; @@ -197,21 +196,6 @@ public class WindowProcessControllerTests extends WindowTestsBase { } @Test - public void testSetRunningBothAnimations() { - mWpc.setRunningRemoteAnimation(true); - mWpc.setRunningRecentsAnimation(true); - - mWpc.setRunningRecentsAnimation(false); - mWpc.setRunningRemoteAnimation(false); - waitHandlerIdle(mAtm.mH); - - InOrder orderVerifier = Mockito.inOrder(mMockListener); - orderVerifier.verify(mMockListener, times(1)).setRunningRemoteAnimation(eq(true)); - orderVerifier.verify(mMockListener, times(1)).setRunningRemoteAnimation(eq(false)); - orderVerifier.verifyNoMoreInteractions(); - } - - @Test public void testConfigurationForSecondaryScreenDisplayArea() { // By default, the process should not listen to any display area. assertNull(mWpc.getDisplayArea()); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index bcf4ebcd9740..a215c0a80b46 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -2131,7 +2131,7 @@ public class WindowTestsBase extends SystemServiceTestsBase { } public void finish() { - mController.finishTransition(mLastTransit); + mController.finishTransition(ActionChain.testFinish(mLastTransit)); } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java index 973ab846fa4a..88ce3a6f5bf8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java @@ -40,23 +40,16 @@ import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS; import static com.android.server.wm.WindowStateAnimator.PRESERVED_SURFACE_LAYER; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import android.graphics.PixelFormat; import android.graphics.Rect; -import android.os.Binder; import android.platform.test.annotations.Presubmit; -import android.util.SparseBooleanArray; -import android.view.IRecentsAnimationRunner; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.window.ScreenCapture; @@ -410,30 +403,6 @@ public class ZOrderingTests extends WindowTestsBase { } @Test - public void testAssignWindowLayers_ForImeOnAppWithRecentsAnimating() { - final WindowState imeAppTarget = createWindow(null, TYPE_APPLICATION, - mAppWindow.mActivityRecord, "imeAppTarget"); - mDisplayContent.setImeInputTarget(imeAppTarget); - mDisplayContent.setImeLayeringTarget(imeAppTarget); - mDisplayContent.setImeControlTarget(imeAppTarget); - mDisplayContent.updateImeParent(); - - // Simulate the ime layering target task is animating with recents animation. - final Task imeAppTargetTask = imeAppTarget.getTask(); - final SurfaceAnimator imeTargetTaskAnimator = imeAppTargetTask.mSurfaceAnimator; - spyOn(imeTargetTaskAnimator); - doReturn(ANIMATION_TYPE_RECENTS).when(imeTargetTaskAnimator).getAnimationType(); - doReturn(true).when(imeTargetTaskAnimator).isAnimating(); - - mDisplayContent.assignChildLayers(mTransaction); - - // Ime should on top of the application window when in recents animation and keep - // attached on app. - assertTrue(mDisplayContent.shouldImeAttachedToApp()); - assertWindowHigher(mImeWindow, imeAppTarget); - } - - @Test public void testAssignWindowLayers_ForImeOnPopupImeLayeringTarget() { final WindowState imeAppTarget = createWindow(null, TYPE_APPLICATION, mAppWindow.mActivityRecord, "imeAppTarget"); @@ -493,43 +462,6 @@ public class ZOrderingTests extends WindowTestsBase { } @Test - public void testAttachNavBarWhenEnteringRecents_expectNavBarHigherThanIme() { - // create RecentsAnimationController - IRecentsAnimationRunner mockRunner = mock(IRecentsAnimationRunner.class); - when(mockRunner.asBinder()).thenReturn(new Binder()); - final int displayId = mDisplayContent.getDisplayId(); - RecentsAnimationController controller = new RecentsAnimationController( - mWm, mockRunner, null, displayId); - spyOn(controller); - doReturn(mNavBarWindow).when(controller).getNavigationBarWindow(); - mWm.setRecentsAnimationController(controller); - - // set ime visible - spyOn(mDisplayContent.mInputMethodWindow); - doReturn(true).when(mDisplayContent.mInputMethodWindow).isVisible(); - - DisplayPolicy policy = mDisplayContent.getDisplayPolicy(); - spyOn(policy); - doReturn(true).when(policy).shouldAttachNavBarToAppDuringTransition(); - - // create home activity - Task rootHomeTask = mDisplayContent.getDefaultTaskDisplayArea().getRootHomeTask(); - final ActivityRecord homeActivity = new ActivityBuilder(mWm.mAtmService) - .setParentTask(rootHomeTask) - .setCreateTask(true) - .build(); - homeActivity.setVisibility(true); - - // start recent animation - controller.initialize(homeActivity.getActivityType(), new SparseBooleanArray(), - homeActivity); - - mDisplayContent.assignChildLayers(mTransaction); - assertZOrderGreaterThan(mTransaction, mNavBarWindow.mToken.getSurfaceControl(), - mDisplayContent.getImeContainer().getSurfaceControl()); - } - - @Test public void testPopupWindowAndParentIsImeTarget_expectHigherThanIme_inMultiWindow() { // Simulate the app window is in multi windowing mode and being IME target mAppWindow.getConfiguration().windowConfiguration.setWindowingMode( diff --git a/telecomm/java/android/telecom/PhoneAccount.java b/telecomm/java/android/telecom/PhoneAccount.java index e6fe406dd8e5..83dac184fc5f 100644 --- a/telecomm/java/android/telecom/PhoneAccount.java +++ b/telecomm/java/android/telecom/PhoneAccount.java @@ -175,8 +175,15 @@ public final class PhoneAccount implements Parcelable { * <p> * The call recording tone is a 1400 hz tone which repeats every 15 seconds while recording is * in progress. + * + * @deprecated this API was only intended to prevent call recording via the microphone by an app + * while in a phone call. Audio policies no longer make this possible. Further, this API was + * never actually used. Call recording solutions integrated in an OEM dialer app must use + * appropriate recording signals to inform the caller/callee of the recording. * @hide */ + @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + @Deprecated @SystemApi public static final String EXTRA_PLAY_CALL_RECORDING_TONE = "android.telecom.extra.PLAY_CALL_RECORDING_TONE"; diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index afd5720d9264..1ba496df5005 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -420,6 +420,7 @@ public class CarrierConfigManager { * <p> * Note: This requires the Telephony config_supports_telephony_audio_device overlay to be true * in order to work. + * @deprecated this functionality was never used and is no longer supported. * @hide */ public static final String KEY_PLAY_CALL_RECORDING_TONE_BOOL = "play_call_recording_tone_bool"; diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 3e8b3268ae4c..7481daa8e396 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -1777,8 +1777,8 @@ public class TelephonyManager { /** * Used as an int value for {@link #EXTRA_DEFAULT_SUBSCRIPTION_SELECT_TYPE} - * to indicate user to decide whether current SIM should be preferred for all - * data / voice / sms. {@link #EXTRA_SUBSCRIPTION_ID} will specified to indicate + * to indicate the current SIM should be preferred for all data / voice / sms. + * {@link #EXTRA_SUBSCRIPTION_ID} will specified to indicate * which subscription should be the default subscription. * @hide */ diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt index 8040610c485b..cfc818b6c0e9 100644 --- a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt +++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt @@ -31,8 +31,7 @@ import org.junit.runner.RunWith @RunWith(FlickerServiceJUnit4ClassRunner::class) class CloseAppBackButton3ButtonLandscape : CloseAppBackButton(NavBar.MODE_3BUTTON, Rotation.ROTATION_90) { - // TODO: Missing CUJ (b/300078127) - @ExpectedScenarios(["ENTIRE_TRACE"]) + @ExpectedScenarios(["APP_CLOSE_TO_HOME"]) @Test override fun closeAppBackButtonTest() = super.closeAppBackButtonTest() diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt index aacccf4e680c..6bf32a8e2083 100644 --- a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt +++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt @@ -31,8 +31,7 @@ import org.junit.runner.RunWith @RunWith(FlickerServiceJUnit4ClassRunner::class) class CloseAppBackButton3ButtonPortrait : CloseAppBackButton(NavBar.MODE_3BUTTON, Rotation.ROTATION_0) { - // TODO: Missing CUJ (b/300078127) - @ExpectedScenarios(["ENTIRE_TRACE"]) + @ExpectedScenarios(["APP_CLOSE_TO_HOME"]) @Test override fun closeAppBackButtonTest() = super.closeAppBackButtonTest() diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt index 74ee46093f6e..4b6ab773f15e 100644 --- a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt +++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt @@ -31,8 +31,7 @@ import org.junit.runner.RunWith @RunWith(FlickerServiceJUnit4ClassRunner::class) class CloseAppBackButtonGesturalNavLandscape : CloseAppBackButton(NavBar.MODE_GESTURAL, Rotation.ROTATION_90) { - // TODO: Missing CUJ (b/300078127) - @ExpectedScenarios(["ENTIRE_TRACE"]) + @ExpectedScenarios(["APP_CLOSE_TO_HOME"]) @Test override fun closeAppBackButtonTest() = super.closeAppBackButtonTest() diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt index 57463c33c1fa..7cc9db027e1f 100644 --- a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt +++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt @@ -31,8 +31,7 @@ import org.junit.runner.RunWith @RunWith(FlickerServiceJUnit4ClassRunner::class) class CloseAppBackButtonGesturalNavPortrait : CloseAppBackButton(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) { - // TODO: Missing CUJ (b/300078127) - @ExpectedScenarios(["ENTIRE_TRACE"]) + @ExpectedScenarios(["APP_CLOSE_TO_HOME"]) @Test override fun closeAppBackButtonTest() = super.closeAppBackButtonTest() diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java index 05a68e9649d4..6db5f8277e52 100644 --- a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java +++ b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java @@ -798,6 +798,38 @@ public class PerfettoProtoLogImplTest { .isEqualTo("My Test Debug Log Message true"); } + @Test + public void usesDefaultLogFromLevel() throws IOException { + PerfettoTraceMonitor traceMonitor = + PerfettoTraceMonitor.newBuilder().enableProtoLog(LogLevel.WARN).build(); + try { + traceMonitor.start(); + mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, + "This message should not be logged"); + mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, + "This message should logged %d", 123); + mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, + "This message should also be logged %d", 567); + } finally { + traceMonitor.stop(mWriter); + } + + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final ProtoLogTrace protolog = reader.readProtoLogTrace(); + + Truth.assertThat(protolog.messages).hasSize(2); + + Truth.assertThat(protolog.messages.get(0).getLevel()) + .isEqualTo(LogLevel.WARN); + Truth.assertThat(protolog.messages.get(0).getMessage()) + .isEqualTo("This message should logged 123"); + + Truth.assertThat(protolog.messages.get(1).getLevel()) + .isEqualTo(LogLevel.ERROR); + Truth.assertThat(protolog.messages.get(1).getMessage()) + .isEqualTo("This message should also be logged 567"); + } + private enum TestProtoLogGroup implements IProtoLogGroup { TEST_GROUP(true, true, false, "TEST_TAG"); diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java index e3ec62d5b5a6..aba6722c0813 100644 --- a/tests/Internal/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java +++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java @@ -41,14 +41,14 @@ import java.io.PrintWriter; public class ProtoLogCommandHandlerTest { @Mock - ProtoLogService mProtoLogService; + ProtoLogConfigurationService mProtoLogConfigurationService; @Mock PrintWriter mPrintWriter; @Test public void printsHelpForAllAvailableCommands() { final ProtoLogCommandHandler cmdHandler = - new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); cmdHandler.onHelp(); validateOnHelpPrinted(); @@ -57,7 +57,7 @@ public class ProtoLogCommandHandlerTest { @Test public void printsHelpIfCommandIsNull() { final ProtoLogCommandHandler cmdHandler = - new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); cmdHandler.onCommand(null); validateOnHelpPrinted(); @@ -65,13 +65,13 @@ public class ProtoLogCommandHandlerTest { @Test public void handlesGroupListCommand() { - Mockito.when(mProtoLogService.getGroups()) + Mockito.when(mProtoLogConfigurationService.getGroups()) .thenReturn(new String[] {"MY_TEST_GROUP", "MY_OTHER_GROUP"}); final ProtoLogCommandHandler cmdHandler = - new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, - new String[] { "groups", "list" }); + cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "groups", "list" }); Mockito.verify(mPrintWriter, times(1)) .println(contains("MY_TEST_GROUP")); @@ -82,10 +82,10 @@ public class ProtoLogCommandHandlerTest { @Test public void handlesIncompleteGroupsCommand() { final ProtoLogCommandHandler cmdHandler = - new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, - new String[] { "groups" }); + cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "groups" }); Mockito.verify(mPrintWriter, times(1)) .println(contains("Incomplete command")); @@ -93,13 +93,14 @@ public class ProtoLogCommandHandlerTest { @Test public void handlesGroupStatusCommand() { - Mockito.when(mProtoLogService.getGroups()).thenReturn(new String[] {"MY_GROUP"}); - Mockito.when(mProtoLogService.isLoggingToLogcat("MY_GROUP")).thenReturn(true); + Mockito.when(mProtoLogConfigurationService.getGroups()) + .thenReturn(new String[] {"MY_GROUP"}); + Mockito.when(mProtoLogConfigurationService.isLoggingToLogcat("MY_GROUP")).thenReturn(true); final ProtoLogCommandHandler cmdHandler = - new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, - new String[] { "groups", "status", "MY_GROUP" }); + cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "groups", "status", "MY_GROUP" }); Mockito.verify(mPrintWriter, times(1)) .println(contains("MY_GROUP")); @@ -109,12 +110,12 @@ public class ProtoLogCommandHandlerTest { @Test public void handlesGroupStatusCommandOfUnregisteredGroups() { - Mockito.when(mProtoLogService.getGroups()).thenReturn(new String[] {}); + Mockito.when(mProtoLogConfigurationService.getGroups()).thenReturn(new String[] {}); final ProtoLogCommandHandler cmdHandler = - new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, - new String[] { "groups", "status", "MY_GROUP" }); + cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "groups", "status", "MY_GROUP" }); Mockito.verify(mPrintWriter, times(1)) .println(contains("MY_GROUP")); @@ -125,10 +126,10 @@ public class ProtoLogCommandHandlerTest { @Test public void handlesGroupStatusCommandWithNoGroups() { final ProtoLogCommandHandler cmdHandler = - new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, - new String[] { "groups", "status" }); + cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "groups", "status" }); Mockito.verify(mPrintWriter, times(1)) .println(contains("Incomplete command")); @@ -137,10 +138,10 @@ public class ProtoLogCommandHandlerTest { @Test public void handlesIncompleteLogcatCommand() { final ProtoLogCommandHandler cmdHandler = - new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, - new String[] { "logcat" }); + cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "logcat" }); Mockito.verify(mPrintWriter, times(1)) .println(contains("Incomplete command")); @@ -149,50 +150,52 @@ public class ProtoLogCommandHandlerTest { @Test public void handlesLogcatEnableCommand() { final ProtoLogCommandHandler cmdHandler = - new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, - new String[] { "logcat", "enable", "MY_GROUP" }); - Mockito.verify(mProtoLogService).enableProtoLogToLogcat("MY_GROUP"); + cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "logcat", "enable", "MY_GROUP" }); + Mockito.verify(mProtoLogConfigurationService).enableProtoLogToLogcat("MY_GROUP"); - cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, + cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "logcat", "enable", "MY_GROUP", "MY_OTHER_GROUP" }); - Mockito.verify(mProtoLogService) + Mockito.verify(mProtoLogConfigurationService) .enableProtoLogToLogcat("MY_GROUP", "MY_OTHER_GROUP"); } @Test public void handlesLogcatDisableCommand() { final ProtoLogCommandHandler cmdHandler = - new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, - new String[] { "logcat", "disable", "MY_GROUP" }); - Mockito.verify(mProtoLogService).disableProtoLogToLogcat("MY_GROUP"); + cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "logcat", "disable", "MY_GROUP" }); + Mockito.verify(mProtoLogConfigurationService).disableProtoLogToLogcat("MY_GROUP"); - cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, + cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "logcat", "disable", "MY_GROUP", "MY_OTHER_GROUP" }); - Mockito.verify(mProtoLogService) + Mockito.verify(mProtoLogConfigurationService) .disableProtoLogToLogcat("MY_GROUP", "MY_OTHER_GROUP"); } @Test public void handlesLogcatEnableCommandWithNoGroups() { final ProtoLogCommandHandler cmdHandler = - new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, - new String[] { "logcat", "enable" }); + cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "logcat", "enable" }); Mockito.verify(mPrintWriter).println(contains("Incomplete command")); } @Test public void handlesLogcatDisableCommandWithNoGroups() { final ProtoLogCommandHandler cmdHandler = - new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); - cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, - new String[] { "logcat", "disable" }); + cmdHandler.exec(mProtoLogConfigurationService, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "logcat", "disable" }); Mockito.verify(mPrintWriter).println(contains("Incomplete command")); } diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogServiceTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java index feac59c702ea..e1bdd777dc5f 100644 --- a/tests/Internal/src/com/android/internal/protolog/ProtoLogServiceTest.java +++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java @@ -67,7 +67,7 @@ import java.util.List; */ @Presubmit @RunWith(MockitoJUnitRunner.class) -public class ProtoLogServiceTest { +public class ProtoLogConfigurationServiceTest { private static final String TEST_GROUP = "MY_TEST_GROUP"; private static final String OTHER_TEST_GROUP = "MY_OTHER_TEST_GROUP"; @@ -128,7 +128,7 @@ public class ProtoLogServiceTest { private File mViewerConfigFile; - public ProtoLogServiceTest() throws IOException { + public ProtoLogConfigurationServiceTest() throws IOException { } @Before @@ -150,10 +150,12 @@ public class ProtoLogServiceTest { @Test public void canRegisterClientWithGroupsOnly() throws RemoteException { - final ProtoLogService service = new ProtoLogService(); + final ProtoLogConfigurationService service = new ProtoLogConfigurationService(); - final ProtoLogService.RegisterClientArgs args = new ProtoLogService.RegisterClientArgs() - .setGroups(new ProtoLogService.RegisterClientArgs.GroupConfig(TEST_GROUP, true)); + final ProtoLogConfigurationService.RegisterClientArgs args = + new ProtoLogConfigurationService.RegisterClientArgs() + .setGroups(new ProtoLogConfigurationService.RegisterClientArgs + .GroupConfig(TEST_GROUP, true)); service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); @@ -163,11 +165,13 @@ public class ProtoLogServiceTest { @Test public void willDumpViewerConfigOnlyOnceOnTraceStop() throws RemoteException, InvalidProtocolBufferException { - final ProtoLogService service = new ProtoLogService(); + final ProtoLogConfigurationService service = new ProtoLogConfigurationService(); - final ProtoLogService.RegisterClientArgs args = new ProtoLogService.RegisterClientArgs() - .setGroups(new ProtoLogService.RegisterClientArgs.GroupConfig(TEST_GROUP, true)) - .setViewerConfigFile(mViewerConfigFile.getAbsolutePath()); + final ProtoLogConfigurationService.RegisterClientArgs args = + new ProtoLogConfigurationService.RegisterClientArgs() + .setGroups(new ProtoLogConfigurationService.RegisterClientArgs + .GroupConfig(TEST_GROUP, true)) + .setViewerConfigFile(mViewerConfigFile.getAbsolutePath()); service.registerClient(mMockClient, args); service.registerClient(mSecondMockClient, args); @@ -196,14 +200,15 @@ public class ProtoLogServiceTest { @Test public void willDumpViewerConfigOnLastClientDisconnected() throws RemoteException, FileNotFoundException { - final ProtoLogService.ViewerConfigFileTracer tracer = - Mockito.mock(ProtoLogService.ViewerConfigFileTracer.class); - final ProtoLogService service = new ProtoLogService(tracer); - - final ProtoLogService.RegisterClientArgs args = new ProtoLogService.RegisterClientArgs() - .setGroups(new ProtoLogService.RegisterClientArgs.GroupConfig( - TEST_GROUP, true)) - .setViewerConfigFile(mViewerConfigFile.getAbsolutePath()); + final ProtoLogConfigurationService.ViewerConfigFileTracer tracer = + Mockito.mock(ProtoLogConfigurationService.ViewerConfigFileTracer.class); + final ProtoLogConfigurationService service = new ProtoLogConfigurationService(tracer); + + final ProtoLogConfigurationService.RegisterClientArgs args = + new ProtoLogConfigurationService.RegisterClientArgs() + .setGroups(new ProtoLogConfigurationService.RegisterClientArgs + .GroupConfig(TEST_GROUP, true)) + .setViewerConfigFile(mViewerConfigFile.getAbsolutePath()); service.registerClient(mMockClient, args); service.registerClient(mSecondMockClient, args); @@ -220,10 +225,11 @@ public class ProtoLogServiceTest { @Test public void sendEnableLoggingToLogcatToClient() throws RemoteException { - final var service = new ProtoLogService(); + final var service = new ProtoLogConfigurationService(); - final var args = new ProtoLogService.RegisterClientArgs() - .setGroups(new ProtoLogService.RegisterClientArgs.GroupConfig(TEST_GROUP, false)); + final var args = new ProtoLogConfigurationService.RegisterClientArgs() + .setGroups(new ProtoLogConfigurationService.RegisterClientArgs + .GroupConfig(TEST_GROUP, false)); service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); @@ -236,10 +242,12 @@ public class ProtoLogServiceTest { @Test public void sendDisableLoggingToLogcatToClient() throws RemoteException { - final ProtoLogService service = new ProtoLogService(); + final ProtoLogConfigurationService service = new ProtoLogConfigurationService(); - final ProtoLogService.RegisterClientArgs args = new ProtoLogService.RegisterClientArgs() - .setGroups(new ProtoLogService.RegisterClientArgs.GroupConfig(TEST_GROUP, true)); + final ProtoLogConfigurationService.RegisterClientArgs args = + new ProtoLogConfigurationService.RegisterClientArgs() + .setGroups(new ProtoLogConfigurationService.RegisterClientArgs + .GroupConfig(TEST_GROUP, true)); service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); @@ -252,10 +260,12 @@ public class ProtoLogServiceTest { @Test public void doNotSendLoggingToLogcatToClientWithoutRegisteredGroup() throws RemoteException { - final ProtoLogService service = new ProtoLogService(); + final ProtoLogConfigurationService service = new ProtoLogConfigurationService(); - final ProtoLogService.RegisterClientArgs args = new ProtoLogService.RegisterClientArgs() - .setGroups(new ProtoLogService.RegisterClientArgs.GroupConfig(TEST_GROUP, false)); + final ProtoLogConfigurationService.RegisterClientArgs args = + new ProtoLogConfigurationService.RegisterClientArgs() + .setGroups(new ProtoLogConfigurationService.RegisterClientArgs + .GroupConfig(TEST_GROUP, false)); service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); @@ -267,14 +277,16 @@ public class ProtoLogServiceTest { @Test public void handlesToggleToLogcatBeforeClientIsRegistered() throws RemoteException { - final ProtoLogService service = new ProtoLogService(); + final ProtoLogConfigurationService service = new ProtoLogConfigurationService(); Truth.assertThat(service.getGroups()).asList().doesNotContain(TEST_GROUP); service.enableProtoLogToLogcat(TEST_GROUP); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); - final ProtoLogService.RegisterClientArgs args = new ProtoLogService.RegisterClientArgs() - .setGroups(new ProtoLogService.RegisterClientArgs.GroupConfig(TEST_GROUP, false)); + final ProtoLogConfigurationService.RegisterClientArgs args = + new ProtoLogConfigurationService.RegisterClientArgs() + .setGroups(new ProtoLogConfigurationService.RegisterClientArgs + .GroupConfig(TEST_GROUP, false)); service.registerClient(mMockClient, args); Mockito.verify(mMockClient).toggleLogcat(eq(true), diff --git a/tools/sdkparcelables/src/com/android/sdkparcelables/Main.kt b/tools/sdkparcelables/src/com/android/sdkparcelables/Main.kt index 0b619488c49c..9c443324defb 100644 --- a/tools/sdkparcelables/src/com/android/sdkparcelables/Main.kt +++ b/tools/sdkparcelables/src/com/android/sdkparcelables/Main.kt @@ -23,13 +23,21 @@ import java.io.IOException import java.util.zip.ZipFile fun main(args: Array<String>) { - if (args.size != 2) { + if (args.size < 2 || args.size > 3) { usage() } val zipFileName = args[0] val aidlFileName = args[1] + var stable = false + if (args.size == 3) { + if (args[2] != "--guarantee_stable") { + usage() + } + stable = true + } + val zipFile: ZipFile try { @@ -55,6 +63,9 @@ fun main(args: Array<String>) { val outFile = File(aidlFileName) val outWriter = outFile.bufferedWriter() for (parcelable in parcelables) { + if (stable) { + outWriter.write("@JavaOnlyStableParcelable ") + } outWriter.write("parcelable ") outWriter.write(parcelable.replace('/', '.').replace('$', '.')) outWriter.write(";\n") diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java index b0f68f7870ee..f68ae2c7e249 100644 --- a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java +++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java @@ -98,6 +98,18 @@ public class SharedConnectivityManager { } @Override + public void onServiceDisconnected() { + if (mCallback != null) { + final long token = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.onServiceDisconnected()); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } + + @Override public void onHotspotNetworksUpdated(@NonNull List<HotspotNetwork> networks) { if (mCallback != null) { final long token = Binder.clearCallingIdentity(); @@ -247,13 +259,13 @@ public class SharedConnectivityManager { mService = null; synchronized (mProxyDataLock) { if (!mCallbackProxyCache.isEmpty()) { - mCallbackProxyCache.keySet().forEach( - SharedConnectivityClientCallback::onServiceDisconnected); + mCallbackProxyCache.values().forEach( + SharedConnectivityCallbackProxy::onServiceDisconnected); mCallbackProxyCache.clear(); } if (!mProxyMap.isEmpty()) { - mProxyMap.keySet().forEach( - SharedConnectivityClientCallback::onServiceDisconnected); + mProxyMap.values().forEach( + SharedConnectivityCallbackProxy::onServiceDisconnected); mProxyMap.clear(); } } diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/service/ISharedConnectivityCallback.aidl b/wifi/java/src/android/net/wifi/sharedconnectivity/service/ISharedConnectivityCallback.aidl index 521f94367f6f..7b892af3c529 100644 --- a/wifi/java/src/android/net/wifi/sharedconnectivity/service/ISharedConnectivityCallback.aidl +++ b/wifi/java/src/android/net/wifi/sharedconnectivity/service/ISharedConnectivityCallback.aidl @@ -32,4 +32,5 @@ interface ISharedConnectivityCallback { oneway void onKnownNetworkConnectionStatusChanged(in KnownNetworkConnectionStatus status); oneway void onSharedConnectivitySettingsChanged(in SharedConnectivitySettingsState state); oneway void onServiceConnected(); + oneway void onServiceDisconnected(); } |